aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet/App/Controller/Moderate.pm
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Moderate.pm')
-rw-r--r--perllib/FixMyStreet/App/Controller/Moderate.pm406
1 files changed, 233 insertions, 173 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm
index 86143b5ea..22869d531 100644
--- a/perllib/FixMyStreet/App/Controller/Moderate.pm
+++ b/perllib/FixMyStreet/App/Controller/Moderate.pm
@@ -23,9 +23,9 @@ data to change.
- user to be from_body
- user to have a "moderate" record in user_body_permissions
-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 admin_log.
+The original and previous 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 admin_log.
=head1 SEE ALSO
@@ -37,71 +37,155 @@ DB tables:
=cut
+sub end : ActionClass('RenderView') {
+ my ($self, $c) = @_;
+
+ if ($c->stash->{moderate_errors}) {
+ $c->stash->{show_moderation} = 'report';
+ $c->stash->{template} = 'report/display.html';
+ $c->forward('/report/display');
+ } elsif ($c->res->redirect) {
+ # Do nothing if we're already going somewhere
+ } else {
+ $c->res->redirect($c->stash->{report_uri});
+ }
+}
+
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);
+ $c->detach unless $problem;
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_ids);
$c->forward('/auth/check_csrf_token');
- my $original = $problem->find_or_new_related( moderation_original_data => {
+ $c->stash->{history} = $problem->new_related( moderation_original_data => {
title => $problem->title,
detail => $problem->detail,
photo => $problem->photo,
anonymous => $problem->anonymous,
+ longitude => $problem->longitude,
+ latitude => $problem->latitude,
+ category => $problem->category,
+ $problem->extra ? (extra => $problem->extra) : (),
});
+ $c->stash->{original} = $problem->moderation_original_data || $c->stash->{history};
$c->stash->{problem} = $problem;
- $c->stash->{problem_original} = $original;
$c->stash->{moderation_reason} = $c->get_param('moderation_reason') // '';
}
sub moderate_report : Chained('report') : PathPart('') : Args(0) {
my ($self, $c) = @_;
+ my $problem = $c->stash->{problem};
+
+ # Make sure user can moderate this report
+ $c->detach unless $c->user->can_moderate($problem);
+
+ $c->forward('check_edited_elsewhere');
$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->forward('moderate_state'),
+ ($c->user->can_moderate_title($problem, 1)
+ ? $c->forward('moderate_text', [ 'title' ])
+ : ()),
+ $c->forward('moderate_text', [ 'detail' ]),
+ $c->forward('moderate_boolean', [ 'anonymous', 'show_name' ]),
+ $c->forward('moderate_boolean', [ 'photo' ]),
+ $c->forward('moderate_location'),
+ $c->forward('moderate_category'),
+ $c->forward('moderate_extra');
+
+ # Deal with possible photo changes. If a moderate form uses a standard
+ # photo upload field (with upload_fileid, label and file upload handlers),
+ # this will allow photos to be changed, not just switched on/off. You will
+ # probably want a hidden field with problem_photo=1 to skip that check.
+ my $photo_edit_form = defined $c->get_param('photo1');
+ if ($photo_edit_form) {
+ $c->forward('/photo/process_photo');
+ if ( my $photo_error = delete $c->stash->{photo_error} ) {
+ $c->stash->{moderate_errors} ||= [];
+ push @{ $c->stash->{moderate_errors} }, $photo_error;
+ } else {
+ my $fileid = $c->stash->{upload_fileid};
+ if ($fileid ne $problem->photo) {
+ $problem->get_photoset->delete_cached;
+ $problem->photo($fileid || undef);
+ push @types, 'photo';
+ }
+ }
+ }
- $c->detach( 'report_moderate_audit', \@types )
+ $c->detach( 'report_moderate_audit', \@types );
}
-sub moderating_user_name {
- my $user = shift;
- return $user->from_body ? $user->from_body->name : _('an administrator');
+sub check_edited_elsewhere : Private {
+ my ($self, $c) = @_;
+
+ my $problem = $c->stash->{problem};
+ my $last_moderation = $problem->latest_moderation;
+ return unless $last_moderation;
+
+ my $form_started = $c->get_param('form_started') || 0;
+ if ($form_started && $form_started < $last_moderation->created->epoch) {
+ $c->stash->{moderate_errors} ||= [];
+ push @{$c->stash->{moderate_errors}},
+ _('Someone has moderated this report since you started.') . ' ' .
+ sprintf(_('Please <a href="#%s">check their changes</a> and resolve any differences.'),
+ 'update_m' . $last_moderation->id);
+ $c->detach;
+ }
}
-sub report_moderate_audit : Private {
- my ($self, $c, @types) = @_;
+sub moderate_log_entry : Private {
+ my ($self, $c, $object_type, @types) = @_;
my $user = $c->user->obj;
my $reason = $c->stash->{'moderation_reason'};
- my $problem = $c->stash->{problem} or die;
+ my $object = $object_type eq 'update' ? $c->stash->{comment} : $c->stash->{problem};
my $types_csv = join ', ' => @types;
+ my $log_reason = "($types_csv)";
+ $log_reason = "$reason $log_reason" if $reason;
+
+ # We attach the log to the moderation entry if present, or the object if not (hiding)
$c->model('DB::AdminLog')->create({
action => 'moderation',
user => $user,
- admin_user => moderating_user_name($user),
- object_id => $problem->id,
- object_type => 'problem',
- reason => (sprintf '%s (%s)', $reason, $types_csv),
+ admin_user => $user->moderating_user_name,
+ object_id => $c->stash->{history}->id || $object->id,
+ object_type => $c->stash->{history}->id ? 'moderation' : $object_type,
+ reason => $log_reason,
});
+}
+
+sub report_moderate_audit : Private {
+ my ($self, $c, @types) = @_;
+
+ my $problem = $c->stash->{problem} or die;
+
+ return unless @types; # If nothing moderated, nothing to do
+ return if $c->stash->{moderate_errors}; # Don't update anything if errors
+
+ # Okay, now update the report
+ $problem->update;
+
+ return if @types == 1 && $types[0] eq 'state'; # If only state changed, no log entry needed
+
+ # We've done some non-state moderation, save the history
+ $c->stash->{history}->insert;
+
+ $c->forward('moderate_log_entry', [ 'problem', @types ]);
if ($problem->user->email_verified && $c->cobrand->send_moderation_notifications) {
my $token = $c->model("DB::Token")->create({
@@ -109,6 +193,7 @@ sub report_moderate_audit : Private {
data => { id => $problem->id }
});
+ my $types_csv = join ', ' => @types;
$c->send_email( 'problem-moderated.txt', {
to => [ [ $problem->user->email, $problem->name ] ],
types => $types_csv,
@@ -116,6 +201,7 @@ sub report_moderate_audit : Private {
problem => $problem,
report_uri => $c->stash->{report_uri},
report_complain_uri => $c->stash->{cobrand_base} . '/contact?m=' . $token->token,
+ moderated_data => $c->stash->{history},
});
}
}
@@ -135,97 +221,153 @@ sub report_moderate_hide : Private {
}
}
-sub report_moderate_title : Private {
- my ( $self, $c ) = @_;
+sub moderate_text : Private {
+ my ($self, $c, $thing) = @_;
- my $problem = $c->stash->{problem} or die;
- my $original = $c->stash->{problem_original};
+ my $object = $c->stash->{comment} || $c->stash->{problem};
+ my $param = $c->stash->{comment} ? 'update_' : 'problem_';
- my $old_title = $problem->title;
- my $original_title = $original->title;
+ my $thing_for_original_table = $thing;
+ # Update 'text' field is stored in original table's 'detail' field
+ $thing_for_original_table = 'detail' if $c->stash->{comment} && $thing eq 'text';
- my $title = $c->get_param('problem_revert_title') ?
- $original_title
- : $c->get_param('problem_title');
+ my $old = $object->$thing;
+ my $original_thing = $c->stash->{original}->$thing_for_original_table;
- if ($title ne $old_title) {
- $original->insert unless $original->in_storage;
- $problem->update({ title => $title });
- return 'title';
- }
+ my $new = $c->get_param($param . 'revert_' . $thing) ?
+ $original_thing
+ : $c->get_param($param . $thing);
- return;
+ if ($new ne $old) {
+ $object->$thing($new);
+ return $thing_for_original_table;
+ }
}
-sub report_moderate_detail : Private {
- my ( $self, $c ) = @_;
+sub moderate_boolean : Private {
+ my ( $self, $c, $thing, $reverse ) = @_;
- 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->get_param('problem_revert_detail') ?
- $original_detail
- : $c->get_param('problem_detail');
-
- if ($detail ne $old_detail) {
- $original->insert unless $original->in_storage;
- $problem->update({ detail => $detail });
- return 'detail';
+ my $object = $c->stash->{comment} || $c->stash->{problem};
+ my $param = $c->stash->{comment} ? 'update_' : 'problem_';
+ my $original = $c->stash->{original}->photo;
+
+ return if $thing eq 'photo' && !$original;
+
+ my $new;
+ if ($reverse) {
+ $new = $c->get_param($param . $reverse) ? 0 : 1;
+ } else {
+ $new = $c->get_param($param . $thing) ? 1 : 0;
+ }
+ my $old = $object->$thing ? 1 : 0;
+
+ if ($new != $old) {
+ if ($thing eq 'photo') {
+ $object->$thing($new ? $original : undef);
+ $object->get_photoset->delete_cached;
+ } else {
+ $object->$thing($new);
+ }
+ return $thing;
}
- return;
}
-sub report_moderate_anon : Private {
- my ( $self, $c ) = @_;
+sub moderate_extra : Private {
+ my ($self, $c) = @_;
- my $problem = $c->stash->{problem} or die;
- my $original = $c->stash->{problem_original};
+ my $object = $c->stash->{comment} || $c->stash->{problem};
+
+ my $changed;
+ my @extra = grep { /^extra\./ } keys %{$c->req->params};
+ foreach (@extra) {
+ my ($field_name) = /extra\.(.*)/;
+ my $old = $object->get_extra_metadata($field_name) || '';
+ my $new = $c->get_param($_);
+ if ($new ne $old) {
+ $object->set_extra_metadata($field_name, $new);
+ $changed = 1;
+ }
+ }
+ if ($changed) {
+ return 'extra';
+ }
+}
- my $show_user = $c->get_param('problem_show_name') ? 1 : 0;
- my $anonymous = $show_user ? 0 : 1;
- my $old_anonymous = $problem->anonymous ? 1 : 0;
+sub moderate_location : Private {
+ my ($self, $c) = @_;
- if ($anonymous != $old_anonymous) {
+ my $problem = $c->stash->{problem};
- $original->insert unless $original->in_storage;
- $problem->update({ anonymous => $anonymous });
- return 'anonymous';
+ my $moved = $c->forward('/admin/report_edit_location', [ $problem ]);
+ if (!$moved) {
+ # New lat/lon isn't valid, show an error
+ $c->stash->{moderate_errors} ||= [];
+ push @{ $c->stash->{moderate_errors} }, _('Invalid location. New location must be covered by the same council.');
+ } elsif ($moved == 2) {
+ return 'location';
}
- return;
}
-sub report_moderate_photo : Private {
- my ( $self, $c ) = @_;
+# No update left at present
+sub moderate_category : Private {
+ my ($self, $c) = @_;
- my $problem = $c->stash->{problem} or die;
- my $original = $c->stash->{problem_original};
+ return unless $c->get_param('category');
- return unless $original->photo;
+ # The admin category editing needs to know all the categories etc
+ $c->forward('/admin/categories_for_point');
- my $show_photo = $c->get_param('problem_show_photo') ? 1 : 0;
- my $old_show_photo = $problem->photo ? 1 : 0;
+ my $problem = $c->stash->{problem};
- if ($show_photo != $old_show_photo) {
- $original->insert unless $original->in_storage;
- $problem->update({ photo => $show_photo ? $original->photo : undef });
- return 'photo';
+ my $changed = $c->forward( '/admin/report_edit_category', [ $problem, 1 ] );
+ # It might need to set_report_extras in future
+ if ($changed) {
+ return 'category';
+ }
+}
+
+# Note that if a cobrand allows state moderation, then the moderation reason
+# given will be added as an update and thus be publicly available (unlike with
+# normal moderation).
+sub moderate_state : Private {
+ my ($self, $c) = @_;
+
+ my $new_state = $c->get_param('state');
+ return unless $new_state;
+
+ my $problem = $c->stash->{problem};
+ if ($problem->state ne $new_state) {
+ $problem->state($new_state);
+ $problem->add_to_comments( {
+ text => $c->stash->{moderation_reason},
+ 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 => $c->user->from_body ? 0 : 1,
+ problem_state => $new_state,
+ } );
+ return 'state';
}
- 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 => {
+ # Make sure user can moderate this update
+ $c->detach unless $comment && $c->user->can_moderate($comment);
+
+ $c->stash->{history} = $comment->new_related( moderation_original_data => {
detail => $comment->text,
photo => $comment->photo,
anonymous => $comment->anonymous,
+ $comment->extra ? (extra => $comment->extra) : (),
});
$c->stash->{comment} = $comment;
- $c->stash->{comment_original} = $original;
+ $c->stash->{original} = $comment->moderation_original_data || $c->stash->{history};
}
sub moderate_update : Chained('update') : PathPart('') : Args(0) {
@@ -234,31 +376,16 @@ sub moderate_update : Chained('update') : PathPart('') : Args(0) {
$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 => moderating_user_name($user),
- object_id => $comment->id,
- object_type => 'update',
- reason => (sprintf '%s (%s)', $reason, $types_csv),
- });
+ $c->forward('moderate_text', [ 'text' ]),
+ $c->forward('moderate_boolean', [ 'anonymous', 'show_name' ]),
+ $c->forward('moderate_extra'),
+ $c->forward('moderate_boolean', [ 'photo' ]);
+
+ if (@types) {
+ $c->stash->{history}->insert;
+ $c->stash->{comment}->update;
+ $c->detach('moderate_log_entry', [ 'update', @types ]);
+ }
}
sub update_moderate_hide : Private {
@@ -269,77 +396,10 @@ sub update_moderate_hide : Private {
if ($c->get_param('update_hide')) {
$comment->hide;
- $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->get_param('update_revert_detail') ?
- $original_detail
- : $c->get_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->get_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->get_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';
+ $c->detach('moderate_log_entry', [ 'update', 'hide' ]); # break chain here.
}
}
-sub return_text : Private {
- my ($self, $c, $text) = @_;
-
- $c->res->content_type('text/plain; charset=utf-8');
- $c->res->body( $text // '' );
-}
-
__PACKAGE__->meta->make_immutable;
1;