aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet/App/Controller/Reports.pm
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Reports.pm')
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm359
1 files changed, 359 insertions, 0 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm
new file mode 100644
index 000000000..f2bb13ae4
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Reports.pm
@@ -0,0 +1,359 @@
+package FixMyStreet::App::Controller::Reports;
+use Moose;
+use namespace::autoclean;
+
+use POSIX qw(strcoll);
+use mySociety::MaPit;
+use mySociety::VotingArea;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Reports - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Catalyst Controller.
+
+=head1 METHODS
+
+=cut
+
+=head2 index
+
+Show the summary page of all reports.
+
+=cut
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->response->header('Cache-Control' => 'max-age=3600');
+
+ # Fetch all areas of the types we're interested in
+ my @area_types = $c->cobrand->area_types;
+ my $areas_info = mySociety::MaPit::call('areas', \@area_types,
+ min_generation => $c->cobrand->area_min_generation
+ );
+
+ # For each area, add its link and perhaps alter its name if we need to for
+ # places with the same name.
+ foreach (values %$areas_info) {
+ $_->{url} = $c->uri_for( '/reports/' . $c->cobrand->short_name( $_, $areas_info ) );
+ if ($_->{parent_area} && $_->{url} =~ /,|%2C/) {
+ $_->{name} .= ', ' . $areas_info->{$_->{parent_area}}{name};
+ }
+ }
+
+ $c->stash->{areas_info} = $areas_info;
+ my @keys = sort { strcoll($areas_info->{$a}{name}, $areas_info->{$b}{name}) } keys %$areas_info;
+ $c->stash->{areas_info_sorted} = [ map { $areas_info->{$_} } @keys ];
+
+ $c->forward( 'load_problems' );
+ $c->forward( 'group_problems' );
+}
+
+=head2 index
+
+Show the summary page for a particular council.
+
+=cut
+
+sub council : Path : Args(1) {
+ my ( $self, $c, $council ) = @_;
+ $c->detach( 'ward', [ $council ] );
+}
+
+=head2 index
+
+Show the summary page for a particular ward.
+
+=cut
+
+sub ward : Path : Args(2) {
+ my ( $self, $c, $council, $ward ) = @_;
+
+ $c->forward( 'council_check', [ $council ] );
+ $c->forward( 'ward_check', [ $ward ] )
+ if $ward;
+ $c->forward( 'load_parent' );
+ $c->forward( 'load_problems' );
+ $c->forward( 'group_problems' );
+ $c->forward( 'sort_problems' );
+
+ $c->stash->{rss_url} = '/rss/reports/'
+ . $c->cobrand->short_name( $c->stash->{council}, $c->stash->{areas_info} );
+ $c->stash->{rss_url} .= '/' . $c->cobrand->short_name( $c->stash->{ward} )
+ if $c->stash->{ward};
+}
+
+sub rss_council : Regex('^rss/(reports|area)$') : Args(1) {
+ my ( $self, $c, $council ) = @_;
+ $c->detach( 'rss_ward', [ $council ] );
+}
+
+sub rss_ward : Regex('^rss/(reports|area)$') : Args(2) {
+ my ( $self, $c, $council, $ward ) = @_;
+
+ my ( $rss ) = $c->req->captures->[0];
+
+ $c->stash->{rss} = 1;
+
+ $c->forward( 'council_check', [ $council ] );
+ $c->forward( 'ward_check', [ $ward ] ) if $ward;
+
+ if ($rss eq 'area' && $c->stash->{council}{type} ne 'DIS' && $c->stash->{council}{type} ne 'CTY') {
+ # Two possibilites are the same for one-tier councils, so redirect one to the other
+ $c->detach( 'redirect_area' );
+ }
+
+ my $url = $c->cobrand->short_name( $c->stash->{council} );
+ $url .= '/' . $c->cobrand->short_name( $c->stash->{ward} ) if $c->stash->{ward};
+ $c->stash->{qs} = "/$url";
+
+ my @params;
+ push @params, $c->stash->{council}->{id} if $rss eq 'reports';
+ push @params, $c->stash->{ward}
+ ? $c->stash->{ward}->{id}
+ : $c->stash->{council}->{id};
+ $c->stash->{db_params} = [ @params ];
+
+ if ( $rss eq 'area' && $c->stash->{ward} ) {
+ # All problems within a particular ward
+ $c->stash->{type} = 'area_problems';
+ $c->stash->{title_params} = { NAME => $c->stash->{ward}{name} };
+ $c->stash->{db_params} = [ $c->stash->{ward}->{id} ];
+ } elsif ( $rss eq 'area' ) {
+ # Problems within a particular council
+ $c->stash->{type} = 'area_problems';
+ $c->stash->{title_params} = { NAME => $c->stash->{council}{name} };
+ $c->stash->{db_params} = [ $c->stash->{council}->{id} ];
+ } elsif ($c->stash->{ward}) {
+ # Problems sent to a council, restricted to a ward
+ $c->stash->{type} = 'ward_problems';
+ $c->stash->{title_params} = { COUNCIL => $c->stash->{council}{name}, WARD => $c->stash->{ward}{name} };
+ $c->stash->{db_params} = [ $c->stash->{council}->{id}, $c->stash->{ward}->{id} ];
+ } else {
+ # Problems sent to a council
+ $c->stash->{type} = 'council_problems';
+ $c->stash->{title_params} = { COUNCIL => $c->stash->{council}{name} };
+ $c->stash->{db_params} = [ $c->stash->{council}->{id}, $c->stash->{council}->{id} ];
+ }
+
+ # Send on to the RSS generation
+ $c->forward( '/rss/output' );
+}
+
+=head2 council_check
+
+This action checks the council name (or code) given in a URI exists, is valid
+and so on. If it is, it stores the Area in the stash, otherwise it redirects
+to the all reports page.
+
+=cut
+
+sub council_check : Private {
+ my ( $self, $c, $q_council ) = @_;
+
+ $q_council =~ s/\+/ /g;
+
+ # Manual misspelling redirect
+ if ($q_council =~ /^rhondda cynon taff$/i) {
+ my $url = $c->uri_for( '/reports/rhondda+cynon+taf' );
+ $c->res->redirect( $url );
+ $c->detach();
+ }
+
+ # Check cobrand specific incantations - e.g. ONS codes for UK,
+ # Oslo/ kommunes sharing a name in Norway
+ return if $c->cobrand->reports_council_check( $c, $q_council );
+
+ # If we're passed an ID number (don't think this is used anywhere, it
+ # certainly shouldn't be), just look that up on MaPit and redirect
+ if ($q_council =~ /^\d+$/) {
+ my $council = mySociety::MaPit::call('area', $q_council);
+ $c->detach( 'redirect_index') if $council->{error};
+ $c->stash->{council} = $council;
+ $c->detach( 'redirect_area' );
+ }
+
+ # We must now have a string to check
+ my @area_types = $c->cobrand->area_types;
+ my $areas = mySociety::MaPit::call( 'areas', $q_council,
+ type => \@area_types,
+ min_generation => $c->cobrand->area_min_generation
+ );
+ if (keys %$areas == 1) {
+ ($c->stash->{council}) = values %$areas;
+ return;
+ } else {
+ foreach (keys %$areas) {
+ if ($areas->{$_}->{name} eq $q_council || $areas->{$_}->{name} =~ /^\Q$q_council\E (Borough|City|District|County) Council$/) {
+ $c->stash->{council} = $areas->{$_};
+ return;
+ }
+ }
+ }
+
+ # No result, bad council name.
+ $c->detach( 'redirect_index' );
+}
+
+=head2 ward_check
+
+This action checks the ward name from a URI exists and is part of the right
+parent, already found with council_check. It either stores the ward Area if
+okay, or redirects to the council page if bad.
+This is currently only used in the UK, hence the use of mySociety::VotingArea.
+
+=cut
+
+sub ward_check : Private {
+ my ( $self, $c, $ward ) = @_;
+
+ $ward =~ s/\+/ /g;
+ my $council = $c->stash->{council};
+
+ my $qw = mySociety::MaPit::call('areas', $ward,
+ type => $mySociety::VotingArea::council_child_types,
+ min_generation => $c->cobrand->area_min_generation
+ );
+ foreach my $id (sort keys %$qw) {
+ if ($qw->{$id}->{parent_area} == $council->{id}) {
+ $c->stash->{ward} = $qw->{$id};
+ return;
+ }
+ }
+ # Given a false ward name
+ $c->detach( 'redirect_area' );
+}
+
+sub load_parent : Private {
+ my ( $self, $c ) = @_;
+
+ my $council = $c->stash->{council};
+ my $areas_info;
+ if ($council->{parent_area}) {
+ $c->stash->{areas_info} = mySociety::MaPit::call('areas', [ $council->{id}, $council->{parent_area} ])
+ } else {
+ $c->stash->{areas_info} = { $council->{id} => $council };
+ }
+}
+
+sub load_problems : Private {
+ my ( $self, $c ) = @_;
+
+ my $where = {
+ state => [ 'confirmed', 'fixed' ]
+ };
+ if ($c->stash->{ward}) {
+ $where->{areas} = { 'like', '%' . $c->stash->{ward}->{id} . '%' }; # FIXME Check this is secure
+ } elsif ($c->stash->{council}) {
+ $where->{areas} = { 'like', '%' . $c->stash->{council}->{id} . '%' };
+ }
+ my $problems = $c->cobrand->problems->search(
+ $where,
+ {
+ columns => [
+ 'id', 'title', 'detail', 'council', 'state', 'areas',
+ { duration => { extract => "epoch from current_timestamp-lastupdate" } },
+ { age => { extract => "epoch from current_timestamp-confirmed" } },
+ ],
+ order_by => { -desc => 'id' },
+ }
+ );
+ $c->stash->{problems} = [ $problems->all ];
+
+ return 1;
+}
+
+sub group_problems : Private {
+ my ( $self, $c ) = @_;
+
+ my ( %fixed, %open );
+ my $re_councils = join('|', keys %{$c->stash->{areas_info}});
+ foreach my $row (@{$c->stash->{problems}}) {
+ if (!$row->council) {
+ # Problem was not sent to any council, add to possible councils
+ my $areas = $row->areas;
+ while ($areas =~ /,($re_councils)(?=,)/g) {
+ add_row($row, $1, \%fixed, \%open);
+ }
+ } else {
+ # Add to councils it was sent to
+ foreach ($row->councils) {
+ next if $c->stash->{council} && $_ != $c->stash->{council}->{id};
+ add_row($row, $_, \%fixed, \%open);
+ }
+ }
+ }
+
+ $c->stash->{fixed} = \%fixed;
+ $c->stash->{open} = \%open;
+
+ return 1;
+}
+
+sub sort_problems : Private {
+ my ( $self, $c ) = @_;
+
+ my $id = $c->stash->{council}->{id};
+ my $fixed = $c->stash->{fixed};
+ my $open = $c->stash->{open};
+
+ foreach (qw/new old/) {
+ $c->stash->{fixed}{$id}{$_} = [ sort { $a->get_column('duration') <=> $b->get_column('duration') } @{$fixed->{$id}{$_}} ]
+ if $fixed->{$id}{$_};
+ }
+ foreach (qw/new older unknown/) {
+ $c->stash->{open}{$id}{$_} = [ sort { $a->get_column('age') <=> $b->get_column('age') } @{$open->{$id}{$_}} ]
+ if $open->{$id}{$_};
+ }
+}
+
+sub redirect_index : Private {
+ my ( $self, $c ) = @_;
+ my $url = '/reports';
+ $c->res->redirect( $c->uri_for($url) );
+}
+
+sub redirect_area : Private {
+ my ( $self, $c ) = @_;
+ my $url = '';
+ $url .= "/rss" if $c->stash->{rss};
+ $url .= '/reports';
+ $url .= '/' . $c->cobrand->short_name( $c->stash->{council} );
+ $url .= '/' . $c->cobrand->short_name( $c->stash->{ward} )
+ if $c->stash->{ward};
+ $c->res->redirect( $c->uri_for($url) );
+}
+
+sub add_row {
+ my ($row, $council, $fixed, $open) = @_;
+ my $fourweeks = 4*7*24*60*60;
+ my $duration = ($row->get_column('duration') > 2 * $fourweeks) ? 'old' : 'new';
+ my $type = ($row->get_column('duration') > 2 * $fourweeks)
+ ? 'unknown'
+ : ($row->get_column('age') > $fourweeks ? 'older' : 'new');
+ # Fixed problems are either old or new
+ push @{$fixed->{$council}{$duration}}, $row if $row->state eq 'fixed';
+ # Open problems are either unknown, older, or new
+ push @{$open->{$council}{$type}}, $row if $row->state eq 'confirmed';
+}
+
+=head1 AUTHOR
+
+Matthew Somerville
+
+=head1 LICENSE
+
+Copyright (c) 2011 UK Citizens Online Democracy. All rights reserved.
+Licensed under the Affero GPL.
+
+=cut
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+