diff options
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Dashboard.pm')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Dashboard.pm | 207 |
1 files changed, 144 insertions, 63 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm index 790e7ec29..bd60f8570 100644 --- a/perllib/FixMyStreet/App/Controller/Dashboard.pm +++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm @@ -3,10 +3,12 @@ use Moose; use namespace::autoclean; use DateTime; +use Encode; use JSON::MaybeXS; use Path::Tiny; use Text::CSV; use Time::Piece; +use FixMyStreet::DateRange; BEGIN { extends 'Catalyst::Controller'; } @@ -54,6 +56,18 @@ Checks if we can view this page, and if not redirect to 404. sub check_page_allowed : Private { my ( $self, $c ) = @_; + # dashboard_permission can return undef (if not present, or to carry on + # with default behaviour), a body ID to use that body for results, or 0 + # to refuse access entirely + my $cobrand_check = $c->cobrand->call_hook('dashboard_permission'); + if (defined $cobrand_check) { + if ($cobrand_check) { + $cobrand_check = $c->model('DB::Body')->find({ id => $cobrand_check }); + } + $c->detach( '/page_error_404_not_found' ) if !$cobrand_check; + return $cobrand_check; + } + $c->detach( '/auth/redirect' ) unless $c->user_exists; $c->detach( '/page_error_404_not_found' ) @@ -93,13 +107,18 @@ sub index : Path : Args(0) { # 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; + $c->stash->{ward} = [ $c->get_param_list('ward') ]; + if ($c->user_exists) { + if (my @areas = @{$c->user->area_ids || []}) { + $c->stash->{ward} = $c->user->area_ids; + $c->stash->{body_name} = join " / ", sort map { $children->{$_}->{name} } grep { $children->{$_} } @areas; + } } } else { - my @bodies = $c->model('DB::Body')->active->translated->with_area_count->all_sorted; + my @bodies = $c->model('DB::Body')->search(undef, { + columns => [ "id", "name" ], + })->active->translated->with_area_count->all_sorted; + $c->stash->{ward} = []; $c->stash->{bodies} = \@bodies; } @@ -110,10 +129,14 @@ sub index : Path : Args(0) { $c->stash->{end_date} = $c->get_param('end_date'); $c->stash->{q_state} = $c->get_param('state') || ''; - $c->forward('construct_rs_filter'); + $c->forward('construct_rs_filter', [ $c->get_param('updates') ]); if ( $c->get_param('export') ) { - $c->forward('export_as_csv'); + if ($c->get_param('updates')) { + $c->forward('export_as_csv_updates'); + } else { + $c->forward('export_as_csv'); + } } else { $c->forward('generate_grouped_data'); $self->generate_summary_figures($c); @@ -121,36 +144,39 @@ sub index : Path : Args(0) { } sub construct_rs_filter : Private { - my ($self, $c) = @_; + my ($self, $c, $updates) = @_; my %where; - $where{areas} = { 'like', '%,' . $c->stash->{ward} . ',%' } - if $c->stash->{ward}; + $where{areas} = [ map { { 'like', "%,$_,%" } } @{$c->stash->{ward}} ] + if @{$c->stash->{ward}}; $where{category} = $c->stash->{category} if $c->stash->{category}; + my $table_name = $updates ? 'problem' : 'me'; + 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() ]; + $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->fixed_states() ]; } elsif ( $state ) { - $where{'me.state'} = $state; + $where{"$table_name.state"} = $state; } else { - $where{'me.state'} = [ FixMyStreet::DB::Result::Problem->visible_states() ]; + $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->visible_states() ]; } - my $dtf = $c->model('DB')->storage->datetime_parser; - - my $start_date = $dtf->parse_datetime($c->stash->{start_date}); - $where{'me.confirmed'} = { '>=', $dtf->format_datetime($start_date) }; + my $days30 = DateTime->now(time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone)->subtract(days => 30); + $days30->truncate( to => 'day' ); - if (my $end_date = $c->stash->{end_date}) { - my $one_day = DateTime::Duration->new( days => 1 ); - $end_date = $dtf->parse_datetime($end_date) + $one_day; - $where{'me.confirmed'} = [ -and => $where{'me.confirmed'}, { '<', $dtf->format_datetime($end_date) } ]; - } + my $range = FixMyStreet::DateRange->new( + start_date => $c->stash->{start_date}, + start_default => $days30, + end_date => $c->stash->{end_date}, + formatter => $c->model('DB')->storage->datetime_parser, + ); + $where{"$table_name.confirmed"} = $range->sql; $c->stash->{params} = \%where; - $c->stash->{problems_rs} = $c->cobrand->problems->to_body($c->stash->{body})->search( \%where ); + my $rs = $updates ? $c->cobrand->updates : $c->cobrand->problems; + $c->stash->{objects_rs} = $rs->to_body($c->stash->{body})->search( \%where ); } sub generate_grouped_data : Private { @@ -182,7 +208,7 @@ sub generate_grouped_data : Private { @groups = qw/category state/; %grouped = map { $_->category => {} } @{$c->stash->{contacts}}; } - my $problems = $c->stash->{problems_rs}->search(undef, { + my $problems = $c->stash->{objects_rs}->search(undef, { group_by => [ map { ref $_ ? $_->{-as} : $_ } @groups ], select => [ @groups, { count => 'me.id' } ], as => [ @groups == 2 ? qw/key1 key2 count/ : qw/key1 count/ ], @@ -238,7 +264,7 @@ sub generate_summary_figures { # problems this month by state $c->stash->{"summary_$_"} = 0 for values %$state_map; - $c->stash->{summary_open} = $c->stash->{problems_rs}->count; + $c->stash->{summary_open} = $c->stash->{objects_rs}->count; my $params = $c->stash->{params}; $params = { map { my $n = $_; s/me\./problem\./ unless /me\.confirmed/; $_ => $params->{$n} } keys %$params }; @@ -268,15 +294,54 @@ sub generate_summary_figures { sub generate_body_response_time : Private { my ( $self, $c ) = @_; - my $avg = $c->stash->{body}->calculate_average; + my $avg = $c->stash->{body}->calculate_average($c->cobrand->call_hook("body_responsiveness_threshold")); $c->stash->{body_average} = $avg ? int($avg / 60 / 60 / 24 + 0.5) : 0; } +sub csv_filename { + my ($self, $c, $updates) = @_; + my %where = ( + category => $c->stash->{category}, + state => $c->stash->{q_state}, + ward => join(',', @{$c->stash->{ward}}), + ); + $where{body} = $c->stash->{body}->id if $c->stash->{body}; + join '-', + $c->req->uri->host, + $updates ? ('updates') : (), + map { + my $value = $where{$_}; + (defined $value and length $value) ? ($_, $value) : () + } sort keys %where +}; + +sub export_as_csv_updates : Private { + my ($self, $c) = @_; + + my $csv = $c->stash->{csv} = { + objects => $c->stash->{objects_rs}->search_rs({}, { + order_by => ['me.confirmed', 'me.id'], + '+columns' => ['problem.bodies_str'], + }), + headers => [ + 'Report ID', 'Update ID', 'Date', 'Status', 'Problem state', + 'Text', 'User Name', 'Reported As', + ], + columns => [ + 'problem_id', 'id', 'confirmed', 'state', 'problem_state', + 'text', 'user_name_display', 'reported_as', + ], + filename => $self->csv_filename($c, 1), + }; + $c->cobrand->call_hook("dashboard_export_updates_add_columns"); + $c->forward('generate_csv'); +} + sub export_as_csv : Private { my ($self, $c) = @_; my $csv = $c->stash->{csv} = { - problems => $c->stash->{problems_rs}->search_rs({}, { + objects => $c->stash->{objects_rs}->search_rs({}, { prefetch => 'comments', order_by => ['me.confirmed', 'me.id'], }), @@ -298,6 +363,8 @@ sub export_as_csv : Private { 'Easting', 'Northing', 'Report URL', + 'Site Used', + 'Reported As', ], columns => [ 'id', @@ -317,23 +384,12 @@ sub export_as_csv : Private { 'local_coords_x', 'local_coords_y', 'url', + 'site_used', + 'reported_as', ], - filename => do { - my %where = ( - 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 { - my $value = $where{$_}; - (defined $value and length $value) ? ($_, $value) : () - } sort keys %where - }, + filename => $self->csv_filename($c, 0), }; - $c->cobrand->call_hook("dashboard_export_add_columns"); + $c->cobrand->call_hook("dashboard_export_problems_add_columns"); $c->forward('generate_csv'); } @@ -354,24 +410,44 @@ hashref of extra data to include that can be used by 'columns'. sub generate_csv : Private { my ($self, $c) = @_; + my $filename = $c->stash->{csv}->{filename}; + $c->res->content_type('text/csv; charset=utf-8'); + $c->res->header('content-disposition' => "attachment; filename=\"${filename}.csv\""); + + # Emit a header (copying Drupal's naming) telling an intermediary (e.g. + # Varnish) not to buffer the output. Varnish will need to know this, e.g.: + # if (beresp.http.Surrogate-Control ~ "BigPipe/1.0") { + # set beresp.do_stream = true; + # set beresp.ttl = 0s; + # } + $c->res->header('Surrogate-Control' => 'content="BigPipe/1.0"'); + + # Tell nginx not to buffer this response + $c->res->header('X-Accel-Buffering' => 'no'); + + # Define an empty body so the web view doesn't get added at the end + $c->res->body(""); + + # Old parameter renaming + $c->stash->{csv}->{objects} //= $c->stash->{csv}->{problems}; + my $csv = Text::CSV->new({ binary => 1, eol => "\n" }); - $csv->combine(@{$c->stash->{csv}->{headers}}); - my @body = ($csv->string); + $csv->print($c->response, $c->stash->{csv}->{headers}); my $fixed_states = FixMyStreet::DB::Result::Problem->fixed_states; my $closed_states = FixMyStreet::DB::Result::Problem->closed_states; my %asked_for = map { $_ => 1 } @{$c->stash->{csv}->{columns}}; - my $problems = $c->stash->{csv}->{problems}; - while ( my $report = $problems->next ) { - my $hashref = $report->as_hashref($c, \%asked_for); + my $objects = $c->stash->{csv}->{objects}; + while ( my $obj = $objects->next ) { + my $hashref = $obj->as_hashref($c, \%asked_for); - $hashref->{user_name_display} = $report->anonymous - ? '(anonymous)' : $report->name; + $hashref->{user_name_display} = $obj->anonymous + ? '(anonymous)' : $obj->name; if ($asked_for{acknowledged}) { - for my $comment ($report->comments) { + for my $comment ($obj->comments) { my $problem_state = $comment->problem_state or next; next unless $comment->state eq 'confirmed'; next if $problem_state eq 'confirmed'; @@ -392,28 +468,33 @@ sub generate_csv : Private { split ',', $hashref->{areas}; } - ($hashref->{local_coords_x}, $hashref->{local_coords_y}) = - $report->local_coords; - $hashref->{url} = join '', $c->cobrand->base_url_for_report($report), $report->url; + if ($obj->can('local_coords') && $asked_for{local_coords_x}) { + ($hashref->{local_coords_x}, $hashref->{local_coords_y}) = + $obj->local_coords; + } + if ($obj->can('url')) { + my $base = $c->cobrand->base_url_for_report($obj->can('problem') ? $obj->problem : $obj); + $hashref->{url} = join '', $base, $obj->url; + } + + $hashref->{site_used} = $obj->can('service') ? ($obj->service || $obj->cobrand) : $obj->cobrand; + + $hashref->{reported_as} = $obj->get_extra_metadata('contributed_as') || ''; if (my $fn = $c->stash->{csv}->{extra_data}) { - my $extra = $fn->($report); + my $extra = $fn->($obj); $hashref = { %$hashref, %$extra }; } - $csv->combine( + $csv->print($c->response, [ + map { + $_ = encode('UTF-8', $_) if $_; + } @{$hashref}{ @{$c->stash->{csv}->{columns}} }, - ); - - push @body, $csv->string; + ] ); } - - my $filename = $c->stash->{csv}->{filename}; - $c->res->content_type('text/csv; charset=utf-8'); - $c->res->header('content-disposition' => "attachment; filename=${filename}.csv"); - $c->res->body( join "", @body ); } =head1 AUTHOR |