aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm270
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm54
-rw-r--r--perllib/FixMyStreet/Cobrand/BathNES.pm49
-rw-r--r--perllib/FixMyStreet/Cobrand/Buckinghamshire.pm7
-rw-r--r--perllib/FixMyStreet/Cobrand/Oxfordshire.pm7
-rw-r--r--perllib/FixMyStreet/Cobrand/TfL.pm43
-rw-r--r--perllib/FixMyStreet/Cobrand/Zurich.pm14
-rw-r--r--perllib/FixMyStreet/Reporting.pm337
8 files changed, 421 insertions, 360 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
index 6fd4154b9..0d0a704bb 100644
--- a/perllib/FixMyStreet/App/Controller/Dashboard.pm
+++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm
@@ -6,9 +6,9 @@ use DateTime;
use Encode;
use JSON::MaybeXS;
use Path::Tiny;
-use Text::CSV;
use Time::Piece;
use FixMyStreet::DateRange;
+use FixMyStreet::Reporting;
BEGIN { extends 'Catalyst::Controller'; }
@@ -135,14 +135,11 @@ 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->get_param('updates') ]);
+ my $reporting = $c->forward('construct_rs_filter', [ $c->get_param('updates') ]);
if ( $c->get_param('export') ) {
- if ($c->get_param('updates')) {
- $c->forward('export_as_csv_updates');
- } else {
- $c->forward('export_as_csv');
- }
+ $reporting->csv_parameters;
+ $reporting->generate_csv_http($c);
} else {
$c->forward('generate_grouped_data');
$self->generate_summary_figures($c);
@@ -152,37 +149,19 @@ sub index : Path : Args(0) {
sub construct_rs_filter : Private {
my ($self, $c, $updates) = @_;
- my $table_name = $updates ? 'problem' : 'me';
-
- my %where;
- $where{areas} = [ map { { 'like', "%,$_,%" } } @{$c->stash->{ward}} ]
- if @{$c->stash->{ward}};
- $where{"$table_name.category"} = $c->stash->{category}
- if $c->stash->{category};
-
- my $state = $c->stash->{q_state};
- if ( FixMyStreet::DB::Result::Problem->fixed_states->{$state} ) { # Probably fixed - council
- $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
- } elsif ( $state ) {
- $where{"$table_name.state"} = $state;
- } else {
- $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->visible_states() ];
- }
-
- my $days30 = DateTime->now(time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone)->subtract(days => 30);
- $days30->truncate( to => 'day' );
-
- my $range = FixMyStreet::DateRange->new(
+ my $reporting = FixMyStreet::Reporting->new(
+ type => $updates ? 'updates' : 'problems',
+ category => $c->stash->{category},
+ state => $c->stash->{q_state},
+ wards => $c->stash->{ward},
+ body => $c->stash->{body} || undef,
start_date => $c->stash->{start_date},
- start_default => $days30,
end_date => $c->stash->{end_date},
- formatter => $c->model('DB')->storage->datetime_parser,
+ user => $c->user_exists ? $c->user->obj : undef,
);
- $where{"$table_name.confirmed"} = $range->sql;
- $c->stash->{params} = \%where;
- my $rs = $updates ? $c->cobrand->updates : $c->cobrand->problems;
- $c->stash->{objects_rs} = $rs->to_body($c->stash->{body})->search( \%where );
+ $c->stash($reporting->construct_rs_filter);
+ return $reporting;
}
sub generate_grouped_data : Private {
@@ -304,229 +283,6 @@ sub generate_body_response_time : Private {
$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'],
- cursor_page_size => 1000,
- }),
- 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),
- user => $c->user_exists ? $c->user->obj : undef,
- };
- $c->cobrand->call_hook(dashboard_export_updates_add_columns => $csv);
- $c->forward('generate_csv');
-}
-
-sub export_as_csv : Private {
- my ($self, $c) = @_;
-
- my $groups = $c->cobrand->enable_category_groups ? 1 : 0;
- my $join = ['comments'];
- my $columns = ['comments.id', 'comments.problem_state', 'comments.state', 'comments.confirmed', 'comments.mark_fixed'];
- if ($groups) {
- push @$join, 'contact';
- push @$columns, 'contact.id', 'contact.extra';
- }
- my $csv = $c->stash->{csv} = {
- objects => $c->stash->{objects_rs}->search_rs({}, {
- join => $join,
- collapse => 1,
- '+columns' => $columns,
- order_by => ['me.confirmed', 'me.id'],
- cursor_page_size => 1000,
- }),
- headers => [
- 'Report ID',
- 'Title',
- 'Detail',
- 'User Name',
- 'Category',
- $groups ? ('Subcategory') : (),
- 'Created',
- 'Confirmed',
- 'Acknowledged',
- 'Fixed',
- 'Closed',
- 'Status',
- 'Latitude', 'Longitude',
- 'Query',
- 'Ward',
- 'Easting',
- 'Northing',
- 'Report URL',
- 'Site Used',
- 'Reported As',
- ],
- columns => [
- 'id',
- 'title',
- 'detail',
- 'user_name_display',
- 'category',
- $groups ? ('subcategory') : (),
- 'created',
- 'confirmed',
- 'acknowledged',
- 'fixed',
- 'closed',
- 'state',
- 'latitude', 'longitude',
- 'postcode',
- 'wards',
- 'local_coords_x',
- 'local_coords_y',
- 'url',
- 'site_used',
- 'reported_as',
- ],
- filename => $self->csv_filename($c, 0),
- user => $c->user_exists ? $c->user->obj : undef,
- category => $c->stash->{category},
- contacts => $c->stash->{contacts},
- };
- $c->cobrand->call_hook(dashboard_export_problems_add_columns => $csv);
- $c->forward('generate_csv');
-}
-
-=head2 generate_csv
-
-Generates a CSV output, given a 'csv' stash hashref containing:
-* filename: filename to be used in output
-* problems: a resultset of the rows to output
-* headers: an arrayref of the header row strings
-* columns: an arrayref of the columns (looked up in the row's as_hashref, plus
-the following: user_name_display, acknowledged, fixed, closed, wards,
-local_coords_x, local_coords_y, url).
-* extra_data: If present, a function that is passed the report and returns a
-hashref of extra data to include that can be used by 'columns'.
-
-=cut
-
-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->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 $objects = $c->stash->{csv}->{objects};
- while ( my $obj = $objects->next ) {
- my $hashref = $obj->as_hashref(\%asked_for);
-
- $hashref->{user_name_display} = $obj->anonymous
- ? '(anonymous)' : $obj->name;
-
- if ($asked_for{acknowledged}) {
- 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';
- $hashref->{acknowledged} //= $comment->confirmed;
- $hashref->{fixed} //= $fixed_states->{ $problem_state } || $comment->mark_fixed ?
- $comment->confirmed : undef;
- if ($closed_states->{ $problem_state }) {
- $hashref->{closed} = $comment->confirmed;
- last;
- }
- }
- }
-
- if ($asked_for{wards}) {
- $hashref->{wards} = join ', ',
- map { $c->stash->{children}->{$_}->{name} }
- grep {$c->stash->{children}->{$_} }
- split ',', $hashref->{areas};
- }
-
- if ($obj->can('local_coords') && $asked_for{local_coords_x}) {
- ($hashref->{local_coords_x}, $hashref->{local_coords_y}) =
- $obj->local_coords;
- }
-
- if ($asked_for{subcategory}) {
- my $group = $obj->contact ? $obj->contact->groups : [];
- $group = join(',', @$group);
- if ($group) {
- $hashref->{subcategory} = $obj->category;
- $hashref->{category} = $group;
- }
- }
-
- 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->($obj);
- $hashref = { %$hashref, %$extra };
- }
-
- $csv->print($c->response, [
- @{$hashref}{
- @{$c->stash->{csv}->{columns}}
- },
- ] );
- }
-}
-
sub heatmap : Local : Args(0) {
my ($self, $c) = @_;
diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm
index d0f2a9394..e65810b91 100644
--- a/perllib/FixMyStreet/App/Controller/Reports.pm
+++ b/perllib/FixMyStreet/App/Controller/Reports.pm
@@ -481,10 +481,10 @@ sub summary : Private {
$c->forward('/admin/fetch_contacts');
$c->stash->{contacts} = [ $c->stash->{contacts}->all ];
- $c->forward('/dashboard/construct_rs_filter', []);
+ my $reporting = $c->forward('/dashboard/construct_rs_filter', []);
if ( $c->get_param('csv') ) {
- $c->detach('export_summary_csv');
+ $c->detach('export_summary_csv', [ $reporting ]);
}
$c->forward('/dashboard/generate_grouped_data');
@@ -494,38 +494,26 @@ sub summary : Private {
}
sub export_summary_csv : Private {
- my ( $self, $c ) = @_;
+ my ( $self, $c, $reporting ) = @_;
- $c->stash->{csv} = {
- objects => $c->stash->{objects_rs}->search_rs({}, {
- rows => 100,
- order_by => { '-desc' => 'me.confirmed' },
- }),
- headers => [
- 'Report ID',
- 'Title',
- 'Category',
- 'Created',
- 'Confirmed',
- 'Status',
- 'Latitude', 'Longitude',
- 'Query',
- 'Report URL',
- ],
- columns => [
- 'id',
- 'title',
- 'category',
- 'created',
- 'confirmed',
- 'state',
- 'latitude', 'longitude',
- 'postcode',
- 'url',
- ],
- filename => 'fixmystreet-data',
- };
- $c->forward('/dashboard/generate_csv');
+ $reporting->objects_attrs({
+ rows => 100,
+ order_by => { '-desc' => 'me.confirmed' },
+ });
+ $reporting->add_csv_columns(
+ id => 'Report ID',
+ title => 'Title',
+ category => 'Category',
+ created => 'Created',
+ confirmed => 'Confirmed',
+ state => 'Status',
+ latitude => 'Latitude',
+ longitude => 'Longitude',
+ postcode => 'Query',
+ url => 'Report URL',
+ );
+ $reporting->filename('fixmystreet-data');
+ $reporting->generate_csv_http($c);
}
=head2 check_canonical_url
diff --git a/perllib/FixMyStreet/Cobrand/BathNES.pm b/perllib/FixMyStreet/Cobrand/BathNES.pm
index 2dc1a4297..e8e2c5427 100644
--- a/perllib/FixMyStreet/Cobrand/BathNES.pm
+++ b/perllib/FixMyStreet/Cobrand/BathNES.pm
@@ -175,20 +175,20 @@ sub _dashboard_user_lookup {
sub dashboard_export_updates_add_columns {
my ($self, $csv) = @_;
- return unless $csv->{user}->has_body_permission_to('export_extra_columns');
+ return unless $csv->user->has_body_permission_to('export_extra_columns');
- push @{$csv->{headers}}, "Staff User";
- push @{$csv->{headers}}, "User Email";
- push @{$csv->{columns}}, "staff_user";
- push @{$csv->{columns}}, "user_email";
+ $csv->add_csv_columns(
+ staff_user => 'Staff User',
+ user_email => 'User Email',
+ );
- $csv->{objects} = $csv->{objects}->search(undef, {
+ $csv->objects_attrs({
'+columns' => ['user.email'],
join => 'user',
});
my $user_lookup = $self->_dashboard_user_lookup;
- $csv->{extra_data} = sub {
+ $csv->csv_extra_data(sub {
my $report = shift;
my $staff_user = '';
@@ -200,37 +200,28 @@ sub dashboard_export_updates_add_columns {
user_email => $report->user->email || '',
staff_user => $staff_user,
};
- };
+ });
}
sub dashboard_export_problems_add_columns {
my ($self, $csv) = @_;
- return unless $csv->{user}->has_body_permission_to('export_extra_columns');
-
- $csv->{headers} = [
- @{ $csv->{headers} },
- "User Email",
- "User Phone",
- "Staff User",
- "Attribute Data",
- ];
-
- $csv->{columns} = [
- @{ $csv->{columns} },
- "user_email",
- "user_phone",
- "staff_user",
- "attribute_data",
- ];
-
- $csv->{objects} = $csv->{objects}->search(undef, {
+ return unless $csv->user->has_body_permission_to('export_extra_columns');
+
+ $csv->add_csv_columns(
+ user_email => 'User Email',
+ user_phone => 'User Phone',
+ staff_user => 'Staff User',
+ attribute_data => "Attribute Data",
+ );
+
+ $csv->objects_attrs({
'+columns' => ['user.email', 'user.phone'],
join => 'user',
});
my $user_lookup = $self->_dashboard_user_lookup;
- $csv->{extra_data} = sub {
+ $csv->csv_extra_data(sub {
my $report = shift;
my $staff_user = '';
@@ -244,7 +235,7 @@ sub dashboard_export_problems_add_columns {
staff_user => $staff_user,
attribute_data => $attribute_data,
};
- };
+ });
}
1;
diff --git a/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm
index 75356dc87..f901c4e2f 100644
--- a/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm
@@ -126,8 +126,7 @@ sub default_map_zoom { 3 }
sub _dashboard_export_add_columns {
my ($self, $csv) = @_;
- push @{$csv->{headers}}, "Staff User";
- push @{$csv->{columns}}, "staff_user";
+ $csv->add_csv_columns( staff_user => 'Staff User' );
# All staff users, for contributed_by lookup
my @user_ids = FixMyStreet::DB->resultset('User')->search(
@@ -135,7 +134,7 @@ sub _dashboard_export_add_columns {
{ columns => [ 'id', 'email', ] })->all;
my %user_lookup = map { $_->id => $_->email } @user_ids;
- $csv->{extra_data} = sub {
+ $csv->csv_extra_data(sub {
my $report = shift;
my $staff_user = '';
if (my $contributed_by = $report->get_extra_metadata('contributed_by')) {
@@ -144,7 +143,7 @@ sub _dashboard_export_add_columns {
return {
staff_user => $staff_user,
};
- };
+ });
}
sub dashboard_export_updates_add_columns {
diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
index 6021827cb..1575d7cc1 100644
--- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
@@ -227,10 +227,9 @@ sub available_permissions {
sub dashboard_export_problems_add_columns {
my ($self, $csv) = @_;
- push @{$csv->{headers}}, "HIAMS/Exor Ref";
- push @{$csv->{columns}}, "external_ref";
+ $csv->add_csv_columns( external_ref => 'HIAMS/Exor Ref' );
- $csv->{extra_data} = sub {
+ $csv->csv_extra_data(sub {
my $report = shift;
# Try and get a HIAMS reference first of all
my $ref = $report->get_extra_metadata('customer_reference');
@@ -243,7 +242,7 @@ sub dashboard_export_problems_add_columns {
return {
external_ref => ( $ref || '' ),
};
- };
+ });
}
1;
diff --git a/perllib/FixMyStreet/Cobrand/TfL.pm b/perllib/FixMyStreet/Cobrand/TfL.pm
index e085b8bd6..31c7e4fe8 100644
--- a/perllib/FixMyStreet/Cobrand/TfL.pm
+++ b/perllib/FixMyStreet/Cobrand/TfL.pm
@@ -244,38 +244,29 @@ sub available_permissions {
sub dashboard_export_problems_add_columns {
my ($self, $csv) = @_;
- $csv->{headers} = [
- map { $_ eq 'Ward' ? 'Borough' : $_ } @{ $csv->{headers} },
- "Agent responsible",
- "Safety critical",
- "Delivered to",
- "Closure email at",
- "Reassigned at",
- "Reassigned by",
- ];
-
- $csv->{columns} = [
- @{ $csv->{columns} },
- "agent_responsible",
- "safety_critical",
- "delivered_to",
- "closure_email_at",
- "reassigned_at",
- "reassigned_by",
- ];
-
- if ($csv->{category}) {
- my ($contact) = grep { $_->category eq $csv->{category} } @{$csv->{contacts}};
+ $csv->modify_csv_header( Ward => 'Borough' );
+
+ $csv->add_csv_columns(
+ agent_responsible => "Agent responsible",
+ safety_critical => "Safety critical",
+ delivered_to => "Delivered to",
+ closure_email_at => "Closure email at",
+ reassigned_at => "Reassigned at",
+ reassigned_by => "Reassigned by",
+ );
+
+ if ($csv->category) {
+ my @contacts = $csv->body->contacts->search(undef, { order_by => [ 'category' ] } )->all;
+ my ($contact) = grep { $_->category eq $csv->category } @contacts;
if ($contact) {
foreach (@{$contact->get_metadata_for_storage}) {
next if $_->{code} eq 'safety_critical';
- push @{$csv->{columns}}, "extra.$_->{code}";
- push @{$csv->{headers}}, $_->{description};
+ $csv->add_csv_columns( "extra.$_->{code}" => $_->{description} );
}
}
}
- $csv->{extra_data} = sub {
+ $csv->csv_extra_data(sub {
my $report = shift;
my $agent = $report->shortlisted_user;
@@ -316,7 +307,7 @@ sub dashboard_export_problems_add_columns {
$fields->{"extra.$_->{name}"} = $_->{value};
}
return $fields;
- };
+ });
}
sub must_have_2fa {
diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm
index e33edc792..5635939ac 100644
--- a/perllib/FixMyStreet/Cobrand/Zurich.pm
+++ b/perllib/FixMyStreet/Cobrand/Zurich.pm
@@ -1275,8 +1275,8 @@ sub admin_stats {
sub export_as_csv {
my ($self, $c, $params) = @_;
- my $csv = $c->stash->{csv} = {
- objects => $c->model('DB::Problem')->search_rs(
+ my $reporting = FixMyStreet::Reporting->new(
+ objects_rs => $c->model('DB::Problem')->search_rs(
$params,
{
join => ['admin_log_entries', 'user'],
@@ -1297,7 +1297,7 @@ sub export_as_csv {
]
}
),
- headers => [
+ csv_headers => [
'Report ID', 'Created', 'Sent to Agency', 'Last Updated',
'E', 'N', 'Category', 'Status', 'Closure Status',
'UserID', 'User email', 'User phone', 'User name',
@@ -1305,7 +1305,7 @@ sub export_as_csv {
'Media URL', 'Interface Used', 'Council Response',
'Strasse', 'Mast-Nr.', 'Haus-Nr.', 'Hydranten-Nr.',
],
- columns => [
+ csv_columns => [
'id', 'created', 'whensent',' lastupdate', 'local_coords_x',
'local_coords_y', 'category', 'state', 'closure_status',
'user_id', 'user_email', 'user_phone', 'user_name',
@@ -1313,7 +1313,7 @@ sub export_as_csv {
'media_url', 'service', 'public_response',
'strasse', 'mast_nr',' haus_nr', 'hydranten_nr',
],
- extra_data => sub {
+ csv_extra_data => sub {
my $report = shift;
my $body_name = "";
@@ -1360,8 +1360,8 @@ sub export_as_csv {
};
},
filename => 'stats',
- };
- $c->forward('/dashboard/generate_csv');
+ );
+ $reporting->generate_csv_http($c);
}
sub problem_confirm_email_extras {
diff --git a/perllib/FixMyStreet/Reporting.pm b/perllib/FixMyStreet/Reporting.pm
new file mode 100644
index 000000000..7ee9fc41a
--- /dev/null
+++ b/perllib/FixMyStreet/Reporting.pm
@@ -0,0 +1,337 @@
+package FixMyStreet::Reporting;
+
+use DateTime;
+use Moo;
+use Text::CSV;
+use Types::Standard qw(ArrayRef CodeRef Enum HashRef InstanceOf Int Maybe Str);
+use FixMyStreet::DB;
+
+# What are we reporting on
+
+has type => ( is => 'ro', isa => Enum['problems','updates'] );
+has on_problems => ( is => 'lazy', default => sub { $_[0]->type eq 'problems' } );
+has on_updates => ( is => 'lazy', default => sub { $_[0]->type eq 'updates' } );
+
+# Filters to restrict the reporting to
+
+has body => ( is => 'ro', isa => Maybe[InstanceOf['FixMyStreet::DB::Result::Body']] );
+has wards => ( is => 'ro', isa => ArrayRef[Int], default => sub { [] } );
+has category => ( is => 'ro', isa => Maybe[Str] );
+has state => ( is => 'ro', isa => Maybe[Str] );
+has start_date => ( is => 'ro',
+ isa => Str,
+ default => sub {
+ my $days30 = DateTime->now(time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone)->subtract(days => 30);
+ $days30->truncate( to => 'day' );
+ $days30->strftime('%Y-%m-%d');
+ }
+);
+has end_date => ( is => 'ro', isa => Maybe[Str] );
+
+# Things needed for cobrand specific extra data or checks
+
+has cobrand => ( is => 'ro', default => sub { FixMyStreet::DB->schema->cobrand } ); # Which cobrand is asking, to get the right data / hooks / base URL
+has user => ( is => 'ro', isa => Maybe[InstanceOf['FixMyStreet::DB::Result::User']] );
+
+# Things created in the process, that can be manually overridden
+
+has objects_rs => ( is => 'rwp' ); # ResultSet of rows
+
+sub objects_attrs {
+ my ($self, $attrs) = @_;
+ my $rs = $self->objects_rs->search(undef, $attrs);
+ $self->_set_objects_rs($rs);
+ return $rs;
+}
+
+# CSV header strings and column keys (looked up in the row's as_hashref, plus
+# the following: user_name_display, acknowledged, fixed, closed, wards,
+# local_coords_x, local_coords_y, url, subcategory, site_used, reported_as)
+has csv_headers => ( is => 'rwp', isa => ArrayRef[Str], default => sub { [] } );
+has csv_columns => ( is => 'rwp', isa => ArrayRef[Str], default => sub { [] } );
+
+sub modify_csv_header {
+ my ($self, %mapping) = @_;
+ $self->_set_csv_headers([
+ map { $mapping{$_} || $_ } @{ $self->csv_headers },
+ ]);
+}
+
+sub add_csv_columns {
+ my $self = shift;
+ for (my $i = 0; $i < @_; $i += 2) {
+ my $column = $_[$i];
+ my $header = $_[$i+1];
+ push @{$self->csv_columns}, $column;
+ push @{$self->csv_headers}, $header;
+ }
+}
+
+# A function that is passed the report and returns a hashref of extra data to
+# include that can be used by 'columns'
+has csv_extra_data => ( is => 'rw', isa => CodeRef );
+
+has filename => ( is => 'rw', isa => Str, lazy => 1, default => sub {
+ my $self = shift;
+ my %where = (
+ category => $self->category,
+ state => $self->state,
+ ward => join(',', @{$self->wards}),
+ start_date => $self->start_date,
+ end_date => $self->end_date,
+ );
+ $where{body} = $self->body->id if $self->body;
+ my $host = URI->new($self->cobrand->base_url)->host;
+ join '-',
+ $host,
+ $self->on_updates ? ('updates') : (),
+ map {
+ my $value = $where{$_};
+ (defined $value and length $value) ? ($_, $value) : ()
+ } sort keys %where
+});
+
+# Generation code
+
+sub construct_rs_filter {
+ my $self = shift;
+
+ my $table_name = $self->on_updates ? 'problem' : 'me';
+
+ my %where;
+ $where{areas} = [ map { { 'like', "%,$_,%" } } @{$self->wards} ]
+ if @{$self->wards};
+ $where{"$table_name.category"} = $self->category
+ if $self->category;
+
+ if ( $self->state && FixMyStreet::DB::Result::Problem->fixed_states->{$self->state} ) { # Probably fixed - council
+ $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
+ } elsif ( $self->state ) {
+ $where{"$table_name.state"} = $self->state;
+ } else {
+ $where{"$table_name.state"} = [ FixMyStreet::DB::Result::Problem->visible_states() ];
+ }
+
+ my $range = FixMyStreet::DateRange->new(
+ start_date => $self->start_date,
+ end_date => $self->end_date,
+ formatter => FixMyStreet::DB->schema->storage->datetime_parser,
+ );
+ $where{"$table_name.confirmed"} = $range->sql;
+
+ my $rs = $self->on_updates ? $self->cobrand->updates : $self->cobrand->problems;
+ my $objects_rs = $rs->to_body($self->body)->search( \%where );
+ $self->_set_objects_rs($objects_rs);
+ return {
+ params => \%where,
+ objects_rs => $objects_rs,
+ }
+}
+
+sub csv_parameters {
+ my $self = shift;
+ if ($self->on_updates) {
+ $self->_csv_parameters_updates;
+ } else {
+ $self->_csv_parameters_problems;
+ }
+}
+
+sub _csv_parameters_updates {
+ my $self = shift;
+
+ $self->objects_attrs({
+ join => 'problem',
+ order_by => ['me.confirmed', 'me.id'],
+ '+columns' => ['problem.bodies_str'],
+ cursor_page_size => 1000,
+ });
+ $self->_set_csv_headers([
+ 'Report ID', 'Update ID', 'Date', 'Status', 'Problem state',
+ 'Text', 'User Name', 'Reported As',
+ ]);
+ $self->_set_csv_columns([
+ 'problem_id', 'id', 'confirmed', 'state', 'problem_state',
+ 'text', 'user_name_display', 'reported_as',
+ ]);
+ $self->cobrand->call_hook(dashboard_export_updates_add_columns => $self);
+}
+
+sub _csv_parameters_problems {
+ my $self = shift;
+
+ my $groups = $self->cobrand->enable_category_groups ? 1 : 0;
+ my $join = ['comments'];
+ my $columns = ['comments.id', 'comments.problem_state', 'comments.state', 'comments.confirmed', 'comments.mark_fixed'];
+ if ($groups) {
+ push @$join, 'contact';
+ push @$columns, 'contact.id', 'contact.extra';
+ }
+ $self->objects_attrs({
+ join => $join,
+ collapse => 1,
+ '+columns' => $columns,
+ order_by => ['me.confirmed', 'me.id'],
+ cursor_page_size => 1000,
+ });
+ $self->_set_csv_headers([
+ 'Report ID',
+ 'Title',
+ 'Detail',
+ 'User Name',
+ 'Category',
+ $groups ? ('Subcategory') : (),
+ 'Created',
+ 'Confirmed',
+ 'Acknowledged',
+ 'Fixed',
+ 'Closed',
+ 'Status',
+ 'Latitude', 'Longitude',
+ 'Query',
+ 'Ward',
+ 'Easting',
+ 'Northing',
+ 'Report URL',
+ 'Site Used',
+ 'Reported As',
+ ]);
+ $self->_set_csv_columns([
+ 'id',
+ 'title',
+ 'detail',
+ 'user_name_display',
+ 'category',
+ $groups ? ('subcategory') : (),
+ 'created',
+ 'confirmed',
+ 'acknowledged',
+ 'fixed',
+ 'closed',
+ 'state',
+ 'latitude', 'longitude',
+ 'postcode',
+ 'wards',
+ 'local_coords_x',
+ 'local_coords_y',
+ 'url',
+ 'site_used',
+ 'reported_as',
+ ]);
+ $self->cobrand->call_hook(dashboard_export_problems_add_columns => $self);
+}
+
+=head2 generate_csv
+
+Generates a CSV output to a file handler provided
+
+=cut
+
+sub generate_csv {
+ my ($self, $handle) = @_;
+
+ my $csv = Text::CSV->new({ binary => 1, eol => "\n" });
+ $csv->print($handle, $self->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 } @{$self->csv_columns};
+
+ my $children = $self->body ? $self->body->first_area_children : {};
+
+ my $objects = $self->objects_rs;
+ while ( my $obj = $objects->next ) {
+ my $hashref = $obj->as_hashref(\%asked_for);
+
+ $hashref->{user_name_display} = $obj->anonymous
+ ? '(anonymous)' : $obj->name;
+
+ if ($asked_for{acknowledged}) {
+ 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';
+ $hashref->{acknowledged} //= $comment->confirmed;
+ $hashref->{fixed} //= $fixed_states->{ $problem_state } || $comment->mark_fixed ?
+ $comment->confirmed : undef;
+ if ($closed_states->{ $problem_state }) {
+ $hashref->{closed} = $comment->confirmed;
+ last;
+ }
+ }
+ }
+
+ if ($asked_for{wards}) {
+ $hashref->{wards} = join ', ',
+ map { $children->{$_}->{name} }
+ grep { $children->{$_} }
+ split ',', $hashref->{areas};
+ }
+
+ if ($obj->can('local_coords') && $asked_for{local_coords_x}) {
+ ($hashref->{local_coords_x}, $hashref->{local_coords_y}) =
+ $obj->local_coords;
+ }
+
+ if ($asked_for{subcategory}) {
+ my $group = $obj->contact ? $obj->contact->groups : [];
+ $group = join(',', @$group);
+ if ($group) {
+ $hashref->{subcategory} = $obj->category;
+ $hashref->{category} = $group;
+ }
+ }
+
+ my $base = $self->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 = $self->csv_extra_data) {
+ my $extra = $fn->($obj);
+ $hashref = { %$hashref, %$extra };
+ }
+
+ $csv->print($handle, [
+ @{$hashref}{
+ @{$self->csv_columns}
+ },
+ ] );
+ }
+}
+
+# Output code
+
+# Outputs relevant CSV HTTP headers, and then streams the CSV
+sub generate_csv_http {
+ my ($self, $c) = @_;
+ $self->http_setup($c);
+ $self->generate_csv($c->response);
+}
+
+sub http_setup {
+ my ($self, $c) = @_;
+ my $filename = $self->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("");
+}
+
+1;