diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Users.pm | 32 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/AdminLog.pm | 75 | ||||
-rw-r--r-- | t/app/controller/admin/bodies.t | 7 | ||||
-rw-r--r-- | t/app/controller/admin/roles.t | 9 | ||||
-rw-r--r-- | t/app/controller/admin/templates.t | 9 | ||||
-rw-r--r-- | t/app/controller/admin/users.t | 22 | ||||
-rw-r--r-- | t/app/controller/moderate.t | 12 | ||||
-rw-r--r-- | templates/web/base/admin/users/index.html | 3 | ||||
-rw-r--r-- | templates/web/base/admin/users/log.html | 72 |
10 files changed, 237 insertions, 6 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b786b6c76..7650612cd 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/perllib/FixMyStreet/App/Controller/Admin/Users.pm b/perllib/FixMyStreet/App/Controller/Admin/Users.pm index ac9353921..802fbb9f5 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/Users.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/Users.pm @@ -406,6 +406,38 @@ sub edit : Chained('user') : PathPart('') : Args(0) { 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 ) = @_; 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/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/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 3bbd7bf85..39903deb1 100644 --- a/t/app/controller/admin/templates.t +++ b/t/app/controller/admin/templates.t @@ -61,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 { @@ -215,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/users/index.html b/templates/web/base/admin/users/index.html index 3b5c5bcc4..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> @@ -65,6 +65,7 @@ <td>[% user.flagged == 2 ? loc('User in abuse table') : user.flagged ? loc('Yes') : ' ' %]</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/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' %] |