package FixMyStreet::App::Controller::Reports; use Moose; use namespace::autoclean; use Problems; use POSIX qw(strcoll); # use FixMyStreet::Alert; 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' ], %{ Problems::site_restriction() } }; 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 $current_timestamp = Problems::current_timestamp(); my $problems = $c->model('DB::Problem')->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;