aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Somerville <matthew@mysociety.org>2019-11-10 19:27:06 +0000
committerMatthew Somerville <matthew@mysociety.org>2019-11-10 19:27:06 +0000
commitd8dd060a4c2f75e89a24f99634d91a6d8ef0e2bc (patch)
treeabb543c32c9e5f239cc66cff1bafa697f322ebd3
parentdb61249c59a96a2fad80523288b7d13881c10965 (diff)
parentb886792181eb77206054e73315a9d14cdb17e936 (diff)
Merge branch 'admin-auditing'
-rw-r--r--CHANGELOG.md2
-rwxr-xr-xbin/update-schema1
-rw-r--r--db/schema.sql4
-rw-r--r--db/schema_0069-admin-log-types.sql17
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm652
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Bodies.pm7
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Reports.pm516
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Roles.pm3
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Templates.pm179
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Triage.pm2
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Users.pm76
-rw-r--r--perllib/FixMyStreet/App/Controller/Moderate.pm6
-rw-r--r--perllib/FixMyStreet/App/Controller/Report.pm10
-rw-r--r--perllib/FixMyStreet/Cobrand/Zurich.pm6
-rw-r--r--perllib/FixMyStreet/DB/Result/AdminLog.pm75
-rw-r--r--perllib/FixMyStreet/Template.pm2
-rw-r--r--t/app/controller/admin/bodies.t7
-rw-r--r--t/app/controller/admin/report_edit.t9
-rw-r--r--t/app/controller/admin/roles.t9
-rw-r--r--t/app/controller/admin/templates.t11
-rw-r--r--t/app/controller/admin/users.t22
-rw-r--r--t/app/controller/moderate.t12
-rw-r--r--templates/web/base/admin/bodies/open311-form-fields.html2
-rw-r--r--templates/web/base/admin/list_updates.html2
-rw-r--r--templates/web/base/admin/problem_row.html4
-rw-r--r--templates/web/base/admin/reports/edit.html (renamed from templates/web/base/admin/report_edit.html)4
-rw-r--r--templates/web/base/admin/reports/index.html (renamed from templates/web/base/admin/reports.html)2
-rw-r--r--templates/web/base/admin/templates/edit.html (renamed from templates/web/base/admin/template_edit.html)2
-rw-r--r--templates/web/base/admin/templates/index.html (renamed from templates/web/base/admin/templates_index.html)2
-rw-r--r--templates/web/base/admin/templates/view.html (renamed from templates/web/base/admin/templates.html)4
-rw-r--r--templates/web/base/admin/users/form.html5
-rw-r--r--templates/web/base/admin/users/import.html4
-rw-r--r--templates/web/base/admin/users/index.html7
-rw-r--r--templates/web/base/admin/users/log.html72
-rw-r--r--templates/web/base/report/inspect/information.html2
-rw-r--r--templates/web/zurich/admin/problem_row.html2
-rw-r--r--templates/web/zurich/admin/report_edit-sdm.html2
-rw-r--r--templates/web/zurich/admin/reports/edit.html (renamed from templates/web/zurich/admin/report_edit.html)2
-rw-r--r--templates/web/zurich/admin/reports/index.html (renamed from templates/web/zurich/admin/reports.html)2
-rw-r--r--templates/web/zurich/admin/templates/edit.html (renamed from templates/web/zurich/admin/template_edit.html)2
-rw-r--r--templates/web/zurich/admin/templates/view.html (renamed from templates/web/zurich/admin/templates.html)4
41 files changed, 1039 insertions, 715 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a3c40560..1f2f3ed3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,8 @@
- Allow editing of category name. #1398
- Allow non-superuser staff to use 2FA, and optional enforcement of 2FA.
- Add optional enforced password expiry.
+ - Store a moderation history on admin report edit.
+ - Add user admin log page.
- New features:
- Categories can be listed under more than one group #2475
- OpenID Connect login support. #2523
diff --git a/bin/update-schema b/bin/update-schema
index 3f4b2bafe..4755142bb 100755
--- a/bin/update-schema
+++ b/bin/update-schema
@@ -212,6 +212,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 '0069' if constraint_contains('admin_log_object_type_check', 'template');
return '0068' if column_exists('users', 'oidc_ids');
return '0067' if table_exists('roles');
return '0066' if column_exists('users', 'area_ids');
diff --git a/db/schema.sql b/db/schema.sql
index a211ef50d..cf2914467 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -445,6 +445,10 @@ create table admin_log (
or object_type = 'update'
or object_type = 'user'
or object_type = 'moderation'
+ or object_type = 'template'
+ or object_type = 'body'
+ or object_type = 'category'
+ or object_type = 'role'
),
object_id integer not null,
action text not null,
diff --git a/db/schema_0069-admin-log-types.sql b/db/schema_0069-admin-log-types.sql
new file mode 100644
index 000000000..e4e9362ac
--- /dev/null
+++ b/db/schema_0069-admin-log-types.sql
@@ -0,0 +1,17 @@
+BEGIN;
+
+ALTER TABLE admin_log DROP CONSTRAINT admin_log_object_type_check;
+
+ALTER TABLE admin_log ADD CONSTRAINT admin_log_object_type_check CHECK (
+ object_type = 'problem'
+ OR object_type = 'update'
+ OR object_type = 'user'
+ OR object_type = 'moderation'
+ OR object_type = 'template'
+ OR object_type = 'body'
+ OR object_type = 'category'
+ OR object_type = 'role'
+);
+
+COMMIT;
+
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index b00c34777..975fed02f 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -4,20 +4,9 @@ use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
-use Path::Class;
-use POSIX qw(strftime strcoll);
-use Digest::SHA qw(sha1_hex);
-use mySociety::EmailUtil qw(is_valid_email is_valid_email_list);
-use DateTime::Format::Strptime;
+use POSIX qw(strcoll);
use List::Util 'first';
-use List::MoreUtils 'uniq';
-use mySociety::ArrayUtils;
-use Text::CSV;
-use Try::Tiny;
-
-use FixMyStreet::SendReport;
use FixMyStreet::SMS;
-use Utils;
=head1 NAME
@@ -214,157 +203,6 @@ sub fetch_languages : Private {
return 1;
}
-sub reports : Path('reports') {
- my ( $self, $c ) = @_;
-
- $c->stash->{edit_body_contacts} = 1
- if grep { $_ eq 'body' } keys %{$c->stash->{allowed_pages}};
-
- my $query = {};
- if ( $c->cobrand->moniker eq 'zurich' ) {
- my $type = $c->stash->{admin_type};
- my $body = $c->stash->{body};
- if ( $type eq 'dm' ) {
- my @children = map { $_->id } $body->bodies->all;
- my @all = (@children, $body->id);
- $query = { bodies_str => \@all };
- } elsif ( $type eq 'sdm' ) {
- $query = { bodies_str => $body->id };
- }
- }
-
- my $order = $c->get_param('o') || 'id';
- my $dir = defined $c->get_param('d') ? $c->get_param('d') : 1;
- $c->stash->{order} = $order;
- $c->stash->{dir} = $dir;
- $order = $dir ? { -desc => "me.$order" } : "me.$order";
-
- my $p_page = $c->get_param('p') || 1;
- my $u_page = $c->get_param('u') || 1;
-
- return if $c->cobrand->call_hook(report_search_query => $query, $p_page, $u_page, $order);
-
- if (my $search = $c->get_param('search')) {
- $search = $self->trim($search);
-
- # In case an email address, wrapped in <...>
- if ($search =~ /^<(.*)>$/) {
- my $possible_email = $1;
- my $parsed = FixMyStreet::SMS->parse_username($possible_email);
- $search = $possible_email if $parsed->{email};
- }
-
- $c->stash->{searched} = $search;
-
- my $search_n = 0;
- $search_n = int($search) if $search =~ /^\d+$/;
-
- my $like_search = "%$search%";
-
- my $parsed = FixMyStreet::SMS->parse_username($search);
- my $valid_phone = $parsed->{phone};
- my $valid_email = $parsed->{email};
-
- if ($valid_email) {
- $query->{'-or'} = [
- 'user.email' => { ilike => $like_search },
- ];
- } elsif ($valid_phone) {
- $query->{'-or'} = [
- 'user.phone' => { ilike => $like_search },
- ];
- } elsif ($search =~ /^id:(\d+)$/) {
- $query->{'-or'} = [
- 'me.id' => int($1),
- ];
- } elsif ($search =~ /^area:(\d+)$/) {
- $query->{'-or'} = [
- 'me.areas' => { like => "%,$1,%" }
- ];
- } elsif ($search =~ /^ref:(\d+)$/) {
- $query->{'-or'} = [
- 'me.external_id' => { like => "%$1%" }
- ];
- } else {
- $query->{'-or'} = [
- 'me.id' => $search_n,
- 'user.email' => { ilike => $like_search },
- 'user.phone' => { ilike => $like_search },
- 'me.external_id' => { ilike => $like_search },
- 'me.name' => { ilike => $like_search },
- 'me.title' => { ilike => $like_search },
- detail => { ilike => $like_search },
- bodies_str => { like => $like_search },
- cobrand_data => { like => $like_search },
- ];
- }
-
- my $problems = $c->cobrand->problems->search(
- $query,
- {
- prefetch => 'user',
- rows => 50,
- order_by => $order,
- }
- )->page( $p_page );
-
- $c->stash->{problems} = [ $problems->all ];
- $c->stash->{problems_pager} = $problems->pager;
-
- if ($valid_email) {
- $query = [
- 'user.email' => { ilike => $like_search },
- ];
- } elsif ($valid_phone) {
- $query = [
- 'user.phone' => { ilike => $like_search },
- ];
- } elsif ($search =~ /^id:(\d+)$/) {
- $query = [
- 'me.id' => int($1),
- 'me.problem_id' => int($1),
- ];
- } elsif ($search =~ /^area:(\d+)$/) {
- $query = [];
- } else {
- $query = [
- 'me.id' => $search_n,
- 'problem.id' => $search_n,
- 'user.email' => { ilike => $like_search },
- 'user.phone' => { ilike => $like_search },
- 'me.name' => { ilike => $like_search },
- text => { ilike => $like_search },
- 'me.cobrand_data' => { ilike => $like_search },
- ];
- }
-
- if (@$query) {
- my $updates = $c->cobrand->updates->search(
- {
- -or => $query,
- },
- {
- -select => [ 'me.*', qw/problem.bodies_str problem.state/ ],
- prefetch => [qw/user problem/],
- rows => 50,
- order_by => { -desc => 'me.id' }
- }
- )->page( $u_page );
- $c->stash->{updates} = [ $updates->all ];
- $c->stash->{updates_pager} = $updates->pager;
- }
-
- } else {
-
- my $problems = $c->cobrand->problems->search(
- $query,
- { order_by => $order, rows => 50 }
- )->page( $p_page );
- $c->stash->{problems} = [ $problems->all ];
- $c->stash->{problems_pager} = $problems->pager;
- }
-}
-
sub update_user : Private {
my ($self, $c, $object) = @_;
my $parsed = FixMyStreet::SMS->parse_username($c->get_param('username'));
@@ -378,482 +216,6 @@ sub update_user : Private {
return 0;
}
-sub report_edit_display : Private {
- my ( $self, $c ) = @_;
-
- my $problem = $c->stash->{problem};
-
- $c->stash->{page} = 'admin';
- FixMyStreet::Map::display_map(
- $c,
- latitude => $problem->latitude,
- longitude => $problem->longitude,
- pins => $problem->used_map
- ? [ {
- latitude => $problem->latitude,
- longitude => $problem->longitude,
- colour => $c->cobrand->pin_colour($problem, 'admin'),
- type => 'big',
- draggable => 1,
- } ]
- : [],
- print_report => 1,
- );
-}
-
-sub report_edit : Path('report_edit') : Args(1) {
- my ( $self, $c, $id ) = @_;
-
- my $problem = $c->cobrand->problems->search( { id => $id } )->first;
-
- $c->detach( '/page_error_404_not_found', [] )
- unless $problem;
-
- unless (
- $c->cobrand->moniker eq 'zurich'
- || $c->user->has_permission_to(report_edit => $problem->bodies_str_ids)
- ) {
- $c->detach( '/page_error_403_access_denied', [] );
- }
-
- $c->stash->{problem} = $problem;
- if ( $problem->extra ) {
- my @fields;
- if ( my $fields = $problem->get_extra_fields ) {
- for my $field ( @{$fields} ) {
- my $name = $field->{description} ?
- "$field->{description} ($field->{name})" :
- "$field->{name}";
- push @fields, { name => $name, val => $field->{value} };
- }
- }
- my $extra = $problem->get_extra_metadata;
- if ( $extra->{duplicates} ) {
- push @fields, { name => 'Duplicates', val => join( ',', @{ $problem->get_extra_metadata('duplicates') } ) };
- delete $extra->{duplicates};
- }
- for my $key ( keys %$extra ) {
- push @fields, { name => $key, val => $extra->{$key} };
- }
-
- $c->stash->{extra_fields} = \@fields;
- }
-
- $c->forward('/auth/get_csrf_token');
-
- $c->forward('categories_for_point');
-
- $c->forward('alerts_for_report');
-
- $c->forward('check_username_for_abuse', [ $problem->user ] );
-
- $c->stash->{updates} =
- [ $c->model('DB::Comment')
- ->search( { problem_id => $problem->id }, { order_by => [ 'created', 'id' ] } )
- ->all ];
-
- if (my $rotate_photo_param = $self->_get_rotate_photo_param($c)) {
- $self->rotate_photo($c, $problem, @$rotate_photo_param);
- $c->detach('report_edit_display');
- }
-
- if ( $c->cobrand->moniker eq 'zurich' ) {
- my $done = $c->cobrand->admin_report_edit();
- $c->detach('report_edit_display') if $done;
- }
-
- if ( $c->get_param('resend') && !$c->cobrand->call_hook('disable_resend') ) {
- $c->forward('/auth/check_csrf_token');
-
- $problem->resend;
- $problem->update();
- $c->stash->{status_message} = _('That problem will now be resent.');
-
- $c->forward( 'log_edit', [ $id, 'problem', 'resend' ] );
- }
- elsif ( $c->get_param('mark_sent') ) {
- $c->forward('/auth/check_csrf_token');
- $problem->update({ whensent => \'current_timestamp' })->discard_changes;
- $c->stash->{status_message} = _('That problem has been marked as sent.');
- $c->forward( 'log_edit', [ $id, 'problem', 'marked sent' ] );
- }
- elsif ( $c->get_param('flaguser') ) {
- $c->forward('users/flag');
- $c->stash->{problem}->discard_changes;
- }
- elsif ( $c->get_param('removeuserflag') ) {
- $c->forward('users/flag_remove');
- $c->stash->{problem}->discard_changes;
- }
- elsif ( $c->get_param('banuser') ) {
- $c->forward('users/ban');
- }
- elsif ( $c->get_param('submit') ) {
- $c->forward('/auth/check_csrf_token');
-
- my $old_state = $problem->state;
-
- my %columns = (
- flagged => $c->get_param('flagged') ? 1 : 0,
- non_public => $c->get_param('non_public') ? 1 : 0,
- );
- foreach (qw/state anonymous title detail name external_id external_body external_team/) {
- $columns{$_} = $c->get_param($_);
- }
- $problem->set_inflated_columns(\%columns);
-
- if ($c->get_param('closed_updates')) {
- $problem->set_extra_metadata(closed_updates => 1);
- } else {
- $problem->unset_extra_metadata('closed_updates');
- }
-
- $c->forward( '/admin/report_edit_category', [ $problem, $problem->state ne $old_state ] );
- $c->forward('update_user', [ $problem ]);
-
- # Deal with photos
- my $remove_photo_param = $self->_get_remove_photo_param($c);
- if ($remove_photo_param) {
- $self->remove_photo($c, $problem, $remove_photo_param);
- }
-
- if ($problem->state eq 'hidden' || $problem->non_public) {
- $problem->get_photoset->delete_cached(plus_updates => 1);
- }
-
- if ( $problem->is_visible() and $old_state eq 'unconfirmed' ) {
- $problem->confirmed( \'current_timestamp' );
- }
-
- $problem->lastupdate( \'current_timestamp' );
- $problem->update;
-
- if ( $problem->state ne $old_state ) {
- $c->forward( 'log_edit', [ $id, 'problem', 'state_change' ] );
-
- my $name = $c->user->moderating_user_name;
- my $extra = { is_superuser => 1 };
- if ($c->user->from_body) {
- delete $extra->{is_superuser};
- $extra->{is_body_user} = $c->user->from_body->id;
- }
- my $timestamp = \'current_timestamp';
- $problem->add_to_comments( {
- text => $c->stash->{update_text} || '',
- created => $timestamp,
- confirmed => $timestamp,
- user_id => $c->user->id,
- name => $name,
- mark_fixed => 0,
- anonymous => 0,
- state => 'confirmed',
- problem_state => $problem->state,
- extra => $extra
- } );
- }
- $c->forward( 'log_edit', [ $id, 'problem', 'edit' ] );
-
- $c->stash->{status_message} = _('Updated!');
-
- # do this here otherwise lastupdate and confirmed times
- # do not display correctly
- $problem->discard_changes;
- }
-
- $c->detach('report_edit_display');
-}
-
-=head2 report_edit_category
-
-Handles changing a problem's category and the complexity that comes with it.
-Returns 1 if category changed, 0 if no change.
-
-=cut
-
-sub report_edit_category : Private {
- my ($self, $c, $problem, $no_comment) = @_;
-
- if ((my $category = $c->get_param('category')) ne $problem->category) {
- my $disable_resend = $c->cobrand->call_hook('disable_resend');
- my $category_old = $problem->category;
- $problem->category($category);
- my @contacts = grep { $_->category eq $problem->category } @{$c->stash->{contacts}};
- my @new_body_ids = map { $_->body_id } @contacts;
- # If the report has changed bodies (and not to a subset!) we need to resend it
- my %old_map = map { $_ => 1 } @{$problem->bodies_str_ids};
- if (!$disable_resend && grep !$old_map{$_}, @new_body_ids) {
- $problem->resend;
- }
- # If the send methods of the old/new contacts differ we need to resend the report
- my @new_send_methods = uniq map {
- ( $_->body->can_be_devolved && $_->send_method ) ?
- $_->send_method : $_->body->send_method
- ? $_->body->send_method
- : $c->cobrand->_fallback_body_sender()->{method};
- } @contacts;
- my %old_send_methods = map { $_ => 1 } split /,/, ($problem->send_method_used || "Email");
- if (!$disable_resend && grep !$old_send_methods{$_}, @new_send_methods) {
- $problem->resend;
- }
-
- $problem->bodies_str(join( ',', @new_body_ids ));
- my $update_text = '*' . sprintf(_('Category changed from ‘%s’ to ‘%s’'), $category_old, $category) . '*';
- if ($no_comment) {
- $c->stash->{update_text} = $update_text;
- } else {
- $problem->add_to_comments({
- text => $update_text,
- created => \'current_timestamp',
- confirmed => \'current_timestamp',
- user_id => $c->user->id,
- name => $c->user->from_body ? $c->user->from_body->name : $c->user->name,
- state => 'confirmed',
- mark_fixed => 0,
- anonymous => 0,
- });
- }
- return 1;
- }
- return 0;
-}
-
-=head2 report_edit_location
-
-Handles changing a problem's location and the complexity that comes with it.
-For now, we reject the new location if the new location and old locations aren't
-covered by the same body.
-
-Returns 2 if the new position (if any) is acceptable and changed,
-1 if acceptable and unchanged, undef otherwise.
-
-NB: This must be called before report_edit_category, as that might modify
-$problem->bodies_str.
-
-=cut
-
-sub report_edit_location : Private {
- my ($self, $c, $problem) = @_;
-
- return 1 unless $c->forward('/location/determine_location_from_coords');
-
- my ($lat, $lon) = map { Utils::truncate_coordinate($_) } $problem->latitude, $problem->longitude;
- if ( $c->stash->{latitude} != $lat || $c->stash->{longitude} != $lon ) {
- # The two actions below change the stash, setting things up for e.g. a
- # new report. But here we're only doing it in order to check the found
- # bodies match; we don't want to overwrite the existing report data if
- # this lookup is bad. So let's save the stash and restore it after the
- # comparison.
- my $safe_stash = { %{$c->stash} };
- $c->stash->{fetch_all_areas} = 1;
- $c->stash->{area_check_action} = 'admin';
- $c->forward('/council/load_and_check_areas', []);
- $c->forward('/report/new/setup_categories_and_bodies');
- my %allowed_bodies = map { $_ => 1 } @{$problem->bodies_str_ids};
- my @new_bodies = keys %{$c->stash->{bodies_to_list}};
- my $bodies_match = grep { exists( $allowed_bodies{$_} ) } @new_bodies;
- $c->stash($safe_stash);
- return unless $bodies_match;
- $problem->latitude($c->stash->{latitude});
- $problem->longitude($c->stash->{longitude});
- my $areas = $c->stash->{all_areas_mapit};
- $problem->areas( ',' . join( ',', sort keys %$areas ) . ',' );
- return 2;
- }
- return 1;
-}
-
-sub categories_for_point : Private {
- my ($self, $c) = @_;
-
- $c->stash->{report} = $c->stash->{problem};
- # We have a report, stash its location
- $c->forward('/report/new/determine_location_from_report');
- # Look up the areas for this location
- my $prefetched_all_areas = [ grep { $_ } split ',', $c->stash->{report}->areas ];
- $c->forward('/around/check_location_is_acceptable', [ $prefetched_all_areas ]);
- # As with a new report, fetch the bodies/categories
- $c->forward('/report/new/setup_categories_and_bodies');
-
- # Remove the "Pick a category" option
- shift @{$c->stash->{category_options}} if @{$c->stash->{category_options}};
-
- $c->stash->{categories_hash} = { map { $_->category => 1 } @{$c->stash->{category_options}} };
-
- $c->forward('/admin/triage/setup_categories');
-
-}
-
-sub alerts_for_report : Private {
- my ($self, $c) = @_;
-
- $c->stash->{alert_count} = $c->model('DB::Alert')->search({
- alert_type => 'new_updates',
- parameter => $c->stash->{report}->id,
- confirmed => 1,
- whendisabled => undef,
- })->count();
-}
-
-sub templates : Path('templates') : Args(0) {
- my ( $self, $c ) = @_;
-
- my $user = $c->user;
-
- if ($user->is_superuser) {
- $c->forward('fetch_all_bodies');
- $c->stash->{template} = 'admin/templates_index.html';
- } elsif ( $user->from_body ) {
- $c->forward('load_template_body', [ $user->from_body->id ]);
- $c->res->redirect( $c->uri_for( 'templates', $c->stash->{body}->id ) );
- } else {
- $c->detach( '/page_error_404_not_found', [] );
- }
-}
-
-sub templates_view : Path('templates') : Args(1) {
- my ($self, $c, $body_id) = @_;
-
- $c->forward('load_template_body', [ $body_id ]);
-
- my @templates = $c->stash->{body}->response_templates->search(
- undef,
- {
- order_by => 'title'
- }
- );
-
- $c->stash->{response_templates} = \@templates;
-
- $c->stash->{template} = 'admin/templates.html';
-}
-
-sub template_edit : Path('templates') : Args(2) {
- my ( $self, $c, $body_id, $template_id ) = @_;
-
- $c->forward('load_template_body', [ $body_id ]);
-
- my $template;
- if ($template_id eq 'new') {
- $template = $c->stash->{body}->response_templates->new({});
- }
- else {
- $template = $c->stash->{body}->response_templates->find( $template_id )
- or $c->detach( '/page_error_404_not_found', [] );
- }
-
- $c->forward('fetch_contacts');
- my @contacts = $template->contacts->all;
- my @live_contacts = $c->stash->{live_contacts}->all;
- my %active_contacts = map { $_->id => 1 } @contacts;
- my @all_contacts = map { {
- id => $_->id,
- category => $_->category_display,
- active => $active_contacts{$_->id},
- email => $_->email,
- } } @live_contacts;
- $c->stash->{contacts} = \@all_contacts;
-
- # bare block to use 'last' if form is invalid.
- if ($c->req->method eq 'POST') { {
- if ($c->get_param('delete_template') && $c->get_param('delete_template') eq _("Delete template")) {
- $template->contact_response_templates->delete_all;
- $template->delete;
- } else {
- my @live_contact_ids = map { $_->id } @live_contacts;
- my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @live_contact_ids;
- my %new_contacts = map { $_ => 1 } @new_contact_ids;
- for my $contact (@all_contacts) {
- $contact->{active} = $new_contacts{$contact->{id}};
- }
-
- $template->title( $c->get_param('title') );
- my $query = { title => $template->title };
- if ($template->in_storage) {
- $query->{id} = { '!=', $template->id };
- }
- if ($c->stash->{body}->response_templates->search($query)->count) {
- $c->stash->{errors} ||= {};
- $c->stash->{errors}->{title} = _("There is already a template with that title.");
- }
-
- $template->text( $c->get_param('text') );
- $template->state( $c->get_param('state') );
- $template->external_status_code( $c->get_param('external_status_code') );
-
- if ( $template->state && $template->external_status_code ) {
- $c->stash->{errors} ||= {};
- $c->stash->{errors}->{state} = _("State and external status code cannot be used simultaneously.");
- $c->stash->{errors}->{external_status_code} = _("State and external status code cannot be used simultaneously.");
- }
-
- $template->auto_response( $c->get_param('auto_response') && ( $template->state || $template->external_status_code ) ? 1 : 0 );
- if ($template->auto_response) {
- my @check_contact_ids = @new_contact_ids;
- # If the new template has not specific categories (i.e. it
- # applies to all categories) then we need to check each of those
- # category ids for existing auto-response templates.
- if (!scalar @check_contact_ids) {
- @check_contact_ids = @live_contact_ids;
- }
- my $query = {
- 'auto_response' => 1,
- 'contact.id' => [ @check_contact_ids, undef ],
- -or => {
- $template->state ? ('me.state' => $template->state) : (),
- $template->external_status_code ? ('me.external_status_code' => $template->external_status_code) : (),
- },
- };
- if ($template->in_storage) {
- $query->{'me.id'} = { '!=', $template->id };
- }
- if ($c->stash->{body}->response_templates->search($query, {
- join => { 'contact_response_templates' => 'contact' },
- })->count) {
- $c->stash->{errors} ||= {};
- $c->stash->{errors}->{auto_response} = _("There is already an auto-response template for this category/state.");
- }
- }
-
- last if $c->stash->{errors};
-
- $template->update_or_insert;
- $template->contact_response_templates->search({
- contact_id => { -not_in => \@new_contact_ids },
- })->delete;
- foreach my $contact_id (@new_contact_ids) {
- $template->contact_response_templates->find_or_create({
- contact_id => $contact_id,
- });
- }
- }
-
- $c->res->redirect( $c->uri_for( 'templates', $c->stash->{body}->id ) );
- } }
-
- $c->stash->{response_template} = $template;
-
- $c->stash->{template} = 'admin/template_edit.html';
-}
-
-sub load_template_body : Private {
- my ($self, $c, $body_id) = @_;
-
- my $zurich_user = $c->user->from_body && $c->cobrand->moniker eq 'zurich';
- my $has_permission = $c->user->has_body_permission_to('template_edit', $body_id);
-
- unless ( $zurich_user || $has_permission ) {
- $c->detach( '/page_error_404_not_found', [] );
- }
-
- # Regular users can only view their own body's templates
- if ( !$c->user->is_superuser && $body_id ne $c->user->from_body->id ) {
- $c->res->redirect( $c->uri_for( 'templates', $c->user->from_body->id ) );
- }
-
- $c->stash->{body} = $c->model('DB::Body')->find($body_id)
- or $c->detach( '/page_error_404_not_found', [] );
-}
-
sub update_edit : Path('update_edit') : Args(1) {
my ( $self, $c, $id ) = @_;
@@ -866,8 +228,8 @@ sub update_edit : Path('update_edit') : Args(1) {
$c->stash->{update} = $update;
- if (my $rotate_photo_param = $self->_get_rotate_photo_param($c)) {
- $self->rotate_photo($c, $update, @$rotate_photo_param);
+ if (my $rotate_photo_param = $c->forward('_get_rotate_photo_param')) {
+ $c->forward('rotate_photo', [ $update, @$rotate_photo_param ]);
return 1;
}
@@ -901,9 +263,9 @@ sub update_edit : Path('update_edit') : Args(1) {
$edited = 1;
}
- my $remove_photo_param = $self->_get_remove_photo_param($c);
+ my $remove_photo_param = $c->forward('_get_remove_photo_param');
if ($remove_photo_param) {
- $self->remove_photo($c, $update, $remove_photo_param);
+ $c->forward('remove_photo', [$update, $remove_photo_param]);
}
$c->stash->{status_message} = _('Updated!');
@@ -1068,7 +430,7 @@ Rotate a photo 90 degrees left or right
=cut
# returns index of photo to rotate, if any
-sub _get_rotate_photo_param {
+sub _get_rotate_photo_param : Private {
my ($self, $c) = @_;
my $key = first { /^rotate_photo/ } keys %{ $c->req->params } or return;
my ($index) = $key =~ /(\d+)$/;
@@ -1098,7 +460,7 @@ Remove a photo from a report
=cut
# Returns index of photo(s) to remove, if any
-sub _get_remove_photo_param {
+sub _get_remove_photo_param : Private {
my ($self, $c) = @_;
return 'ALL' if $c->get_param('remove_photo');
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm
index 098c29ad4..ea03b146f 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm
@@ -280,7 +280,6 @@ sub update_contact : Private {
$contact->unset_extra_metadata( 'group' );
}
-
$c->forward('/admin/update_extra_fields', [ $contact ]);
$c->forward('contact_cobrand_extra_fields', [ $contact, \%errors ]);
@@ -306,12 +305,13 @@ sub update_contact : Private {
$c->stash->{errors} = \%errors;
} elsif ( $contact->in_storage ) {
$c->stash->{updated} = _('Values updated');
-
+ $c->forward('/admin/log_edit', [ $contact->id, 'category', 'edit' ]);
# NB: History is automatically stored by a trigger in the database
$contact->update;
} else {
$c->stash->{updated} = _('New category contact added');
$contact->insert;
+ $c->forward('/admin/log_edit', [ $contact->id, 'category', 'add' ]);
}
unless ( %errors ) {
@@ -346,6 +346,7 @@ sub confirm_contacts : Private {
}
);
+ $c->forward('/admin/log_edit', [ $c->stash->{body_id}, 'body', 'edit' ]);
$c->stash->{updated} = _('Values updated');
}
@@ -360,8 +361,10 @@ sub update_body : Private {
if ($body) {
$body->update( $values->{params} );
+ $c->forward('/admin/log_edit', [ $body->id, 'body', 'edit' ]);
} else {
$body = $c->model('DB::Body')->create( $values->{params} );
+ $c->forward('/admin/log_edit', [ $body->id, 'body', 'add' ]);
}
if ($values->{extras}) {
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Reports.pm b/perllib/FixMyStreet/App/Controller/Admin/Reports.pm
new file mode 100644
index 000000000..bee2ed498
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/Reports.pm
@@ -0,0 +1,516 @@
+package FixMyStreet::App::Controller::Admin::Reports;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use List::MoreUtils 'uniq';
+use FixMyStreet::SMS;
+use Utils;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Admin::Reports - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Admin pages
+
+=head1 METHODS
+
+=cut
+
+sub index : Path {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{edit_body_contacts} = 1
+ if grep { $_ eq 'body' } keys %{$c->stash->{allowed_pages}};
+
+ my $query = {};
+ if ( $c->cobrand->moniker eq 'zurich' ) {
+ my $type = $c->stash->{admin_type};
+ my $body = $c->stash->{body};
+ if ( $type eq 'dm' ) {
+ my @children = map { $_->id } $body->bodies->all;
+ my @all = (@children, $body->id);
+ $query = { bodies_str => \@all };
+ } elsif ( $type eq 'sdm' ) {
+ $query = { bodies_str => $body->id };
+ }
+ }
+
+ my $order = $c->get_param('o') || 'id';
+ my $dir = defined $c->get_param('d') ? $c->get_param('d') : 1;
+ $c->stash->{order} = $order;
+ $c->stash->{dir} = $dir;
+ $order = $dir ? { -desc => "me.$order" } : "me.$order";
+
+ my $p_page = $c->get_param('p') || 1;
+ my $u_page = $c->get_param('u') || 1;
+
+ return if $c->cobrand->call_hook(report_search_query => $query, $p_page, $u_page, $order);
+
+ if (my $search = $c->get_param('search')) {
+ $search = $self->trim($search);
+
+ # In case an email address, wrapped in <...>
+ if ($search =~ /^<(.*)>$/) {
+ my $possible_email = $1;
+ my $parsed = FixMyStreet::SMS->parse_username($possible_email);
+ $search = $possible_email if $parsed->{email};
+ }
+
+ $c->stash->{searched} = $search;
+
+ my $search_n = 0;
+ $search_n = int($search) if $search =~ /^\d+$/;
+
+ my $like_search = "%$search%";
+
+ my $parsed = FixMyStreet::SMS->parse_username($search);
+ my $valid_phone = $parsed->{phone};
+ my $valid_email = $parsed->{email};
+
+ if ($valid_email) {
+ $query->{'-or'} = [
+ 'user.email' => { ilike => $like_search },
+ ];
+ } elsif ($valid_phone) {
+ $query->{'-or'} = [
+ 'user.phone' => { ilike => $like_search },
+ ];
+ } elsif ($search =~ /^id:(\d+)$/) {
+ $query->{'-or'} = [
+ 'me.id' => int($1),
+ ];
+ } elsif ($search =~ /^area:(\d+)$/) {
+ $query->{'-or'} = [
+ 'me.areas' => { like => "%,$1,%" }
+ ];
+ } elsif ($search =~ /^ref:(\d+)$/) {
+ $query->{'-or'} = [
+ 'me.external_id' => { like => "%$1%" }
+ ];
+ } else {
+ $query->{'-or'} = [
+ 'me.id' => $search_n,
+ 'user.email' => { ilike => $like_search },
+ 'user.phone' => { ilike => $like_search },
+ 'me.external_id' => { ilike => $like_search },
+ 'me.name' => { ilike => $like_search },
+ 'me.title' => { ilike => $like_search },
+ detail => { ilike => $like_search },
+ bodies_str => { like => $like_search },
+ cobrand_data => { like => $like_search },
+ ];
+ }
+
+ my $problems = $c->cobrand->problems->search(
+ $query,
+ {
+ prefetch => 'user',
+ rows => 50,
+ order_by => $order,
+ }
+ )->page( $p_page );
+
+ $c->stash->{problems} = [ $problems->all ];
+ $c->stash->{problems_pager} = $problems->pager;
+
+ if ($valid_email) {
+ $query = [
+ 'user.email' => { ilike => $like_search },
+ ];
+ } elsif ($valid_phone) {
+ $query = [
+ 'user.phone' => { ilike => $like_search },
+ ];
+ } elsif ($search =~ /^id:(\d+)$/) {
+ $query = [
+ 'me.id' => int($1),
+ 'me.problem_id' => int($1),
+ ];
+ } elsif ($search =~ /^area:(\d+)$/) {
+ $query = [];
+ } else {
+ $query = [
+ 'me.id' => $search_n,
+ 'problem.id' => $search_n,
+ 'user.email' => { ilike => $like_search },
+ 'user.phone' => { ilike => $like_search },
+ 'me.name' => { ilike => $like_search },
+ text => { ilike => $like_search },
+ 'me.cobrand_data' => { ilike => $like_search },
+ ];
+ }
+
+ if (@$query) {
+ my $updates = $c->cobrand->updates->search(
+ {
+ -or => $query,
+ },
+ {
+ -select => [ 'me.*', qw/problem.bodies_str problem.state/ ],
+ prefetch => [qw/user problem/],
+ rows => 50,
+ order_by => { -desc => 'me.id' }
+ }
+ )->page( $u_page );
+ $c->stash->{updates} = [ $updates->all ];
+ $c->stash->{updates_pager} = $updates->pager;
+ }
+
+ } else {
+
+ my $problems = $c->cobrand->problems->search(
+ $query,
+ { order_by => $order, rows => 50 }
+ )->page( $p_page );
+ $c->stash->{problems} = [ $problems->all ];
+ $c->stash->{problems_pager} = $problems->pager;
+ }
+}
+
+sub edit_display : Private {
+ my ( $self, $c ) = @_;
+
+ my $problem = $c->stash->{problem};
+
+ $c->stash->{page} = 'admin';
+ FixMyStreet::Map::display_map(
+ $c,
+ latitude => $problem->latitude,
+ longitude => $problem->longitude,
+ pins => $problem->used_map
+ ? [ {
+ latitude => $problem->latitude,
+ longitude => $problem->longitude,
+ colour => $c->cobrand->pin_colour($problem, 'admin'),
+ type => 'big',
+ draggable => 1,
+ } ]
+ : [],
+ print_report => 1,
+ );
+}
+
+sub edit : Path('/admin/report_edit') : Args(1) {
+ my ( $self, $c, $id ) = @_;
+
+ my $problem = $c->cobrand->problems->search( { id => $id } )->first;
+
+ $c->detach( '/page_error_404_not_found', [] )
+ unless $problem;
+
+ unless (
+ $c->cobrand->moniker eq 'zurich'
+ || $c->user->has_permission_to(report_edit => $problem->bodies_str_ids)
+ ) {
+ $c->detach( '/page_error_403_access_denied', [] );
+ }
+
+ $c->stash->{problem} = $problem;
+ if ( $problem->extra ) {
+ my @fields;
+ if ( my $fields = $problem->get_extra_fields ) {
+ for my $field ( @{$fields} ) {
+ my $name = $field->{description} ?
+ "$field->{description} ($field->{name})" :
+ "$field->{name}";
+ push @fields, { name => $name, val => $field->{value} };
+ }
+ }
+ my $extra = $problem->get_extra_metadata;
+ if ( $extra->{duplicates} ) {
+ push @fields, { name => 'Duplicates', val => join( ',', @{ $problem->get_extra_metadata('duplicates') } ) };
+ delete $extra->{duplicates};
+ }
+ for my $key ( keys %$extra ) {
+ push @fields, { name => $key, val => $extra->{$key} };
+ }
+
+ $c->stash->{extra_fields} = \@fields;
+ }
+
+ $c->forward('/auth/get_csrf_token');
+
+ $c->forward('categories_for_point');
+
+ $c->forward('alerts_for_report');
+
+ $c->forward('/admin/check_username_for_abuse', [ $problem->user ] );
+
+ $c->stash->{updates} =
+ [ $c->model('DB::Comment')
+ ->search( { problem_id => $problem->id }, { order_by => [ 'created', 'id' ] } )
+ ->all ];
+
+ if (my $rotate_photo_param = $c->forward('/admin/_get_rotate_photo_param')) {
+ $c->forward('/admin/rotate_photo', [$problem, @$rotate_photo_param]);
+ $c->detach('edit_display');
+ }
+
+ if ( $c->cobrand->moniker eq 'zurich' ) {
+ my $done = $c->cobrand->admin_report_edit();
+ $c->detach('edit_display') if $done;
+ }
+
+ if ( $c->get_param('resend') && !$c->cobrand->call_hook('disable_resend') ) {
+ $c->forward('/auth/check_csrf_token');
+
+ $problem->resend;
+ $problem->update();
+ $c->stash->{status_message} = _('That problem will now be resent.');
+
+ $c->forward( '/admin/log_edit', [ $id, 'problem', 'resend' ] );
+ }
+ elsif ( $c->get_param('mark_sent') ) {
+ $c->forward('/auth/check_csrf_token');
+ $problem->update({ whensent => \'current_timestamp' })->discard_changes;
+ $c->stash->{status_message} = _('That problem has been marked as sent.');
+ $c->forward( '/admin/log_edit', [ $id, 'problem', 'marked sent' ] );
+ }
+ elsif ( $c->get_param('flaguser') ) {
+ $c->forward('/admin/users/flag');
+ $c->stash->{problem}->discard_changes;
+ }
+ elsif ( $c->get_param('removeuserflag') ) {
+ $c->forward('/admin/users/flag_remove');
+ $c->stash->{problem}->discard_changes;
+ }
+ elsif ( $c->get_param('banuser') ) {
+ $c->forward('/admin/users/ban');
+ }
+ elsif ( $c->get_param('submit') ) {
+ $c->forward('/auth/check_csrf_token');
+
+ my $old_state = $problem->state;
+
+ my %columns = (
+ flagged => $c->get_param('flagged') ? 1 : 0,
+ non_public => $c->get_param('non_public') ? 1 : 0,
+ );
+ foreach (qw/state anonymous title detail name external_id external_body external_team/) {
+ $columns{$_} = $c->get_param($_);
+ }
+
+ # Look this up here for moderation line to use
+ my $remove_photo_param = $c->forward('/admin/_get_remove_photo_param');
+
+ if ($columns{title} ne $problem->title || $columns{detail} ne $problem->detail ||
+ $columns{anonymous} ne $problem->anonymous || $remove_photo_param) {
+ $problem->create_related( moderation_original_data => {
+ title => $problem->title,
+ detail => $problem->detail,
+ photo => $problem->photo,
+ anonymous => $problem->anonymous,
+ category => $problem->category,
+ $problem->extra ? (extra => $problem->extra) : (),
+ });
+ }
+
+ $problem->set_inflated_columns(\%columns);
+
+ if ($c->get_param('closed_updates')) {
+ $problem->set_extra_metadata(closed_updates => 1);
+ } else {
+ $problem->unset_extra_metadata('closed_updates');
+ }
+
+ $c->forward( '/admin/reports/edit_category', [ $problem, $problem->state ne $old_state ] );
+ $c->forward('/admin/update_user', [ $problem ]);
+
+ # Deal with photos
+ if ($remove_photo_param) {
+ $c->forward('/admin/remove_photo', [ $problem, $remove_photo_param ]);
+ }
+
+ if ($problem->state eq 'hidden' || $problem->non_public) {
+ $problem->get_photoset->delete_cached(plus_updates => 1);
+ }
+
+ if ( $problem->is_visible() and $old_state eq 'unconfirmed' ) {
+ $problem->confirmed( \'current_timestamp' );
+ }
+
+ $problem->lastupdate( \'current_timestamp' );
+ $problem->update;
+
+ if ( $problem->state ne $old_state ) {
+ $c->forward( '/admin/log_edit', [ $id, 'problem', 'state_change' ] );
+
+ my $name = $c->user->moderating_user_name;
+ my $extra = { is_superuser => 1 };
+ if ($c->user->from_body) {
+ delete $extra->{is_superuser};
+ $extra->{is_body_user} = $c->user->from_body->id;
+ }
+ my $timestamp = \'current_timestamp';
+ $problem->add_to_comments( {
+ text => $c->stash->{update_text} || '',
+ created => $timestamp,
+ confirmed => $timestamp,
+ user_id => $c->user->id,
+ name => $name,
+ mark_fixed => 0,
+ anonymous => 0,
+ state => 'confirmed',
+ problem_state => $problem->state,
+ extra => $extra
+ } );
+ }
+ $c->forward( '/admin/log_edit', [ $id, 'problem', 'edit' ] );
+
+ $c->stash->{status_message} = _('Updated!');
+
+ # do this here otherwise lastupdate and confirmed times
+ # do not display correctly
+ $problem->discard_changes;
+ }
+
+ $c->detach('edit_display');
+}
+
+=head2 edit_category
+
+Handles changing a problem's category and the complexity that comes with it.
+Returns 1 if category changed, 0 if no change.
+
+=cut
+
+sub edit_category : Private {
+ my ($self, $c, $problem, $no_comment) = @_;
+
+ if ((my $category = $c->get_param('category')) ne $problem->category) {
+ my $disable_resend = $c->cobrand->call_hook('disable_resend');
+ my $category_old = $problem->category;
+ $problem->category($category);
+ my @contacts = grep { $_->category eq $problem->category } @{$c->stash->{contacts}};
+ my @new_body_ids = map { $_->body_id } @contacts;
+ # If the report has changed bodies (and not to a subset!) we need to resend it
+ my %old_map = map { $_ => 1 } @{$problem->bodies_str_ids};
+ if (!$disable_resend && grep !$old_map{$_}, @new_body_ids) {
+ $problem->resend;
+ }
+ # If the send methods of the old/new contacts differ we need to resend the report
+ my @new_send_methods = uniq map {
+ ( $_->body->can_be_devolved && $_->send_method ) ?
+ $_->send_method : $_->body->send_method
+ ? $_->body->send_method
+ : $c->cobrand->_fallback_body_sender()->{method};
+ } @contacts;
+ my %old_send_methods = map { $_ => 1 } split /,/, ($problem->send_method_used || "Email");
+ if (!$disable_resend && grep !$old_send_methods{$_}, @new_send_methods) {
+ $problem->resend;
+ }
+
+ $problem->bodies_str(join( ',', @new_body_ids ));
+ my $update_text = '*' . sprintf(_('Category changed from ‘%s’ to ‘%s’'), $category_old, $category) . '*';
+ if ($no_comment) {
+ $c->stash->{update_text} = $update_text;
+ } else {
+ $problem->add_to_comments({
+ text => $update_text,
+ created => \'current_timestamp',
+ confirmed => \'current_timestamp',
+ user_id => $c->user->id,
+ name => $c->user->from_body ? $c->user->from_body->name : $c->user->name,
+ state => 'confirmed',
+ mark_fixed => 0,
+ anonymous => 0,
+ });
+ }
+ $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'category_change' ] );
+ return 1;
+ }
+ return 0;
+}
+
+=head2 edit_location
+
+Handles changing a problem's location and the complexity that comes with it.
+For now, we reject the new location if the new location and old locations aren't
+covered by the same body.
+
+Returns 2 if the new position (if any) is acceptable and changed,
+1 if acceptable and unchanged, undef otherwise.
+
+NB: This must be called before edit_category, as that might modify
+$problem->bodies_str.
+
+=cut
+
+sub edit_location : Private {
+ my ($self, $c, $problem) = @_;
+
+ return 1 unless $c->forward('/location/determine_location_from_coords');
+
+ my ($lat, $lon) = map { Utils::truncate_coordinate($_) } $problem->latitude, $problem->longitude;
+ if ( $c->stash->{latitude} != $lat || $c->stash->{longitude} != $lon ) {
+ # The two actions below change the stash, setting things up for e.g. a
+ # new report. But here we're only doing it in order to check the found
+ # bodies match; we don't want to overwrite the existing report data if
+ # this lookup is bad. So let's save the stash and restore it after the
+ # comparison.
+ my $safe_stash = { %{$c->stash} };
+ $c->stash->{fetch_all_areas} = 1;
+ $c->stash->{area_check_action} = 'admin';
+ $c->forward('/council/load_and_check_areas', []);
+ $c->forward('/report/new/setup_categories_and_bodies');
+ my %allowed_bodies = map { $_ => 1 } @{$problem->bodies_str_ids};
+ my @new_bodies = keys %{$c->stash->{bodies_to_list}};
+ my $bodies_match = grep { exists( $allowed_bodies{$_} ) } @new_bodies;
+ $c->stash($safe_stash);
+ return unless $bodies_match;
+ $problem->latitude($c->stash->{latitude});
+ $problem->longitude($c->stash->{longitude});
+ my $areas = $c->stash->{all_areas_mapit};
+ $problem->areas( ',' . join( ',', sort keys %$areas ) . ',' );
+ return 2;
+ }
+ return 1;
+}
+
+sub categories_for_point : Private {
+ my ($self, $c) = @_;
+
+ $c->stash->{report} = $c->stash->{problem};
+ # We have a report, stash its location
+ $c->forward('/report/new/determine_location_from_report');
+ # Look up the areas for this location
+ my $prefetched_all_areas = [ grep { $_ } split ',', $c->stash->{report}->areas ];
+ $c->forward('/around/check_location_is_acceptable', [ $prefetched_all_areas ]);
+ # As with a new report, fetch the bodies/categories
+ $c->forward('/report/new/setup_categories_and_bodies');
+
+ # Remove the "Pick a category" option
+ shift @{$c->stash->{category_options}} if @{$c->stash->{category_options}};
+
+ $c->stash->{categories_hash} = { map { $_->category => 1 } @{$c->stash->{category_options}} };
+
+ $c->forward('/admin/triage/setup_categories');
+
+}
+
+sub alerts_for_report : Private {
+ my ($self, $c) = @_;
+
+ $c->stash->{alert_count} = $c->model('DB::Alert')->search({
+ alert_type => 'new_updates',
+ parameter => $c->stash->{report}->id,
+ confirmed => 1,
+ whendisabled => undef,
+ })->count();
+}
+
+sub trim {
+ my $self = shift;
+ my $e = shift;
+ $e =~ s/^\s+//;
+ $e =~ s/\s+$//;
+ return $e;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Roles.pm b/perllib/FixMyStreet/App/Controller/Admin/Roles.pm
index 902ed6255..279ee695c 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/Roles.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/Roles.pm
@@ -63,6 +63,7 @@ sub form {
if ($c->get_param('delete_role')) {
$role->delete;
+ $c->forward('/admin/log_edit', [ $role->id, 'role', 'delete' ]);
$c->response->redirect($c->uri_for($self->action_for('index')));
$c->detach;
}
@@ -88,11 +89,13 @@ sub form {
$opts->{body_id} = $c->user->from_body->id;
}
+ my $action = $role->in_storage ? 'edit' : 'add';
my $form = FixMyStreet::App::Form::Role->new(%$opts);
$c->stash(template => 'admin/roles/form.html', form => $form);
$form->process(item => $role, params => $c->req->params);
return unless $form->validated;
+ $c->forward('/admin/log_edit', [ $role->id, 'role', $action ]);
$c->response->redirect($c->uri_for($self->action_for('index')));
}
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Templates.pm b/perllib/FixMyStreet/App/Controller/Admin/Templates.pm
new file mode 100644
index 000000000..97a62c2b8
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/Templates.pm
@@ -0,0 +1,179 @@
+package FixMyStreet::App::Controller::Admin::Templates;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Admin::Templates - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Admin pages for response templates
+
+=head1 METHODS
+
+=cut
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $user = $c->user;
+
+ if ($user->is_superuser) {
+ $c->forward('/admin/fetch_all_bodies');
+ } elsif ( $user->from_body ) {
+ $c->forward('load_template_body', [ $user->from_body->id ]);
+ $c->res->redirect( $c->uri_for_action( '/admin/templates/view', $c->stash->{body}->id ) );
+ } else {
+ $c->detach( '/page_error_404_not_found', [] );
+ }
+}
+
+sub view : Path : Args(1) {
+ my ($self, $c, $body_id) = @_;
+
+ $c->forward('load_template_body', [ $body_id ]);
+
+ my @templates = $c->stash->{body}->response_templates->search(
+ undef,
+ {
+ order_by => 'title'
+ }
+ );
+
+ $c->stash->{response_templates} = \@templates;
+}
+
+sub edit : Path : Args(2) {
+ my ( $self, $c, $body_id, $template_id ) = @_;
+
+ $c->forward('load_template_body', [ $body_id ]);
+
+ my $template;
+ if ($template_id eq 'new') {
+ $template = $c->stash->{body}->response_templates->new({});
+ }
+ else {
+ $template = $c->stash->{body}->response_templates->find( $template_id )
+ or $c->detach( '/page_error_404_not_found', [] );
+ }
+
+ $c->forward('/admin/fetch_contacts');
+ my @contacts = $template->contacts->all;
+ my @live_contacts = $c->stash->{live_contacts}->all;
+ my %active_contacts = map { $_->id => 1 } @contacts;
+ my @all_contacts = map { {
+ id => $_->id,
+ category => $_->category_display,
+ active => $active_contacts{$_->id},
+ email => $_->email,
+ } } @live_contacts;
+ $c->stash->{contacts} = \@all_contacts;
+
+ # bare block to use 'last' if form is invalid.
+ if ($c->req->method eq 'POST') { {
+ if ($c->get_param('delete_template') && $c->get_param('delete_template') eq _("Delete template")) {
+ $template->contact_response_templates->delete_all;
+ $template->delete;
+ $c->forward('/admin/log_edit', [ $template->id, 'template', 'delete' ]);
+ } else {
+ my @live_contact_ids = map { $_->id } @live_contacts;
+ my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @live_contact_ids;
+ my %new_contacts = map { $_ => 1 } @new_contact_ids;
+ for my $contact (@all_contacts) {
+ $contact->{active} = $new_contacts{$contact->{id}};
+ }
+
+ $template->title( $c->get_param('title') );
+ my $query = { title => $template->title };
+ if ($template->in_storage) {
+ $query->{id} = { '!=', $template->id };
+ }
+ if ($c->stash->{body}->response_templates->search($query)->count) {
+ $c->stash->{errors} ||= {};
+ $c->stash->{errors}->{title} = _("There is already a template with that title.");
+ }
+
+ $template->text( $c->get_param('text') );
+ $template->state( $c->get_param('state') );
+ $template->external_status_code( $c->get_param('external_status_code') );
+
+ if ( $template->state && $template->external_status_code ) {
+ $c->stash->{errors} ||= {};
+ $c->stash->{errors}->{state} = _("State and external status code cannot be used simultaneously.");
+ $c->stash->{errors}->{external_status_code} = _("State and external status code cannot be used simultaneously.");
+ }
+
+ $template->auto_response( $c->get_param('auto_response') && ( $template->state || $template->external_status_code ) ? 1 : 0 );
+ if ($template->auto_response) {
+ my @check_contact_ids = @new_contact_ids;
+ # If the new template has not specific categories (i.e. it
+ # applies to all categories) then we need to check each of those
+ # category ids for existing auto-response templates.
+ if (!scalar @check_contact_ids) {
+ @check_contact_ids = @live_contact_ids;
+ }
+ my $query = {
+ 'auto_response' => 1,
+ 'contact.id' => [ @check_contact_ids, undef ],
+ -or => {
+ $template->state ? ('me.state' => $template->state) : (),
+ $template->external_status_code ? ('me.external_status_code' => $template->external_status_code) : (),
+ },
+ };
+ if ($template->in_storage) {
+ $query->{'me.id'} = { '!=', $template->id };
+ }
+ if ($c->stash->{body}->response_templates->search($query, {
+ join => { 'contact_response_templates' => 'contact' },
+ })->count) {
+ $c->stash->{errors} ||= {};
+ $c->stash->{errors}->{auto_response} = _("There is already an auto-response template for this category/state.");
+ }
+ }
+
+ last if $c->stash->{errors};
+
+ $template->update_or_insert;
+ $template->contact_response_templates->search({
+ contact_id => { -not_in => \@new_contact_ids },
+ })->delete;
+ foreach my $contact_id (@new_contact_ids) {
+ $template->contact_response_templates->find_or_create({
+ contact_id => $contact_id,
+ });
+ }
+ my $action = $template_id eq 'new' ? 'add' : 'edit';
+ $c->forward('/admin/log_edit', [ $template->id, 'template', $action ]);
+ }
+
+ $c->res->redirect( $c->uri_for_action( '/admin/templates/view', $c->stash->{body}->id ) );
+ } }
+
+ $c->stash->{response_template} = $template;
+}
+
+sub load_template_body : Private {
+ my ($self, $c, $body_id) = @_;
+
+ my $zurich_user = $c->user->from_body && $c->cobrand->moniker eq 'zurich';
+ my $has_permission = $c->user->has_body_permission_to('template_edit', $body_id);
+
+ unless ( $zurich_user || $has_permission ) {
+ $c->detach( '/page_error_404_not_found', [] );
+ }
+
+ # Regular users can only view their own body's templates
+ if ( !$c->user->is_superuser && $body_id ne $c->user->from_body->id ) {
+ $c->res->redirect( $c->uri_for_action( '/admin/templates/view', $c->user->from_body->id ) );
+ }
+
+ $c->stash->{body} = $c->model('DB::Body')->find($body_id)
+ or $c->detach( '/page_error_404_not_found', [] );
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Triage.pm b/perllib/FixMyStreet/App/Controller/Admin/Triage.pm
index 0eabd340d..428c35073 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/Triage.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/Triage.pm
@@ -113,7 +113,7 @@ sub update : Private {
my $current_category = $problem->category;
my $new_category = $c->get_param('category');
- my $changed = $c->forward('/admin/report_edit_category', [ $problem, 1 ] );
+ my $changed = $c->forward('/admin/reports/edit_category', [ $problem, 1 ] );
if ( $changed ) {
$c->stash->{problem}->update( { state => 'confirmed' } );
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Users.pm b/perllib/FixMyStreet/App/Controller/Admin/Users.pm
index 0d7c23fff..802fbb9f5 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/Users.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/Users.pm
@@ -171,7 +171,7 @@ sub add : Local : Args(0) {
$c->forward('user_cobrand_extra_fields');
$user->insert;
- $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'add' ] );
$c->flash->{status_message} = _("Updated!");
$c->detach('post_edit_redirect', [ $user ]);
@@ -189,19 +189,24 @@ sub fetch_body_roles : Private {
$c->stash->{roles} = [ $roles->all ];
}
-sub edit : Path : Args(1) {
+sub user : Chained('/') PathPart('admin/users') : CaptureArgs(1) {
my ( $self, $c, $id ) = @_;
- $c->forward('/auth/get_csrf_token');
-
my $user = $c->cobrand->users->find( { id => $id } );
$c->detach( '/page_error_404_not_found', [] ) unless $user;
+ $c->stash->{user} = $user;
unless ( $c->user->has_body_permission_to('user_edit') || $c->cobrand->moniker eq 'zurich' ) {
$c->detach('/page_error_403_access_denied', []);
}
+}
- $c->stash->{user} = $user;
+sub edit : Chained('user') : PathPart('') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->forward('/auth/get_csrf_token');
+
+ my $user = $c->stash->{user};
$c->forward( '/admin/check_username_for_abuse', [ $user ] );
if ( $user->from_body && $c->user->has_permission_to('user_manage_permissions', $user->from_body->id) ) {
@@ -232,14 +237,12 @@ sub edit : Path : Args(1) {
} elsif ( $c->get_param('submit') and $c->get_param('send_login_email') ) {
my $email = lc $c->get_param('email');
my %args = ( email => $email );
- $args{user_id} = $id if $user->email ne $email || !$user->email_verified;
+ $args{user_id} = $user->id if $user->email ne $email || !$user->email_verified;
$c->forward('send_login_email', [ \%args ]);
} elsif ( $c->get_param('update_alerts') ) {
$c->forward('update_alerts');
} elsif ( $c->get_param('submit') ) {
- my $edited = 0;
-
my $name = $c->get_param('name');
my $email = lc $c->get_param('email');
my $phone = $c->get_param('phone');
@@ -281,19 +284,10 @@ sub edit : Path : Args(1) {
return if %{$c->stash->{field_errors}};
- if ( ($user->email || "") ne $email ||
- $user->name ne $name ||
- ($user->phone || "") ne $phone ||
- ($user->from_body && $c->get_param('body') && $user->from_body->id ne $c->get_param('body')) ||
- (!$user->from_body && $c->get_param('body'))
- ) {
- $edited = 1;
- }
-
if ($existing_user_cobrand) {
$existing_user->adopt($user);
- $c->forward( '/admin/log_edit', [ $id, 'user', 'merge' ] );
- return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $existing_user->id ) );
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'merge' ] );
+ return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', [ $existing_user->id ] ) );
}
$user->email($email) if !$existing_email;
@@ -382,9 +376,7 @@ sub edit : Path : Args(1) {
}
$user->update;
- if ($edited) {
- $c->forward( '/admin/log_edit', [ $id, 'user', 'edit' ] );
- }
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
$c->flash->{status_message} = _("Updated!");
$c->detach('post_edit_redirect', [ $user ]);
@@ -414,13 +406,45 @@ sub edit : Path : Args(1) {
return 1;
}
+sub log : Chained('user') : PathPart('log') : Args(0) {
+ my ($self, $c) = @_;
+
+ my $user = $c->stash->{user};
+
+ my $after = $c->get_param('after');
+
+ my %time;
+ foreach ($user->admin_logs->all) {
+ push @{$time{$_->whenedited->epoch}}, { type => 'log', date => $_->whenedited, log => $_ };
+ }
+ foreach ($c->cobrand->problems->search({ extra => { like => '%contributed_by%' . $user->id . '%' } })->all) {
+ next unless $_->get_extra_metadata('contributed_by') == $user->id;
+ push @{$time{$_->created->epoch}}, { type => 'problemContributedBy', date => $_->created, obj => $_ };
+ }
+
+ foreach ($user->user_planned_reports->all) {
+ push @{$time{$_->added->epoch}}, { type => 'shortlistAdded', date => $_->added, obj => $_->report };
+ push @{$time{$_->removed->epoch}}, { type => 'shortlistRemoved', date => $_->removed, obj => $_->report } if $_->removed;
+ }
+
+ foreach ($user->problems->all) {
+ push @{$time{$_->created->epoch}}, { type => 'problem', date => $_->created, obj => $_ };
+ }
+
+ foreach ($user->comments->all) {
+ push @{$time{$_->created->epoch}}, { type => 'update', date => $_->created, obj => $_};
+ }
+
+ $c->stash->{time} = \%time;
+}
+
sub post_edit_redirect : Private {
my ( $self, $c, $user ) = @_;
# User may not be visible on this cobrand, e.g. if their from_body
# wasn't set.
if ( $c->cobrand->users->find( { id => $user->id } ) ) {
- return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $user->id ) );
+ return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', [ $user->id ] ) );
} else {
return $c->res->redirect( $c->uri_for_action( 'admin/users/index' ) );
}
@@ -605,6 +629,7 @@ sub user_remove_account : Private {
my ( $self, $c, $user ) = @_;
$c->forward('user_logout_everywhere', [ $user ]);
$user->anonymize_account;
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
$c->stash->{status_message} = _('That user’s personal details have been removed.');
}
@@ -632,6 +657,7 @@ sub ban : Private {
$c->stash->{status_message} = _('User already in abuse list');
} else {
$abuse->insert;
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
$c->stash->{status_message} = _('User added to abuse list');
}
$c->stash->{username_in_abuse} = 1;
@@ -642,6 +668,7 @@ sub ban : Private {
$c->stash->{status_message} = _('User already in abuse list');
} else {
$abuse->insert;
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
$c->stash->{status_message} = _('User added to abuse list');
}
$c->stash->{username_in_abuse} = 1;
@@ -663,6 +690,7 @@ sub unban : Private {
my $abuse = $c->model('DB::Abuse')->search({ email => \@username });
if ( $abuse ) {
$abuse->delete;
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
$c->stash->{status_message} = _('user removed from abuse list');
} else {
$c->stash->{status_message} = _('user not in abuse list');
@@ -692,6 +720,7 @@ sub flag : Private {
} else {
$user->flagged(1);
$user->update;
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
$c->stash->{status_message} = _('User flagged');
}
@@ -721,6 +750,7 @@ sub flag_remove : Private {
} else {
$user->flagged(0);
$user->update;
+ $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] );
$c->stash->{status_message} = _('User flag removed');
}
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm
index f71698e84..8a9f3b5a2 100644
--- a/perllib/FixMyStreet/App/Controller/Moderate.pm
+++ b/perllib/FixMyStreet/App/Controller/Moderate.pm
@@ -298,7 +298,7 @@ sub moderate_location : Private {
my $problem = $c->stash->{problem};
- my $moved = $c->forward('/admin/report_edit_location', [ $problem ]);
+ my $moved = $c->forward('/admin/reports/edit_location', [ $problem ]);
if (!$moved) {
# New lat/lon isn't valid, show an error
$c->stash->{moderate_errors} ||= [];
@@ -315,11 +315,11 @@ sub moderate_category : Private {
return unless $c->get_param('category');
# The admin category editing needs to know all the categories etc
- $c->forward('/admin/categories_for_point');
+ $c->forward('/admin/reports/categories_for_point');
my $problem = $c->stash->{problem};
- my $changed = $c->forward( '/admin/report_edit_category', [ $problem, 1 ] );
+ my $changed = $c->forward( '/admin/reports/edit_category', [ $problem, 1 ] );
# It might need to set_report_extras in future
if ($changed) {
return 'category';
diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm
index 89350b1cb..190687d41 100644
--- a/perllib/FixMyStreet/App/Controller/Report.pm
+++ b/perllib/FixMyStreet/App/Controller/Report.pm
@@ -372,7 +372,7 @@ sub inspect : Private {
my $problem = $c->stash->{problem};
my $permissions = $c->stash->{_permissions};
- $c->forward('/admin/categories_for_point');
+ $c->forward('/admin/reports/categories_for_point');
$c->stash->{report_meta} = { map { 'x' . $_->{name} => $_ } @{ $c->stash->{problem}->get_extra_fields() } };
if ($c->cobrand->can('council_area_id')) {
@@ -477,7 +477,7 @@ sub inspect : Private {
$problem->get_photoset->delete_cached(plus_updates => 1);
}
- if ( !$c->forward( '/admin/report_edit_location', [ $problem ] ) ) {
+ if ( !$c->forward( '/admin/reports/edit_location', [ $problem ] ) ) {
# New lat/lon isn't valid, show an error
$valid = 0;
$c->stash->{errors} ||= [];
@@ -485,10 +485,11 @@ sub inspect : Private {
}
if ($permissions->{report_inspect} || $permissions->{report_edit_category}) {
- $c->forward( '/admin/report_edit_category', [ $problem, 1 ] );
+ $c->forward( '/admin/reports/edit_category', [ $problem, 1 ] );
if ($c->stash->{update_text}) {
- $update_text .= "\n\n" . $c->stash->{update_text};
+ $update_text .= "\n\n" if $update_text;
+ $update_text .= $c->stash->{update_text};
}
# The new category might require extra metadata (e.g. pothole size), so
@@ -515,6 +516,7 @@ sub inspect : Private {
if ($valid) {
$problem->lastupdate( \'current_timestamp' );
$problem->update;
+ $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'edit' ] );
if ($update_text || %update_params) {
my $timestamp = \'current_timestamp';
if (my $saved_at = $c->get_param('saved_at')) {
diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm
index 6bbbdc775..1040116ed 100644
--- a/perllib/FixMyStreet/Cobrand/Zurich.pm
+++ b/perllib/FixMyStreet/Cobrand/Zurich.pm
@@ -364,7 +364,7 @@ sub set_problem_state {
my ($self, $c, $problem, $new_state) = @_;
return $self->update_admin_log($c, $problem) if $new_state eq $problem->state;
$problem->state( $new_state );
- $c->forward( 'log_edit', [ $problem->id, 'problem', "state change to $new_state" ] );
+ $c->forward( '/admin/log_edit', [ $problem->id, 'problem', "state change to $new_state" ] );
}
=head1 C<update_admin_log>
@@ -388,7 +388,7 @@ sub update_admin_log {
$text = "Logging time_spent";
}
- $c->forward( 'log_edit', [ $problem->id, 'problem', $text, $time_spent ] );
+ $c->forward( '/admin/log_edit', [ $problem->id, 'problem', $text, $time_spent ] );
}
# Any user with from_body set can view admin
@@ -898,7 +898,7 @@ sub admin_report_edit {
$self->set_problem_state($c, $problem, 'confirmed');
}
$problem->update;
- $c->forward( 'log_edit', [ $problem->id, 'problem',
+ $c->forward( '/admin/log_edit', [ $problem->id, 'problem',
$not_contactable ?
_('Customer not contactable')
: _('Sent report back') ] );
diff --git a/perllib/FixMyStreet/DB/Result/AdminLog.pm b/perllib/FixMyStreet/DB/Result/AdminLog.pm
index 221690405..5564d829a 100644
--- a/perllib/FixMyStreet/DB/Result/AdminLog.pm
+++ b/perllib/FixMyStreet/DB/Result/AdminLog.pm
@@ -61,4 +61,79 @@ __PACKAGE__->belongs_to(
# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BLPP1KitphuY56ptaXhzgg
+sub link {
+ my $self = shift;
+
+ my $type = $self->object_type;
+ my $id = $self->object_id;
+ return "/report/$id" if $type eq 'problem';
+ return "/admin/users/$id" if $type eq 'user';
+ return "/admin/body/$id" if $type eq 'body';
+ return "/admin/roles/$id" if $type eq 'role';
+ if ($type eq 'update') {
+ my $update = $self->object;
+ return "/report/" . $update->problem_id . "#update_$id";
+ }
+ if ($type eq 'moderation') {
+ my $mod = $self->object;
+ if ($mod->comment_id) {
+ my $update = $self->result_source->schema->resultset('Comment')->find($mod->comment_id);
+ return "/report/" . $update->problem_id . "#update_" . $mod->comment_id;
+ } else {
+ return "/report/" . $mod->problem_id;
+ }
+ }
+ if ($type eq 'template') {
+ my $template = $self->object;
+ return "/admin/templates/" . $template->body_id . "/$id";
+ }
+ if ($type eq 'category') {
+ my $category = $self->object;
+ return "/admin/body/" . $category->body_id . '/' . $category->category;
+ }
+ return '';
+}
+
+sub actual_object_type {
+ my $self = shift;
+ my $type = $self->object_type;
+ return $type unless $type eq 'moderation' && $self->object;
+ return $self->object->comment_id ? 'update' : 'report';
+}
+
+sub object_summary {
+ my $self = shift;
+ my $object = $self->object;
+ return unless $object;
+
+ return $object->comment_id || $object->problem_id if $self->object_type eq 'moderation';
+ return $object->email || $object->phone || $object->id if $self->object_type eq 'user';
+
+ my $type_to_thing = {
+ body => 'name',
+ role => 'name',
+ template => 'title',
+ category => 'category',
+ };
+ my $thing = $type_to_thing->{$self->object_type} || 'id';
+
+ return $object->$thing;
+}
+
+sub object {
+ my $self = shift;
+
+ my $type = $self->object_type;
+ my $id = $self->object_id;
+ my $type_to_object = {
+ moderation => 'ModerationOriginalData',
+ template => 'ResponseTemplate',
+ category => 'Contact',
+ update => 'Comment',
+ };
+ $type = $type_to_object->{$type} || ucfirst $type;
+ my $object = $self->result_source->schema->resultset($type)->find($id);
+ return $object;
+}
+
1;
diff --git a/perllib/FixMyStreet/Template.pm b/perllib/FixMyStreet/Template.pm
index 9c565114b..ba4376959 100644
--- a/perllib/FixMyStreet/Template.pm
+++ b/perllib/FixMyStreet/Template.pm
@@ -113,7 +113,7 @@ into <br>s too.
sub html_paragraph : Filter('html_para') {
my $text = shift;
- my @paras = split(/(?:\r?\n){2,}/, $text);
+ my @paras = grep { $_ } split(/(?:\r?\n){2,}/, $text);
s/\r?\n/<br>\n/g for @paras;
$text = "<p>\n" . join("\n</p>\n\n<p>\n", @paras) . "</p>\n";
return $text;
diff --git a/t/app/controller/admin/bodies.t b/t/app/controller/admin/bodies.t
index cd86e3da6..aaa9bca65 100644
--- a/t/app/controller/admin/bodies.t
+++ b/t/app/controller/admin/bodies.t
@@ -327,4 +327,11 @@ FixMyStreet::override_config {
};
};
+subtest 'check log of the above' => sub {
+ $mech->get_ok('/admin/users/' . $superuser->id . '/log');
+ $mech->content_contains('Added category <a href="/admin/body/' . $body->id . '/test/category">test/category</a>');
+ $mech->content_contains('Edited category <a href="/admin/body/' . $body->id . '/test category">test category</a>');
+ $mech->content_contains('Edited body <a href="/admin/body/' . $body->id . '">Aberdeen City Council</a>');
+};
+
done_testing();
diff --git a/t/app/controller/admin/report_edit.t b/t/app/controller/admin/report_edit.t
index c6e03ff7e..85f83781c 100644
--- a/t/app/controller/admin/report_edit.t
+++ b/t/app/controller/admin/report_edit.t
@@ -353,7 +353,7 @@ foreach my $test (
user_body => $oxfordshire,
changes => { state => 'in progress', category => 'Potholes' },
log_entries => [
- qw/edit state_change edit state_change edit edit resend edit state_change edit state_change edit state_change edit state_change edit state_change edit edit edit edit edit/
+ qw/edit state_change category_change edit state_change edit edit resend edit state_change edit state_change edit state_change edit state_change edit state_change edit edit edit edit edit/
],
resend => 0,
},
@@ -412,6 +412,13 @@ foreach my $test (
delete $test->{changes}->{closed_updates};
}
+ if ($test->{changes}{title} || $test->{changes}{detail} || $test->{changes}{anonymous}) {
+ $mech->get_ok("/report/$report_id");
+ $mech->content_contains("Anonymous: <del style='background-color:#fcc'>No</del><ins style='background-color:#cfc'>Yes</ins>") if $test->{changes}{anonymous};
+ $mech->content_contains("Details: <ins style='background-color:#cfc'>Edited </ins>Detail<del style='background-color:#fcc'> for Report to Edit</del>") if $test->{changes}{detail};
+ $mech->content_contains("Subject: <ins style='background-color:#cfc'>Edited </ins>Repor<del style='background-color:#fcc'>t to Edi</del>") if $test->{changes}{title};
+ }
+
is $report->$_, $test->{changes}->{$_}, "$_ updated" for grep { $_ ne 'username' } keys %{ $test->{changes} };
if ( $test->{user} ) {
diff --git a/t/app/controller/admin/roles.t b/t/app/controller/admin/roles.t
index 6dd40cbb6..bc8371404 100644
--- a/t/app/controller/admin/roles.t
+++ b/t/app/controller/admin/roles.t
@@ -22,7 +22,7 @@ $user->user_body_permissions->create({
permission_type => 'report_edit_priority',
});
-FixMyStreet::DB->resultset("Role")->create({
+my $role_a = FixMyStreet::DB->resultset("Role")->create({
body => $body,
name => 'Role A',
permissions => ['moderate', 'user_edit'],
@@ -129,4 +129,11 @@ subtest 'superuser can see all bodies' => sub {
$mech->content_contains('Role C');
};
+subtest 'check log of the above' => sub {
+ my $id = FixMyStreet::DB->resultset("Role")->find({ name => "Role B" })->id;
+ $mech->get_ok('/admin/users/' . $editor->id . '/log');
+ $mech->content_contains('Added role <a href="/admin/roles/' . $id . '">Role B</a>');
+ $mech->content_contains('Deleted role ' . $role_a->id);
+};
+
done_testing();
diff --git a/t/app/controller/admin/templates.t b/t/app/controller/admin/templates.t
index 6944f4b04..39903deb1 100644
--- a/t/app/controller/admin/templates.t
+++ b/t/app/controller/admin/templates.t
@@ -45,8 +45,6 @@ my $report = FixMyStreet::App->model('DB::Problem')->find_or_create(
}
);
-$mech->log_in_ok( $superuser->email );
-
my $report_id = $report->id;
ok $report, "created test report - $report_id";
@@ -63,7 +61,13 @@ subtest "response templates can be added" => sub {
};
$mech->submit_form_ok( { with_fields => $fields } );
- is $oxfordshire->response_templates->count, 1, "Response template was added";
+ is $oxfordshire->response_templates->count, 1, "Response template was added";
+};
+
+subtest 'check log of the above' => sub {
+ my $template_id = $oxfordshire->response_templates->first->id;
+ $mech->get_ok('/admin/users/' . $superuser->id . '/log');
+ $mech->content_contains('Added template <a href="/admin/templates/' . $oxfordshire->id . '/' . $template_id . '">Report acknowledgement</a>');
};
subtest "but not another with the same title" => sub {
@@ -217,7 +221,6 @@ subtest "auto-response templates that duplicate external_status_code can't be ad
});
is $oxfordshire->response_templates->count, 1, "Initial response template was created";
-
$mech->log_in_ok( $superuser->email );
$mech->get_ok( "/admin/templates/" . $oxfordshire->id . "/new" );
diff --git a/t/app/controller/admin/users.t b/t/app/controller/admin/users.t
index 17256a214..95aac051d 100644
--- a/t/app/controller/admin/users.t
+++ b/t/app/controller/admin/users.t
@@ -3,6 +3,7 @@ use FixMyStreet::TestMech;
my $mech = FixMyStreet::TestMech->new;
my $user = $mech->create_user_ok('test@example.com', name => 'Test User');
+my $original_user_id = $user->id; # For log later
my $user2 = $mech->create_user_ok('test2@example.com', name => 'Test User 2');
my $user3 = $mech->create_user_ok('test3@example.com', name => 'Test User 3');
@@ -649,4 +650,25 @@ subtest "View timeline" => sub {
$mech->get_ok('/admin/timeline');
};
+subtest 'View user log' => sub {
+ my $p = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id })->first;
+ $user->add_to_planned_reports($p);
+
+ # User 1 created all the reports
+ my $id = $p->id;
+ $mech->get_ok('/admin/users?search=' . $user->email);
+ $mech->follow_link_ok({ text => 'Timeline', n => 2 });
+ $mech->content_like(qr/Problem.*?>$id<\/a> created/);
+ $mech->content_like(qr/Problem.*?>$id<\/a> added to shortlist/);
+
+ # User 3 edited user 2 above
+ $mech->get_ok('/admin/users/' . $user3->id . '/log');
+ $mech->content_like(qr/Edited user.*?test2\@example/);
+
+ # Superuser added a user, and merged one
+ $mech->get_ok('/admin/users/' . $superuser->id . '/log');
+ $mech->content_like(qr/Added user.*?0156/);
+ $mech->content_like(qr/Merged user $original_user_id/);
+};
+
done_testing();
diff --git a/t/app/controller/moderate.t b/t/app/controller/moderate.t
index e22d9edbc..fdbd0abb6 100644
--- a/t/app/controller/moderate.t
+++ b/t/app/controller/moderate.t
@@ -475,8 +475,6 @@ subtest 'updates' => sub {
}});
$mech->content_lacks('update good good bad good');
};
-
- $update->moderation_original_data->delete;
};
my $update2 = create_update();
@@ -515,4 +513,14 @@ subtest 'And do it as a superuser' => sub {
subtest 'Check moderation history in admin' => sub {
$mech->get_ok('/admin/report_edit/' . $report->id);
};
+
+subtest 'Check moderation in user log' => sub {
+ $mech->get_ok('/admin/users/' . $user->id . '/log');
+ my $report_id = $report->id;
+ $mech->content_like(qr/Moderated report.*?$report_id/);
+ my $update_id = $update->id;
+ $mech->content_like(qr/Moderated update.*?$update_id/);
+};
+
+
done_testing();
diff --git a/templates/web/base/admin/bodies/open311-form-fields.html b/templates/web/base/admin/bodies/open311-form-fields.html
index be2f13af0..dbb0f84e2 100644
--- a/templates/web/base/admin/bodies/open311-form-fields.html
+++ b/templates/web/base/admin/bodies/open311-form-fields.html
@@ -90,7 +90,7 @@
<label for"comment_user_id">[% loc('User ID to attribute fetched comments to') %]</label>
<input type="text" class="form-control" name="comment_user_id" value="[% object.comment_user_id %]">
[% IF object.comment_user_id %]
- <a href="[% c.uri_for_action('admin/users/edit', object.comment_user_id) %]">[% loc('edit user') %]</a>
+ <a href="[% c.uri_for_action('admin/users/edit', [ object.comment_user_id ]) %]">[% loc('edit user') %]</a>
[% END %]
</p>
diff --git a/templates/web/base/admin/list_updates.html b/templates/web/base/admin/list_updates.html
index b23cd7ca3..e8abdddad 100644
--- a/templates/web/base/admin/list_updates.html
+++ b/templates/web/base/admin/list_updates.html
@@ -41,7 +41,7 @@
</small></td>
<td rowspan=2>
[% IF c.user.has_permission_to('report_edit', update.problem.bodies_str_ids) %]
- <a href="[% c.uri_for( 'update_edit', update.id ) %]">[% loc('Edit') %]</a>
+ <a href="[% c.uri_for_action( 'admin/update_edit', [ update.id ] ) %]">[% loc('Edit') %]</a>
[% END %]
</td>
</tr>
diff --git a/templates/web/base/admin/problem_row.html b/templates/web/base/admin/problem_row.html
index 99142af4e..8d74fec13 100644
--- a/templates/web/base/admin/problem_row.html
+++ b/templates/web/base/admin/problem_row.html
@@ -23,7 +23,7 @@
[% PROCESS value_or_nbsp value=problem.category_display %]
<br>[%- IF edit_body_contacts -%]
[% FOR body IN problem.bodies.values %]
- <a href="[% c.uri_for('body', body.id ) %]">[% PROCESS value_or_nbsp value=body.name %]</a>
+ <a href="[% c.uri_for_action('admin/bodies/edit', [ body.id ] ) %]">[% PROCESS value_or_nbsp value=body.name %]</a>
[% END %]
[%- ELSE -%]
[%- PROCESS value_or_nbsp value=problem.bodies_str -%]
@@ -40,7 +40,7 @@
</small></td>
<td>
[% IF c.user.has_permission_to('report_edit', problem.bodies_str_ids) %]
- <a href="[% c.uri_for( 'report_edit', problem.id ) %]">[% loc('Edit') %]</a>
+ <a href="[% c.uri_for_action( '/admin/reports/edit', [ problem.id ] ) %]">[% loc('Edit') %]</a>
[% END %]
</td>
</tr>
diff --git a/templates/web/base/admin/report_edit.html b/templates/web/base/admin/reports/edit.html
index da800ff71..b4af705a9 100644
--- a/templates/web/base/admin/report_edit.html
+++ b/templates/web/base/admin/reports/edit.html
@@ -16,7 +16,7 @@
<div id="side">
-<form method="post" action="[% c.uri_for( 'report_edit', problem.id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
+<form method="post" action="[% c.uri_for_action( '/admin/reports/edit', [ problem.id ] ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
<input type="hidden" name="token" value="[% csrf_token %]" >
<input type="hidden" name="submit" value="1" >
@@ -57,7 +57,7 @@ class="admin-offsite-link">[% problem.latitude %], [% problem.longitude %]</a>
[% IF problem.bodies_str %]
[% FOREACH body IN problem.bodies.values %]
[% SET body_printed = 1 %]
- <a href="[% c.uri_for('body', body.id) %]">[% body.name | html %]</a>
+ <a href="[% c.uri_for_action('admin/bodies/edit', [ body.id ]) %]">[% body.name | html %]</a>
[%- ',' IF NOT loop.last %]
[% END %]
[% IF NOT body_printed %]
diff --git a/templates/web/base/admin/reports.html b/templates/web/base/admin/reports/index.html
index 7d8fe9561..adbd50224 100644
--- a/templates/web/base/admin/reports.html
+++ b/templates/web/base/admin/reports/index.html
@@ -1,7 +1,7 @@
[% INCLUDE 'admin/header.html' title=loc('Search Reports') %]
[% PROCESS 'admin/report_blocks.html' %]
-<form method="get" action="[% c.uri_for('reports') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
+<form method="get" action="[% c.uri_for_action('/admin/reports/index') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
<p><label for="search">[% loc('Search:') %]</label>
<input class="form-control" type="text" name="search" size="30" id="search" value="[% searched | html %]">
</form>
diff --git a/templates/web/base/admin/template_edit.html b/templates/web/base/admin/templates/edit.html
index 87218c7dd..7ce67f96f 100644
--- a/templates/web/base/admin/template_edit.html
+++ b/templates/web/base/admin/templates/edit.html
@@ -4,7 +4,7 @@
[% UNLESS rt.id %]<h3>[% loc('New template') %]</h3>[% END %]
<form method="post"
- action="[% c.uri_for('templates', body.id, rt.id || 'new' ) %]"
+ action="[% c.uri_for_action('/admin/templates/edit', body.id, rt.id || 'new' ) %]"
enctype="application/x-www-form-urlencoded"
accept-charset="utf-8"
class="validate">
diff --git a/templates/web/base/admin/templates_index.html b/templates/web/base/admin/templates/index.html
index 40e1be300..9322c6ef6 100644
--- a/templates/web/base/admin/templates_index.html
+++ b/templates/web/base/admin/templates/index.html
@@ -3,7 +3,7 @@
<ul>
[% FOR body IN bodies %]
<li>
- <a href="[% c.uri_for('templates', body.id) %]">[% body.name %]</a>
+ <a href="[% c.uri_for_action('/admin/templates/view', body.id) %]">[% body.name %]</a>
</li>
[% END %]
</ul>
diff --git a/templates/web/base/admin/templates.html b/templates/web/base/admin/templates/view.html
index 21e4eea25..6a1dd9614 100644
--- a/templates/web/base/admin/templates.html
+++ b/templates/web/base/admin/templates/view.html
@@ -28,12 +28,12 @@
[% IF t.external_status_code %][% t.external_status_code | html %] (external)[% END %]
</td>
<td> [% IF t.auto_response %]X[% END %] </td>
- <td> <a href="[% c.uri_for('templates', body.id, t.id) %]" class="btn">[% loc('Edit') %]</a> </td>
+ <td> <a href="[% c.uri_for_action('/admin/templates/edit', body.id, t.id) %]" class="btn">[% loc('Edit') %]</a> </td>
</tr>
[% END %]
</tbody>
</table>
-<a href="[% c.uri_for('templates', body.id, 'new') %]" class="btn">[% loc('New template') %]</a>
+<a href="[% c.uri_for_action('/admin/templates/index', body.id, 'new') %]" class="btn">[% loc('New template') %]</a>
[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/admin/users/form.html b/templates/web/base/admin/users/form.html
index f141dc02c..495da8648 100644
--- a/templates/web/base/admin/users/form.html
+++ b/templates/web/base/admin/users/form.html
@@ -1,4 +1,7 @@
-<form method="post" id="user_edit" action="[% c.uri_for_action( 'admin/users/edit', user.id || 'add' ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
+<form method="post" id="user_edit" action="[%
+ SET action_end = user.id || 'add';
+ c.uri_for_action( 'admin/users/edit', [ action_end ] )
+ %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
<input type="hidden" name="token" value="[% csrf_token %]" >
<input type="hidden" name="submit" value="1" >
diff --git a/templates/web/base/admin/users/import.html b/templates/web/base/admin/users/import.html
index 6e11c74a9..38e4fe240 100644
--- a/templates/web/base/admin/users/import.html
+++ b/templates/web/base/admin/users/import.html
@@ -27,7 +27,7 @@
[% FOREACH user IN new_users %]
<tr>
<td>
- <a href="[% c.uri_for_action( 'admin/users/edit', user.id ) %]">
+ <a href="[% c.uri_for_action( 'admin/users/edit', [ user.id ] ) %]">
[% user.name %]
</a>
</td>
@@ -50,7 +50,7 @@
[% FOREACH user IN existing_users %]
<tr>
<td>
- <a href="[% c.uri_for_action( 'admin/users/edit', user.id ) %]">
+ <a href="[% c.uri_for_action( 'admin/users/edit', [ user.id ] ) %]">
[% user.name %]
</a>
</td>
diff --git a/templates/web/base/admin/users/index.html b/templates/web/base/admin/users/index.html
index f48893cb0..e573c10fe 100644
--- a/templates/web/base/admin/users/index.html
+++ b/templates/web/base/admin/users/index.html
@@ -46,7 +46,7 @@
[% IF c.cobrand.moniker != 'zurich' %]
<th>[% loc('Flagged') %]</th>
[% END %]
- <th>*</th>
+ <th colspan="2">*</th>
</tr>
[%- FOREACH user IN users %]
<tr>
@@ -57,14 +57,15 @@
[% PROCESS value_or_nbsp value=user.name %]
[% IF user.from_body %]</label>[% END %]
</td>
- <td><a href="[% c.uri_for_action( 'admin/reports', search => user.email ) %]">[% PROCESS value_or_nbsp value=user.email %]</a></td>
+ <td><a href="[% c.uri_for_action( 'admin/reports/index', search => user.email ) %]">[% PROCESS value_or_nbsp value=user.email %]</a></td>
<td>[% PROCESS value_or_nbsp value=user.from_body.name %]
[% IF user.is_superuser %] * [% END %]
</td>
[% IF c.cobrand.moniker != 'zurich' %]
<td>[% user.flagged == 2 ? loc('User in abuse table') : user.flagged ? loc('Yes') : '&nbsp;' %]</td>
[% END %]
- <td>[% IF user.id %]<a href="[% c.uri_for_action( 'admin/users/edit', user.id ) %]">[% loc('Edit') %]</a>[% END %]</td>
+ <td>[% IF user.id %]<a href="[% c.uri_for_action( 'admin/users/edit', [ user.id ] ) %]">[% loc('Edit') %]</a>[% END %]</td>
+ <td>[% IF user.id %]<a href="[% c.uri_for_action( 'admin/users/log', [ user.id ] ) %]">[% loc('Timeline') %]</a>[% END %]</td>
</tr>
[%- END -%]
</table>
diff --git a/templates/web/base/admin/users/log.html b/templates/web/base/admin/users/log.html
new file mode 100644
index 000000000..a596d040c
--- /dev/null
+++ b/templates/web/base/admin/users/log.html
@@ -0,0 +1,72 @@
+[% INCLUDE 'admin/header.html' title=loc('Timeline') _ ', ' _ user.name %]
+
+<style>
+.timeline ul {
+ margin-bottom: 0;
+}
+.timeline dd {
+ margin-bottom: 0;
+}
+</style>
+
+[%
+action_map = {
+ add = 'Added'
+ delete = 'Deleted'
+ edit = 'Edited'
+ merge = 'Merged'
+ moderation = 'Moderated'
+ resend = 'Resent'
+ category_change = 'Changed category of'
+ state_change = 'Changed state of'
+}
+%]
+
+[%- date = '' %]
+[% FOREACH moment IN time.keys.sort.reverse %]
+ [%- curdate = time.$moment.0.date.strftime('%A, %e %B %Y') -%]
+ [%- IF date != curdate %]
+ [% '</dl>' IF date %]
+ <h2>[% curdate %]</h2>
+
+ <dl class="timeline">
+ [%- date = curdate -%]
+ [%- END -%]
+ <dt><b>[% time.$moment.0.date.hms %]</b></dt>
+ <dd><ul>
+ [% FOREACH item IN time.$moment %]
+ <li>
+ [%~ IF item.obj.problem_id %]
+ [%~ SET report_url = c.uri_for( '/report', item.obj.problem_id ) _ "#update_" _ item.obj.id %]
+ [%~ ELSE %]
+ [%~ SET report_url = c.uri_for('/report', item.obj.id) %]
+ [%~ END %]
+ [%~ SET report_link = "<a href='" _ report_url _ "'>" _ item.obj.id _ "</a>" %]
+ [%- SWITCH item.type -%]
+ [%~ CASE 'problem' %]
+ [%- tprintf(loc('Problem %s created'), report_link) %], ‘[% item.obj.title | html %]’
+ [%~ CASE 'problemContributedBy' %]
+ [%- tprintf(loc('Problem %s created on behalf of %s'), report_link, item.obj.name) %], ‘[% item.obj.title | html %]’
+ [%~ CASE 'update' %]
+ [% tprintf(loc("Update %s created for problem %d"), report_link, item.obj.problem_id) %]
+ [% item.obj.text | add_links | markup(item.obj.user) | html_para %]
+ [%~ CASE 'shortlistAdded' %]
+ [%- tprintf(loc('Problem %s added to shortlist'), report_link) %]
+ [%~ CASE 'shortlistRemoved' %]
+ [%- tprintf(loc('Problem %s removed from shortlist'), report_link) %]
+ [%~ CASE 'log' %]
+ [%~ SET object_summary = item.log.object_summary %]
+ [% IF object_summary %]
+ [%~ SET link = tprintf('<a href="%s">%s</a>', item.log.link, object_summary) %]
+ [%- tprintf('%s %s %s', action_map.${item.log.action}, item.log.actual_object_type, link) %]
+ [% ' – ' _ item.log.reason IF item.log.reason %]
+ [% ELSE %]
+ [%- tprintf('%s %s %s', action_map.${item.log.action}, item.log.actual_object_type, item.log.object_id) %]
+ [% END %]
+ [%- END %]
+ </li>
+ [%- END %]
+ </ul></dd>
+[% END %]
+
+[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/report/inspect/information.html b/templates/web/base/report/inspect/information.html
index cc8989522..b81b37543 100644
--- a/templates/web/base/report/inspect/information.html
+++ b/templates/web/base/report/inspect/information.html
@@ -7,7 +7,7 @@
<strong>[% loc('Report ID:') %]</strong>
<span class="js-report-id">[% problem.id %]</span>
[% IF c.user_exists AND c.cobrand.admin_allow_user(c.user) AND c.user.has_permission_to('report_edit', problem.bodies_str_ids) %]
- (<a href="[% c.uri_for_action( "admin/report_edit", problem.id ) %]">[% loc('admin') %]</a>)
+ (<a href="[% c.uri_for_action( 'admin/reports/edit', [ problem.id ] ) %]">[% loc('admin') %]</a>)
[% END %]
</p>
[% IF permissions.report_inspect AND problem.user.phone %]
diff --git a/templates/web/zurich/admin/problem_row.html b/templates/web/zurich/admin/problem_row.html
index 973d9f651..502a7bc39 100644
--- a/templates/web/zurich/admin/problem_row.html
+++ b/templates/web/zurich/admin/problem_row.html
@@ -46,7 +46,7 @@
</td>
[% IF NOT no_edit %]
- <td><a href="[% c.uri_for( 'report_edit', problem.id ) %]">[% loc('Edit') %]</a></td>
+ <td><a href="[% c.uri_for_action( 'admin/reports/edit', [ problem.id ] ) %]">[% loc('Edit') %]</a></td>
[% END %]
</tr>
[%- END -%]
diff --git a/templates/web/zurich/admin/report_edit-sdm.html b/templates/web/zurich/admin/report_edit-sdm.html
index d8e6c2625..8d25ab40a 100644
--- a/templates/web/zurich/admin/report_edit-sdm.html
+++ b/templates/web/zurich/admin/report_edit-sdm.html
@@ -12,7 +12,7 @@
<div id="map_sidebar">
-<form method="post" action="[% c.uri_for( 'report_edit', problem.id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
+<form method="post" action="[% c.uri_for_action( 'admin/reports/edit', [ problem.id ] ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
<input type="hidden" name="token" value="[% csrf_token %]" >
<input type="hidden" name="submit" value="1" >
diff --git a/templates/web/zurich/admin/report_edit.html b/templates/web/zurich/admin/reports/edit.html
index 6f69161fe..4b0490b4a 100644
--- a/templates/web/zurich/admin/report_edit.html
+++ b/templates/web/zurich/admin/reports/edit.html
@@ -14,7 +14,7 @@
[% pstate = problem.get_extra_metadata('closure_status') || problem.state %]
-<form id="report_edit" method="post" action="[% c.uri_for( 'report_edit', problem.id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
+<form id="report_edit" method="post" action="[% c.uri_for_action( 'admin/reports/edit', [ problem.id ] ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
<input type="hidden" name="token" value="[% csrf_token %]" >
<input type="hidden" name="submit" value="1" >
diff --git a/templates/web/zurich/admin/reports.html b/templates/web/zurich/admin/reports/index.html
index 7e425c8ec..481dfb49d 100644
--- a/templates/web/zurich/admin/reports.html
+++ b/templates/web/zurich/admin/reports/index.html
@@ -1,7 +1,7 @@
[% PROCESS 'admin/header.html' title=loc('Search Reports') %]
[% PROCESS 'admin/report_blocks.html' %]
-<form method="get" action="[% c.uri_for('reports') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
+<form method="get" action="[% c.uri_for_action('admin/reports/index') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
<p><label for="search">[% loc('Search:') %]</label> <input type="text" name="search" size="30" id="search" value="[% searched | html %]">
</form>
diff --git a/templates/web/zurich/admin/template_edit.html b/templates/web/zurich/admin/templates/edit.html
index b6f68106c..8a95f2a04 100644
--- a/templates/web/zurich/admin/template_edit.html
+++ b/templates/web/zurich/admin/templates/edit.html
@@ -11,7 +11,7 @@
</h3>
<form method="post"
- action="[% c.uri_for('templates', body.id, rt.id || 'new' ) %]"
+ action="[% c.uri_for_action('/admin/templates/edit', body.id, rt.id || 'new' ) %]"
enctype="application/x-www-form-urlencoded"
accept-charset="utf-8"
class="validate">
diff --git a/templates/web/zurich/admin/templates.html b/templates/web/zurich/admin/templates/view.html
index 2db9e2e34..7b95c8a99 100644
--- a/templates/web/zurich/admin/templates.html
+++ b/templates/web/zurich/admin/templates/view.html
@@ -17,12 +17,12 @@
<td> [% t.title %] </td>
<td> [% t.text %] </td>
<td> [% t.created %] </td>
- <td> <a href="[% c.uri_for('templates', body.id, t.id) %]" class="btn">[% loc('Edit') %]</a> </td>
+ <td> <a href="[% c.uri_for_action('/admin/templates/edit', body.id, t.id) %]" class="btn">[% loc('Edit') %]</a> </td>
</tr>
[% END %]
</tbody>
</table>
-<a href="[% c.uri_for('templates', body.id, 'new') %]" class="btn">[% loc('New template') %]</a>
+<a href="[% c.uri_for_action('/admin/templates/edit', body.id, 'new') %]" class="btn">[% loc('New template') %]</a>
[% INCLUDE 'admin/footer.html' %]