From 6d2795b407cfd1c88bbb28401d34ee61dcb1cf9b Mon Sep 17 00:00:00 2001
From: Moray Jones
Date: Thu, 23 Jun 2022 16:55:16 +0100
Subject: [PATCH] [Bucks] Add email alternative to templates
* Adds setting to allow email alternative text to be added
* Filters out alerts that need to send the private email text
from general alerts of added comments
* Update database with new fields
* Create private email templates
* Updates documentation to include the feature
---
CHANGELOG.md | 1 +
bin/update-schema | 1 +
db/downgrade_0079---0078.sql | 4 +
db/schema.sql | 4 +-
..._0079-add-response-template-email-text.sql | 2 +
docs/_includes/admin-tasks-content.md | 6 ++
.../App/Controller/Admin/Templates.pm | 5 +
perllib/FixMyStreet/DB/Result/Comment.pm | 2 +
.../FixMyStreet/DB/Result/ResponseTemplate.pm | 2 +
perllib/FixMyStreet/Queue/Item/Report.pm | 3 +-
perllib/FixMyStreet/Script/Alerts.pm | 30 +++++-
perllib/FixMyStreet/Template.pm | 10 +-
perllib/Open311/UpdatesBase.pm | 16 +++-
perllib/Utils.pm | 1 -
t/app/controller/admin/templates.t | 21 ++++
t/app/controller/report_new_update.t | 53 ++++++++++-
t/open311/getservicerequestupdates.t | 95 ++++++++++++-------
.../default/templated_email_alert-update.html | 31 ++++++
.../default/templated_email_alert-update.txt | 29 ++++++
templates/web/base/admin/list_updates.html | 7 +-
templates/web/base/admin/templates/edit.html | 14 ++-
templates/web/base/admin/update_edit.html | 5 +
22 files changed, 287 insertions(+), 55 deletions(-)
create mode 100644 db/downgrade_0079---0078.sql
create mode 100644 db/schema_0079-add-response-template-email-text.sql
create mode 100644 templates/email/default/templated_email_alert-update.html
create mode 100644 templates/email/default/templated_email_alert-update.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a878c75e98..b6c225288b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@
- Add an index on problem(external_id) to speed up bin/fetch --updates
- Upgrade Net::DNS and libwww to deal with IPv6 issues.
- Add send_state column to updates. #3865
+ - Enable alternative response from templates to be emailed to issue reporter. #4001
- Security
- Permit control over database connection `sslmode` via $FMS_DB_SSLMODE
- Open311 improvements:
diff --git a/bin/update-schema b/bin/update-schema
index 11baf03e0f7..0e8b1d534bc 100755
--- a/bin/update-schema
+++ b/bin/update-schema
@@ -215,6 +215,7 @@ else {
# (assuming schema change files are never half-applied, which should be the case)
sub get_db_version {
return 'EMPTY' if ! table_exists('problem');
+ return '0079' if column_exists('response_templates', 'email_text');
return '0078' if column_exists('problem','send_fail_body_ids');
return '0077' if column_exists('comment', 'send_state');
return '0076' if index_exists('problem_external_id_idx');
diff --git a/db/downgrade_0079---0078.sql b/db/downgrade_0079---0078.sql
new file mode 100644
index 00000000000..c98a219194c
--- /dev/null
+++ b/db/downgrade_0079---0078.sql
@@ -0,0 +1,4 @@
+begin;
+alter table response_templates drop email_text;
+alter table comment drop private_email_text;
+commit;
diff --git a/db/schema.sql b/db/schema.sql
index 3c6e8444aea..3addb57dba1 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -368,7 +368,8 @@ create table comment (
send_fail_count integer not null default 0,
send_fail_reason text,
send_fail_timestamp timestamp,
- whensent timestamp
+ whensent timestamp,
+ private_email_text text
);
create index comment_user_id_idx on comment(user_id);
@@ -532,6 +533,7 @@ create table response_templates (
body_id int references body(id) not null,
title text not null,
text text not null,
+ email_text text,
created timestamp not null default current_timestamp,
auto_response boolean NOT NULL DEFAULT 'f',
state text,
diff --git a/db/schema_0079-add-response-template-email-text.sql b/db/schema_0079-add-response-template-email-text.sql
new file mode 100644
index 00000000000..e980c84eca3
--- /dev/null
+++ b/db/schema_0079-add-response-template-email-text.sql
@@ -0,0 +1,2 @@
+alter table response_templates add email_text text;
+alter table comment add private_email_text text;
\ No newline at end of file
diff --git a/docs/_includes/admin-tasks-content.md b/docs/_includes/admin-tasks-content.md
index 7c61796414b..fbb1523600b 100644
--- a/docs/_includes/admin-tasks-content.md
+++ b/docs/_includes/admin-tasks-content.md
@@ -872,6 +872,12 @@ will automatically be added as a first update to any new report created that
matches the template (ie. in the relevant category if assigned). This lets
you give e.g. estimated timescales or other useful information up front.
+If you enter text in the ‘Text for email alert field’, the template text will update
+the report on the website and the email text will be sent to the user if they have
+opted into alerts.
+
+
+
#### Editing or deleting a template
Click on ‘Templates’ in the admin menu. You will see a table of existing templates. Click on ‘Edit’
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Templates.pm b/perllib/FixMyStreet/App/Controller/Admin/Templates.pm
index 75c59c441c7..e92557735af 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/Templates.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/Templates.pm
@@ -98,7 +98,12 @@ sub edit : Path : Args(2) {
$c->stash->{errors}->{title} = _("There is already a template with that title.");
}
+ if ($c->get_param('email') && !$c->get_param('text') ) {
+ $c->stash->{errors}->{email_text} = _("There must be template text if there is alternative email text.");
+ };
$template->text( $c->get_param('text') );
+ $template->email_text( $c->get_param('email') || '');
+
$template->state( $c->get_param('state') );
my $ext_code = $c->cobrand->call_hook('admin_templates_external_status_code_hook');
diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm
index b7af6650553..61b17ade7cf 100644
--- a/perllib/FixMyStreet/DB/Result/Comment.pm
+++ b/perllib/FixMyStreet/DB/Result/Comment.pm
@@ -72,6 +72,8 @@ __PACKAGE__->add_columns(
{ data_type => "timestamp", is_nullable => 1 },
"send_state",
{ data_type => "text", default_value => "unprocessed", is_nullable => 0 },
+ "private_email_text",
+ { data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
diff --git a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
index 7d6dfe29ff8..768013510d4 100644
--- a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
+++ b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
@@ -41,6 +41,8 @@ __PACKAGE__->add_columns(
{ data_type => "text", is_nullable => 1 },
"external_status_code",
{ data_type => "text", is_nullable => 1 },
+ "email_text",
+ { data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint("response_templates_body_id_title_key", ["body_id", "title"]);
diff --git a/perllib/FixMyStreet/Queue/Item/Report.pm b/perllib/FixMyStreet/Queue/Item/Report.pm
index affe88e3821..d1aaca78c3c 100644
--- a/perllib/FixMyStreet/Queue/Item/Report.pm
+++ b/perllib/FixMyStreet/Queue/Item/Report.pm
@@ -333,7 +333,7 @@ sub _add_confirmed_update {
);
my $template = $problem->response_template_for('confirmed', 'dummy', '', '');
- my $description = $updates->comment_text_for_request($template, {}, $problem);
+ my ($description, $email_text) = $updates->comment_text_for_request($template, {}, $problem);
next unless $description;
my $request = {
@@ -343,6 +343,7 @@ sub _add_confirmed_update {
# which uses current_timestamp (and thus microseconds) whilst this update
# is rounded down to the nearest second
comment_time => DateTime->now->add( seconds => 1 ),
+ email_text => $email_text,
status => 'open',
description => $description,
};
diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm
index 39bc42b10f0..a3e202377fe 100644
--- a/perllib/FixMyStreet/Script/Alerts.pm
+++ b/perllib/FixMyStreet/Script/Alerts.pm
@@ -55,6 +55,7 @@ sub send_alert_type {
$item_table.problem_state as item_problem_state,
$item_table.cobrand as item_cobrand,
$item_table.extra as item_extra,
+ $item_table.private_email_text as item_private_email_text,
$head_table.*
from alert, $item_table, $head_table
where alert.parameter::integer = $head_table.id
@@ -85,14 +86,13 @@ sub send_alert_type {
my $last_problem_state = 'confirmed';
my %data = ( template => $alert_type->template, data => [], schema => $schema );
while (my $row = $query->fetchrow_hashref) {
-
$row->{is_new_update} = defined($row->{item_text});
my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->{alert_cobrand})->new();
$cobrand->set_lang_and_domain( $row->{alert_lang}, 1, FixMyStreet->path_to('locale')->stringify );
- # Cobranded and non-cobranded messages can share a database. In this case, the conf file
- # should specify a vhost to send the reports for each cobrand, so that they don't get sent
+ # Cobranded and non-cobranded messages can share a database. In this case, the conf file
+ # should specify a vhost to send the reports for each cobrand, so that they don't get sent
# more than once if there are multiple vhosts running off the same database. The email_host
# call checks if this is the host that sends mail for this cobrand.
next unless $cobrand->email_host;
@@ -156,7 +156,6 @@ sub send_alert_type {
} else {
_extra_new_area_data($row, $ref);
}
-
push @{$data{data}}, $row;
if (!$data{alert_user_id}) {
@@ -360,6 +359,28 @@ sub _send_aggregated_alert(%) {
} );
$data{unsubscribe_url} = $cobrand->base_url( $data{cobrand_data} ) . '/A/' . $token->token;
+# Filter out alerts that have templated email responses for separate sending and send those to the problem reporter
+ my @template_data = grep { $_->{item_private_email_text } && $_->{user_id} == $_->{alert_user_id} } @{ $data{data} };
+ @{ $data{data} } = grep {! $_->{item_private_email_text } || $_->{user_id} != $_->{alert_user_id} } @{ $data{data} };
+
+ if (@template_data) {
+ my %template_data = %data;
+ $template_data{data} = [@template_data];
+ $template_data{template} = 'templated_email_alert-update';
+ trigger_alert_sending($alert_by, $token, %template_data);
+ };
+
+ if (@{ $data{data} }) {
+ trigger_alert_sending($alert_by, $token, %data);
+ }
+
+}
+
+sub trigger_alert_sending {
+ my $alert_by = shift;
+ my $token = shift;
+ my %data = @_;
+
my $result;
if ($alert_by eq 'phone') {
$result = _send_aggregated_alert_phone(%data);
@@ -380,7 +401,6 @@ sub _send_aggregated_alert_email {
my $cobrand = $data{cobrand};
FixMyStreet::Map::set_map_class($cobrand);
-
my $sender = FixMyStreet::Email::unique_verp_id([ 'alert', $data{alert_id} ], $cobrand->call_hook('verp_email_domain'));
my $result = FixMyStreet::Email::send_cron(
$data{schema},
diff --git a/perllib/FixMyStreet/Template.pm b/perllib/FixMyStreet/Template.pm
index bdbfeaabad8..7febefb489a 100644
--- a/perllib/FixMyStreet/Template.pm
+++ b/perllib/FixMyStreet/Template.pm
@@ -190,13 +190,14 @@ it all to text.
sub email_sanitize_text : Fn('email_sanitize_text') {
my $update = shift;
+ my $column = shift;
- my $text = $update->{item_text};
+ my $text = $column ? $update->{$column} : $update->{item_text};
my $extra = $update->{item_extra};
utf8::encode($extra) if $extra;
$extra = $extra ? RABX::wire_rd(new IO::String($extra)) : {};
- my $staff = $extra->{is_superuser} || $extra->{is_body_user};
+ my $staff = $extra->{is_superuser} || $extra->{is_body_user} || $column;
return $text unless $staff;
@@ -250,13 +251,14 @@ in updates from staff/superusers.
sub email_sanitize_html : Fn('email_sanitize_html') {
my $update = shift;
+ my $column = shift;
- my $text = $update->{item_text};
+ my $text = $column ? $update->{$column} : $update->{item_text};
my $extra = $update->{item_extra};
utf8::encode($extra) if $extra;
$extra = $extra ? RABX::wire_rd(new IO::String($extra)) : {};
- my $staff = $extra->{is_superuser} || $extra->{is_body_user};
+ my $staff = $extra->{is_superuser} || $extra->{is_body_user} || $column;
return _staff_html_markup($text, $staff);
}
diff --git a/perllib/Open311/UpdatesBase.pm b/perllib/Open311/UpdatesBase.pm
index 9e9bf7359d8..89e07447b2c 100644
--- a/perllib/Open311/UpdatesBase.pm
+++ b/perllib/Open311/UpdatesBase.pm
@@ -150,6 +150,7 @@ sub find_problem {
sub process_update {
my ($self, $request, $p) = @_;
+
my $open311 = $self->current_open311;
my $body = $self->current_body;
@@ -161,7 +162,10 @@ sub process_update {
my $template = $p->response_template_for(
$state, $old_state, $external_status_code, $old_external_status_code
);
- my $text = $self->comment_text_for_request($template, $request, $p);
+ my ($text, $email_text) = $self->comment_text_for_request($template, $request, $p);
+ if (!$email_text && $request->{email_text}) {
+ $email_text = $request->{email_text};
+ };
my $comment = $self->schema->resultset('Comment')->new(
{
problem => $p,
@@ -171,6 +175,7 @@ sub process_update {
text => $text,
confirmed => $request->{comment_time},
created => $request->{comment_time},
+ private_email_text => $email_text,
}
);
@@ -265,22 +270,23 @@ sub process_update {
sub comment_text_for_request {
my ($self, $template, $request, $problem) = @_;
+ my $template_email_text = $template->email_text if $template;
$template = $template->text if $template;
my $desc = $request->{description} || '';
if ($desc && (!$template || ($template !~ /\{\{description}}/ && !$request->{prefer_template}))) {
- return $desc;
+ return ($desc, undef);
}
if ($template) {
$template =~ s/\{\{description}}/$desc/;
- return $template;
+ return ($template, $template_email_text);
}
- return "" if $self->blank_updates_permitted;
+ return ("", undef) if $self->blank_updates_permitted;
print STDERR "Couldn't determine update text for $request->{update_id} (report " . $problem->id . ")\n";
- return "";
+ return ("", undef);
}
1;
diff --git a/perllib/Utils.pm b/perllib/Utils.pm
index 8c4ba6c3195..ad5ce3e7ba4 100644
--- a/perllib/Utils.pm
+++ b/perllib/Utils.pm
@@ -257,5 +257,4 @@ sub _part {
}
}
-
1;
diff --git a/t/app/controller/admin/templates.t b/t/app/controller/admin/templates.t
index 9bcc8df070c..955bfe95843 100644
--- a/t/app/controller/admin/templates.t
+++ b/t/app/controller/admin/templates.t
@@ -328,6 +328,27 @@ subtest "templates that set state and external_status_code can't be added" => su
is $oxfordshire->response_templates->count, 0, "Invalid response template wasn't added";
};
+subtest "templates that set private email text but not public text can't be added" => sub {
+ $mech->delete_response_template($_) for $oxfordshire->response_templates;
+ $mech->log_in_ok( $superuser->email );
+ $mech->get_ok( "/admin/templates/" . $oxfordshire->id . "/new" );
+
+ my $fields = {
+ title => "Report marked fixed - all cats",
+ text => "",
+ email => "Thank you for your report. This problems has been fixed.",
+ auto_response => 'on',
+ state => 'fixed - council',
+ external_status_code => '100',
+ };
+ $mech->submit_form_ok( { with_fields => $fields } );
+ is $mech->uri->path, '/admin/templates/' . $oxfordshire->id . '/new', 'not redirected';
+ $mech->content_contains( 'Please correct the errors below' );
+ $mech->content_contains( 'There must be template text if there is alternative email text.' );
+
+ is $oxfordshire->response_templates->count, 0, "Invalid response template wasn't added";
+};
+
subtest "category groups are shown" => sub {
FixMyStreet::override_config {
ALLOWED_COBRANDS => [ 'oxfordshire' ],
diff --git a/t/app/controller/report_new_update.t b/t/app/controller/report_new_update.t
index 6ed401cba38..8b420db5f7e 100644
--- a/t/app/controller/report_new_update.t
+++ b/t/app/controller/report_new_update.t
@@ -56,7 +56,58 @@ subtest "test resending does not leave another initial auto-update" => sub {
$report->discard_changes;
$report->update({ whensent => undef });
FixMyStreet::Script::Reports::send(0, 0, 1);
- is +FixMyStreet::DB->resultset('Comment')->count, 1;
+ my $comments = FixMyStreet::DB->resultset('Comment');
+ is $comments->count, 1;
+ $comments->delete;
+ $report->delete;
+};
+
+$template->update({ email_text => 'Thanks for your report.
+
+This is the email alternative.',
+});
+
+subtest "test report creation with initial auto-update and alternative email text" => sub {
+
+ my $report = make_report();
+ my $report_id = $report->id;
+ $mech->clear_emails_ok;
+
+ my $user3 = $mech->log_in_ok('test-3@example.com');
+ $mech->log_in_ok($user3->email);
+ $mech->get_ok("/report/$report_id");
+ $mech->submit_form_ok({ button => 'alert', with_fields => { type => 'updates' } });
+ $mech->log_out_ok;
+
+ FixMyStreet::Script::Alerts::send_updates();
+
+ my @emails = $mech->get_email;
+
+ is scalar @emails, 2, 'Two alerts sent';
+
+ my ($email_for_reporter) = grep { $_->header('To') =~ /test-2/ } @emails;
+ my ($email_for_subscriber) = grep { $_->header('To') =~ /test-3/ } @emails;
+
+ is $mech->get_text_body_from_email($email_for_subscriber) =~ /Thanks for your report. We will investigate within 5 working days./, 1, "Text template sent to subscriber";
+ is $mech->get_text_body_from_email($email_for_reporter) =~ /This is the email alternative \[https:\/\/google.com\]/, 1, "Email template sent to reporter";
+ is $mech->get_html_body_from_email($email_for_reporter) =~ /
+ [% TRY %][% INCLUDE '_council_reference_alert_update.html' problem=report p_style=secondary_p_style %][% CATCH file %][% END %]
+[% END %]
+
+[% INCLUDE '_email_bottom.html' %]
diff --git a/templates/email/default/templated_email_alert-update.txt b/templates/email/default/templated_email_alert-update.txt
new file mode 100644
index 00000000000..94eed94dbbf
--- /dev/null
+++ b/templates/email/default/templated_email_alert-update.txt
@@ -0,0 +1,29 @@
+Subject: New [% site_name %] council updates on report: '[% report.title %]'
+
+[% FOR row IN data -%]
+ [% email_sanitize_text(row, 'item_private_email_text') %]
+[% END %]
+
+[% state_message %]
+
+Please visit the following URL to view your report:
+
+ [% problem_url %]
+
+[% TRY %][% INCLUDE '_council_reference_alert_update.txt' problem=report %][% CATCH file %][% END %]
+
+This email was sent automatically, from an unmonitored email account - so
+please do not reply to it.
+
+[% signature %]
+
+
+[% IF unsubscribe_url %]
+Unsubscribe?
+
+We currently email you whenever someone leaves an update on the
+[% site_name %] report: [% report.title %].
+
+If you no longer wish to receive an email whenever this report is updated,
+please follow this link: [% unsubscribe_url %]
+[% END %]
diff --git a/templates/web/base/admin/list_updates.html b/templates/web/base/admin/list_updates.html
index c90ba5cdb83..def3500082f 100644
--- a/templates/web/base/admin/list_updates.html
+++ b/templates/web/base/admin/list_updates.html
@@ -39,7 +39,12 @@
[% loc('Updates') %]
-
[% update.text | html %]
+
[% update.text | html %]
+ [% IF update.private_email_text %]
+
+ [% loc('Template email response:') %] [% update.private_email_text | html %]
+ [% END %]
+
[% loc('This is the public text that will be shown on the site.') %]
@@ -35,6 +37,16 @@
[% loc('Text:') %]
+
+
+
+ [% loc('This is the text that will be sent to the reporting citizen in the alert email.') %]
+
+
+
+ [% loc('Text for email alert:') %]
+
+
[% INCLUDE 'admin/category-checkboxes.html' hint=loc('If you only want this template to be an option for specific categories, pick them here. By default they will show for all categories.') %]
diff --git a/templates/web/base/admin/update_edit.html b/templates/web/base/admin/update_edit.html
index 584567c0293..f5e2dd00a80 100644
--- a/templates/web/base/admin/update_edit.html
+++ b/templates/web/base/admin/update_edit.html
@@ -13,6 +13,11 @@