diff options
author | Hakim Cassimally <hakim@mysociety.org> | 2014-05-27 16:08:50 +0000 |
---|---|---|
committer | Hakim Cassimally <hakim@mysociety.org> | 2014-08-13 15:11:29 +0000 |
commit | 382a782c16a998776176de37037dd029fab4e3fe (patch) | |
tree | c425b24e3ce2c396214fd463e0fc0bde21f792be /perllib/FixMyStreet/App/Controller/Moderate.pm | |
parent | 68a486f190aa5054b3bcf1be2a63ad0c7c6aef26 (diff) |
Report moderation
- redaction marked with [...]
- of report and comments
- stores original data
- uses a single form, on the report/_main view
- requires additional permissions (user_body_permissions)
- Hide report functionality
- Moderation notification/contact form
- Moderation writes to admin_log
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Moderate.pm')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Moderate.pm | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm new file mode 100644 index 000000000..bf70d52c5 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Moderate.pm @@ -0,0 +1,365 @@ +package FixMyStreet::App::Controller::Moderate; + +use Moose; +use namespace::autoclean; +use Algorithm::Diff; +BEGIN { extends 'Catalyst::Controller'; } + +=head1 NAME + +FixMyStreet::App::Controller::Moderate - process a moderation event + +=head1 DESCRIPTION + +The intent of this is that council users will be able to moderate reports +by themselves, but without requiring access to the full admin panel. + +From a given report page, an authenticated user will be able to press +the "moderate" button on report and any updates to bring up a form with +data to change. + +(Authentication requires: + + - user to be from_body + - user to have a "moderate" record in user_body_permissions (there is + currently no admin interface for this. Should be added, but + while we're trialing this, it's a simple case of adding a DB record + manually) + +The original data of the report is stored in moderation_original_data, so +that it can be reverted/consulted if required. All moderation events are +stored in moderation_log. (NB: In future, this could be combined with +admin_log). + +=head1 SEE ALSO + +DB tables: + + AdminLog + ModerationOriginalData + UserBodyPermissions + +=cut + +sub moderate : Chained('/') : PathPart('moderate') : CaptureArgs(0) { } + +sub report : Chained('moderate') : PathPart('report') : CaptureArgs(1) { + my ($self, $c, $id) = @_; + my $problem = $c->model('DB::Problem')->find($id); + + my $cobrand_base = $c->cobrand->base_url_for_report( $problem ); + my $report_uri = $cobrand_base . $problem->url; + $c->stash->{cobrand_base} = $cobrand_base; + $c->stash->{report_uri} = $report_uri; + $c->res->redirect( $report_uri ); # this will be the final endpoint after all processing... + + # ... and immediately, if the user isn't authorized + $c->detach unless $c->user_exists; + $c->detach unless $c->user->has_permission_to(moderate => $problem->bodies_str); + + my $original = $problem->find_or_new_related( moderation_original_data => { + title => $problem->title, + detail => $problem->detail, + photo => $problem->photo, + anonymous => $problem->anonymous, + }); + $c->stash->{problem} = $problem; + $c->stash->{problem_original} = $original; + $c->stash->{moderation_reason} = $c->req->param('moderation_reason') // ''; +} + +sub moderate_report : Chained('report') : PathPart('') : Args(0) { + my ($self, $c) = @_; + + $c->forward('report_moderate_hide'); + + my @types = grep $_, + $c->forward('report_moderate_title'), + $c->forward('report_moderate_detail'), + $c->forward('report_moderate_anon'), + $c->forward('report_moderate_photo'); + + $c->detach( 'report_moderate_audit', \@types ) +} + +sub report_moderate_audit : Private { + my ($self, $c, @types) = @_; + + my $user = $c->user->obj; + my $reason = $c->stash->{'moderation_reason'}; + my $problem = $c->stash->{problem} or die; + + my $types_csv = join ', ' => @types; + + $c->model('DB::AdminLog')->create({ + action => 'moderation', + user => $user, + admin_user => $user->name, + object_id => $problem->id, + object_type => 'problem', + reason => (sprintf '%s (%s)', $reason, $types_csv), + }); + + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($problem->cobrand)->new(); + + my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL'); + my $sender_name = _($cobrand->contact_name); + + $c->send_email( 'problem-moderated.txt', { + + to => [ [ $user->email, $user->name ] ], + from => [ $sender, $sender_name ], + types => $types_csv, + user => $user, + problem => $problem, + report_uri => $c->stash->{report_uri}, + report_complain_uri => $c->stash->{cobrand_base} . '/contact?m=1&id=' . $problem->id, + }); +} + +sub report_moderate_hide : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + + if ($c->req->param('problem_hide')) { + + + $problem->update({ state => 'hidden' }); + + $c->detach( 'report_moderate_audit', ['hide'] ); # break chain here. + } +} + +sub report_moderate_title : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $original = $c->stash->{problem_original}; + + my $old_title = $problem->title; + my $original_title = $original->title; + + my $title = $c->req->param('problem_revert_title') ? + $original_title + : $self->diff($original_title, $c->req->param('problem_title')); + + if ($title ne $old_title) { + $original->insert unless $original->in_storage; + $problem->update({ title => $title }); + return 'title'; + } + + return; +} + +sub report_moderate_detail : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $original = $c->stash->{problem_original}; + + my $old_detail = $problem->detail; + my $original_detail = $original->detail; + my $detail = $c->req->param('problem_revert_detail') ? + $original_detail + : $self->diff($original_detail, $c->req->param('problem_detail')); + + if ($detail ne $old_detail) { + $original->insert unless $original->in_storage; + $problem->update({ detail => $detail }); + return 'detail'; + } + return; +} + +sub report_moderate_anon : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $original = $c->stash->{problem_original}; + + my $show_user = $c->req->param('problem_show_name') ? 1 : 0; + my $anonymous = $show_user ? 0 : 1; + my $old_anonymous = $problem->anonymous ? 1 : 0; + + if ($anonymous != $old_anonymous) { + + $original->insert unless $original->in_storage; + $problem->update({ anonymous => $anonymous }); + return 'anonymous'; + } + return; +} + +sub report_moderate_photo : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $original = $c->stash->{problem_original}; + + return unless $original->photo; + + my $show_photo = $c->req->param('problem_show_photo') ? 1 : 0; + my $old_show_photo = $problem->photo ? 1 : 0; + + if ($show_photo != $old_show_photo) { + $original->insert unless $original->in_storage; + $problem->update({ photo => $show_photo ? $original->photo : undef }); + return 'photo'; + } + return; +} + +sub update : Chained('report') : PathPart('update') : CaptureArgs(1) { + my ($self, $c, $id) = @_; + my $comment = $c->stash->{problem}->comments->find($id); + + my $original = $comment->find_or_new_related( moderation_original_data => { + detail => $comment->text, + photo => $comment->photo, + anonymous => $comment->anonymous, + }); + $c->stash->{comment} = $comment; + $c->stash->{comment_original} = $original; +} + +sub moderate_update : Chained('update') : PathPart('') : Args(0) { + my ($self, $c) = @_; + + $c->forward('update_moderate_hide'); + + my @types = grep $_, + $c->forward('update_moderate_detail'), + $c->forward('update_moderate_anon'), + $c->forward('update_moderate_photo'); + + $c->detach( 'update_moderate_audit', \@types ) +} + +sub update_moderate_audit : Private { + my ($self, $c, @types) = @_; + + my $user = $c->user->obj; + my $reason = $c->stash->{'moderation_reason'}; + my $problem = $c->stash->{problem} or die; + my $comment = $c->stash->{comment} or die; + + my $types_csv = join ', ' => @types; + + $c->model('DB::AdminLog')->create({ + action => 'moderation', + user => $user, + admin_user => $user->name, + object_id => $comment->id, + object_type => 'update', + reason => (sprintf '%s (%s)', $reason, $types_csv), + }); +} + +sub update_moderate_hide : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $comment = $c->stash->{comment} or die; + + if ($c->req->param('update_hide')) { + $comment->update({ state => 'hidden' }); + $c->detach( 'update_moderate_audit', ['hide'] ); # break chain here. + } + return; +} + +sub update_moderate_detail : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $comment = $c->stash->{comment} or die; + my $original = $c->stash->{comment_original}; + + my $old_detail = $comment->text; + my $original_detail = $original->detail; + my $detail = $c->req->param('update_revert_detail') ? + $original_detail + : $self->diff($original_detail, $c->req->param('update_detail')); + + if ($detail ne $old_detail) { + $original->insert unless $original->in_storage; + $comment->update({ text => $detail }); + return 'detail'; + } + return; +} + +sub update_moderate_anon : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $comment = $c->stash->{comment} or die; + my $original = $c->stash->{comment_original}; + + my $show_user = $c->req->param('update_show_name') ? 1 : 0; + my $anonymous = $show_user ? 0 : 1; + my $old_anonymous = $comment->anonymous ? 1 : 0; + + if ($anonymous != $old_anonymous) { + $original->insert unless $original->in_storage; + $comment->update({ anonymous => $anonymous }); + return 'anonymous'; + } + return; +} + +sub update_moderate_photo : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem} or die; + my $comment = $c->stash->{comment} or die; + my $original = $c->stash->{comment_original}; + + return unless $original->photo; + + my $show_photo = $c->req->param('update_show_photo') ? 1 : 0; + my $old_show_photo = $comment->photo ? 1 : 0; + + if ($show_photo != $old_show_photo) { + $original->insert unless $original->in_storage; + $comment->update({ photo => $show_photo ? $original->photo : undef }); + return 'photo'; + } +} + +sub return_text : Private { + my ($self, $c, $text) = @_; + + $c->res->content_type('text/plain; charset=utf-8'); + $c->res->body( $text // '' ); +} + +sub diff { + my ($self, $old, $new) = @_; + + $new =~s/\[\.{3}\]//g; + + my $diff = Algorithm::Diff->new( [ split //, $old ], [ split //, $new ] ); + my $string; + while ($diff->Next) { + my $d = $diff->Diff; + if ($d & 1) { + my $deleted = join '', $diff->Items(1); + unless ($deleted =~/^\s*$/) { + $string .= ' ' if $deleted =~/^ /; + my $letters = ($deleted=~s/\W//r); + $string .= '[...]'; + $string .= ' ' if $deleted =~/ $/; + } + } + $string .= join '', $diff->Items(2); + } + return $string; +} + + +__PACKAGE__->meta->make_immutable; + +1; |