aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm2
-rw-r--r--perllib/FixMyStreet/App/Controller/Moderate.pm120
-rw-r--r--perllib/FixMyStreet/DB/Result/Comment.pm42
-rw-r--r--perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm131
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm21
-rw-r--r--perllib/FixMyStreet/ImageMagick.pm1
-rw-r--r--perllib/FixMyStreet/Roles/Moderation.pm41
7 files changed, 241 insertions, 117 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index 60dea4c1b..75f83c6b6 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -889,7 +889,7 @@ sub report_edit : Path('report_edit') : Args(1) {
$c->stash->{updates} =
[ $c->model('DB::Comment')
- ->search( { problem_id => $problem->id }, { order_by => 'created' } )
+ ->search( { problem_id => $problem->id }, { order_by => [ 'created', 'id' ] } )
->all ];
if (my $rotate_photo_param = $self->_get_rotate_photo_param($c)) {
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm
index 90396fa07..154f09176 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
@@ -55,7 +55,7 @@ sub report : Chained('moderate') : PathPart('report') : CaptureArgs(1) {
$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,
@@ -63,10 +63,10 @@ sub report : Chained('moderate') : PathPart('report') : CaptureArgs(1) {
longitude => $problem->longitude,
latitude => $problem->latitude,
category => $problem->category,
- extra => $problem->extra,
+ $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') // '';
}
@@ -119,23 +119,32 @@ sub moderating_user_name {
return $user->from_body ? $user->from_body->name : _('an administrator');
}
-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;
+ # 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',
+ object_id => $c->stash->{history}->id || $object->id,
+ object_type => $c->stash->{history}->id ? 'moderation' : $object_type,
reason => (sprintf '%s (%s)', $reason, $types_csv),
});
+}
+
+sub report_moderate_audit : Private {
+ my ($self, $c, @types) = @_;
+
+ my $problem = $c->stash->{problem} or die;
+
+ $c->forward('moderate_log_entry', [ 'problem', @types ]);
if ($problem->user->email_verified && $c->cobrand->send_moderation_notifications) {
my $token = $c->model("DB::Token")->create({
@@ -143,6 +152,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,
@@ -150,6 +160,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},
});
}
}
@@ -172,29 +183,22 @@ sub report_moderate_hide : Private {
sub moderate_text : Private {
my ($self, $c, $thing) = @_;
- my ($object, $original, $param);
+ my $object = $c->stash->{comment} || $c->stash->{problem};
+ my $param = $c->stash->{comment} ? 'update_' : 'problem_';
+
my $thing_for_original_table = $thing;
- if (my $comment = $c->stash->{comment}) {
- $object = $comment;
- $original = $c->stash->{comment_original};
- $param = 'update_';
- # Update 'text' field is stored in original table's 'detail' field
- $thing_for_original_table = 'detail' if $thing eq 'text';
- } else {
- $object = $c->stash->{problem};
- $original = $c->stash->{problem_original};
- $param = 'problem_';
- }
+ # Update 'text' field is stored in original table's 'detail' field
+ $thing_for_original_table = 'detail' if $c->stash->{comment} && $thing eq 'text';
my $old = $object->$thing;
- my $original_thing = $original->$thing_for_original_table;
+ my $original_thing = $c->stash->{original}->$thing_for_original_table;
my $new = $c->get_param($param . 'revert_' . $thing) ?
$original_thing
: $c->get_param($param . $thing);
if ($new ne $old) {
- $original->insert unless $original->in_storage;
+ $c->stash->{history}->insert;
$object->update({ $thing => $new });
return $thing_for_original_table;
}
@@ -205,18 +209,11 @@ sub moderate_text : Private {
sub moderate_boolean : Private {
my ( $self, $c, $thing, $reverse ) = @_;
- my ($object, $original, $param);
- if (my $comment = $c->stash->{comment}) {
- $object = $comment;
- $original = $c->stash->{comment_original};
- $param = 'update_';
- } else {
- $object = $c->stash->{problem};
- $original = $c->stash->{problem_original};
- $param = 'problem_';
- }
+ 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->photo;
+ return if $thing eq 'photo' && !$original;
my $new;
if ($reverse) {
@@ -227,9 +224,9 @@ sub moderate_boolean : Private {
my $old = $object->$thing ? 1 : 0;
if ($new != $old) {
- $original->insert unless $original->in_storage;
+ $c->stash->{history}->insert;
if ($thing eq 'photo') {
- $object->update({ $thing => $new ? $original->photo : undef });
+ $object->update({ $thing => $new ? $original : undef });
} else {
$object->update({ $thing => $new });
}
@@ -241,14 +238,7 @@ sub moderate_boolean : Private {
sub moderate_extra : Private {
my ($self, $c) = @_;
- my ($object, $original);
- if (my $comment = $c->stash->{comment}) {
- $object = $comment;
- $original = $c->stash->{comment_original};
- } else {
- $object = $c->stash->{problem};
- $original = $c->stash->{problem_original};
- }
+ my $object = $c->stash->{comment} || $c->stash->{problem};
my $changed;
my @extra = grep { /^extra\./ } keys %{$c->req->params};
@@ -257,12 +247,12 @@ sub moderate_extra : Private {
my $old = $object->get_extra_metadata($field_name) || '';
my $new = $c->get_param($_);
if ($new ne $old) {
- $original->insert unless $original->in_storage;
$object->set_extra_metadata($field_name, $new);
$changed = 1;
}
}
if ($changed) {
+ $c->stash->{history}->insert;
$object->update;
return 'extra';
}
@@ -272,7 +262,6 @@ sub moderate_location : Private {
my ($self, $c) = @_;
my $problem = $c->stash->{problem};
- my $original = $c->stash->{problem_original};
my $moved = $c->forward('/admin/report_edit_location', [ $problem ]);
if (!$moved) {
@@ -283,7 +272,7 @@ sub moderate_location : Private {
}
if ($moved == 2) {
- $original->insert unless $original->in_storage;
+ $c->stash->{history}->insert;
$problem->update;
return 'location';
}
@@ -299,12 +288,11 @@ sub moderate_category : Private {
$c->forward('/admin/categories_for_point');
my $problem = $c->stash->{problem};
- my $original = $c->stash->{problem_original};
my $changed = $c->forward( '/admin/report_edit_category', [ $problem, 1 ] );
# It might need to set_report_extras in future
if ($changed) {
- $original->insert unless $original->in_storage;
+ $c->stash->{history}->insert;
$problem->update;
return 'category';
}
@@ -317,14 +305,14 @@ sub update : Chained('report') : PathPart('update') : CaptureArgs(1) {
# Make sure user can moderate this update
$c->detach unless $comment && $c->user->can_moderate($comment);
- my $original = $comment->find_or_new_related( moderation_original_data => {
+ $c->stash->{history} = $comment->new_related( moderation_original_data => {
detail => $comment->text,
photo => $comment->photo,
anonymous => $comment->anonymous,
- extra => $comment->extra,
+ $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) {
@@ -338,27 +326,7 @@ sub moderate_update : Chained('update') : PathPart('') : Args(0) {
$c->forward('moderate_extra'),
$c->forward('moderate_boolean', [ '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->detach('moderate_log_entry', [ 'update', @types ]);
}
sub update_moderate_hide : Private {
@@ -369,7 +337,7 @@ sub update_moderate_hide : Private {
if ($c->get_param('update_hide')) {
$comment->hide;
- $c->detach( 'update_moderate_audit', ['hide'] ); # break chain here.
+ $c->detach('moderate_log_entry', [ 'update', 'hide' ]); # break chain here.
}
return;
}
diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm
index 738b58f82..31e9f3e63 100644
--- a/perllib/FixMyStreet/DB/Result/Comment.pm
+++ b/perllib/FixMyStreet/DB/Result/Comment.pm
@@ -6,7 +6,6 @@ package FixMyStreet::DB::Result::Comment;
use strict;
use warnings;
-use FixMyStreet::Template;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
@@ -70,8 +69,8 @@ __PACKAGE__->add_columns(
{ data_type => "timestamp", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
-__PACKAGE__->might_have(
- "moderation_original_data",
+__PACKAGE__->has_many(
+ "moderation_original_datas",
"FixMyStreet::DB::Result::ModerationOriginalData",
{ "foreign.comment_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
@@ -90,8 +89,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZR+YNA1Jej3s+8mr52iq6Q
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-11-20 16:13:59
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:5w/4Og9uCy54lGyyJiLzxA
#
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
@@ -99,9 +98,11 @@ __PACKAGE__->rabx_column('extra');
use Moo;
use namespace::clean -except => [ 'meta' ];
+use FixMyStreet::Template;
with 'FixMyStreet::Roles::Abuser',
'FixMyStreet::Roles::Extra',
+ 'FixMyStreet::Roles::Moderation',
'FixMyStreet::Roles::PhotoSet';
my $stz = sub {
@@ -176,17 +177,6 @@ sub url {
return "/report/" . $self->problem_id . '#update_' . $self->id;
}
-=head2 latest_moderation_log_entry
-
-Return most recent ModerationLog object
-
-=cut
-
-sub latest_moderation_log_entry {
- my $self = shift;
- return $self->admin_log_entries->search({ action => 'moderation' }, { order_by => { -desc => 'id' } })->first;
-}
-
__PACKAGE__->has_many(
"admin_log_entries",
"FixMyStreet::DB::Result::AdminLog",
@@ -197,24 +187,24 @@ __PACKAGE__->has_many(
}
);
-# we already had the `moderation_original_data` rel above, as inferred by
-# Schema::Loader, but that doesn't know about the problem_id mapping, so we now
-# (slightly hackishly) redefine here:
-#
-# we also add cascade_delete, though this seems to be insufficient.
-#
-# TODO: should add FK on moderation_original_data field for this, to get S::L to
-# pick up without hacks.
-
+# This will return the oldest moderation_original_data, if any.
+# The plural can be used to return all entries.
__PACKAGE__->might_have(
"moderation_original_data",
"FixMyStreet::DB::Result::ModerationOriginalData",
{ "foreign.comment_id" => "self.id",
"foreign.problem_id" => "self.problem_id",
},
- { cascade_copy => 0, cascade_delete => 1 },
+ { order_by => 'id',
+ rows => 1,
+ cascade_copy => 0, cascade_delete => 1 },
);
+sub moderation_filter {
+ my $self = shift;
+ { problem_id => $self->problem_id };
+}
+
=head2 meta_line
Returns a string to be used on a report update, describing some of the metadata
diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
index 3e6482658..01ae1d6e1 100644
--- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
+++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
@@ -47,7 +47,6 @@ __PACKAGE__->add_columns(
{ data_type => "double precision", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
-__PACKAGE__->add_unique_constraint("moderation_original_data_comment_id_key", ["comment_id"]);
__PACKAGE__->belongs_to(
"comment",
"FixMyStreet::DB::Result::Comment",
@@ -67,14 +66,140 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-11-13 10:48:41
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OQAXPriTc3G2jKFPw0TqdQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-11-20 16:13:59
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zFOhQnS4WfVzD7qHxaAr6w
use Moo;
+use Text::Diff;
+use Data::Dumper;
with 'FixMyStreet::Roles::Extra';
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
+sub admin_log {
+ my $self = shift;
+ my $rs = $self->result_source->schema->resultset("AdminLog");
+ my $log = $rs->search({
+ object_id => $self->id,
+ object_type => 'moderation',
+ })->first;
+ return $log;
+}
+
+sub compare_with {
+ my ($self, $other) = @_;
+ if ($self->comment_id) {
+ my $new_detail = $other->can('text') ? $other->text : $other->detail;
+ return {
+ detail => string_diff($self->detail, $new_detail),
+ photo => $self->compare_photo($other),
+ anonymous => $self->compare_anonymous($other),
+ extra => $self->compare_extra($other),
+ };
+ }
+ return {
+ title => string_diff($self->title, $other->title),
+ detail => string_diff($self->detail, $other->detail),
+ photo => $self->compare_photo($other),
+ anonymous => $self->compare_anonymous($other),
+ coords => $self->compare_coords($other),
+ category => string_diff($self->category, $other->category, single => 1),
+ extra => $self->compare_extra($other),
+ }
+}
+
+sub compare_anonymous {
+ my ($self, $other) = @_;
+ string_diff(
+ $self->anonymous ? _('Yes') : _('No'),
+ $other->anonymous ? _('Yes') : _('No'),
+ );
+}
+
+sub compare_coords {
+ my ($self, $other) = @_;
+ my $old = join ',', $self->latitude, $self->longitude;
+ my $new = join ',', $other->latitude, $other->longitude;
+ string_diff($old, $new, single => 1);
+}
+
+sub compare_photo {
+ my ($self, $other) = @_;
+
+ my $old = $self->photo || '';
+ my $new = $other->photo || '';
+ return '' if $old eq $new;
+
+ $old = [ split /,/, $old ];
+ $new = [ split /,/, $new ];
+
+ my $diff = Algorithm::Diff->new( $old, $new );
+ my (@added, @deleted);
+ while ( $diff->Next ) {
+ next if $diff->Same;
+ push @deleted, $diff->Items(1);
+ push @added, $diff->Items(2);
+ }
+ return (join ', ', map {
+ "<del style='background-color:#fcc'>$_</del>";
+ } @deleted) . (join ', ', map {
+ "<ins style='background-color:#cfc'>$_</ins>";
+ } @added);
+}
+
+sub compare_extra {
+ my ($self, $other) = @_;
+
+ my $old = $self->get_extra_metadata;
+ my $new = $other->get_extra_metadata;
+
+ my $both = { %$old, %$new };
+ my @all_keys = sort keys %$both;
+ my @s;
+ foreach (@all_keys) {
+ if ($old->{$_} && $new->{$_}) {
+ push @s, string_diff("$_ = $old->{$_}", "$_ = $new->{$_}");
+ } elsif ($new->{$_}) {
+ push @s, string_diff("", "$_ = $new->{$_}");
+ } else {
+ push @s, string_diff("$_ = $old->{$_}", "");
+ }
+ }
+ return join ', ', @s;
+}
+
+sub string_diff {
+ my ($old, $new, %options) = @_;
+
+ return '' if $old eq $new;
+
+ $old = FixMyStreet::Template::html_filter($old);
+ $new = FixMyStreet::Template::html_filter($new);
+
+ if ($options{single}) {
+ $old = [ $old ];
+ $new = [ $new ];
+ }
+ $old = [ split //, $old ] unless ref $old;
+ $new = [ split //, $new ] unless ref $new;
+ my $diff = Algorithm::Diff->new( $old, $new );
+ my $string;
+ while ($diff->Next) {
+ my $d = $diff->Diff;
+ if ($d & 1) {
+ my $deleted = join '', $diff->Items(1);
+ $string .= "<del style='background-color:#fcc'>$deleted</del>";
+ }
+ my $inserted = join '', $diff->Items(2);
+ if ($d & 2) {
+ $string .= "<ins style='background-color:#cfc'>$inserted</ins>";
+ } else {
+ $string .= $inserted;
+ }
+ }
+ return $string;
+}
+
1;
diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm
index 0b8bc4de0..a222ea1f6 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -177,11 +177,15 @@ __PACKAGE__->has_one(
{ cascade_copy => 0, cascade_delete => 0 },
);
+# This will return the oldest moderation_original_data, if any.
+# The plural can be used to return all entries.
__PACKAGE__->might_have(
"moderation_original_data",
"FixMyStreet::DB::Result::ModerationOriginalData",
{ "foreign.problem_id" => "self.id" },
{ where => { 'comment_id' => undef },
+ order_by => 'id',
+ rows => 1,
cascade_copy => 0, cascade_delete => 1 },
);
@@ -206,6 +210,7 @@ my $IM = eval {
with 'FixMyStreet::Roles::Abuser',
'FixMyStreet::Roles::Extra',
+ 'FixMyStreet::Roles::Moderation',
'FixMyStreet::Roles::Translatable',
'FixMyStreet::Roles::PhotoSet';
@@ -958,17 +963,6 @@ sub as_hashref {
return $out;
}
-=head2 latest_moderation_log_entry
-
-Return most recent ModerationLog object
-
-=cut
-
-sub latest_moderation_log_entry {
- my $self = shift;
- return $self->admin_log_entries->search({ action => 'moderation' }, { order_by => { -desc => 'id' } })->first;
-}
-
__PACKAGE__->has_many(
"admin_log_entries",
"FixMyStreet::DB::Result::AdminLog",
@@ -979,6 +973,11 @@ __PACKAGE__->has_many(
}
);
+sub moderation_filter {
+ my $self = shift;
+ { comment_id => undef };
+}
+
sub get_time_spent {
my $self = shift;
my $admin_logs = $self->admin_log_entries->search({},
diff --git a/perllib/FixMyStreet/ImageMagick.pm b/perllib/FixMyStreet/ImageMagick.pm
index 26c5c6d74..af9f56478 100644
--- a/perllib/FixMyStreet/ImageMagick.pm
+++ b/perllib/FixMyStreet/ImageMagick.pm
@@ -3,6 +3,7 @@ package FixMyStreet::ImageMagick;
use Moo;
my $IM = eval {
+ return 0 if FixMyStreet->test_mode;
require Image::Magick;
Image::Magick->import;
1;
diff --git a/perllib/FixMyStreet/Roles/Moderation.pm b/perllib/FixMyStreet/Roles/Moderation.pm
new file mode 100644
index 000000000..f43b65208
--- /dev/null
+++ b/perllib/FixMyStreet/Roles/Moderation.pm
@@ -0,0 +1,41 @@
+package FixMyStreet::Roles::Moderation;
+use Moo::Role;
+
+=head2 latest_moderation_log_entry
+
+Return most recent AdminLog object concerning moderation
+
+=cut
+
+sub latest_moderation_log_entry {
+ my $self = shift;
+
+ my $latest = $self->moderation_original_datas->search(
+ $self->moderation_filter,
+ { order_by => { -desc => 'id' } })->first;
+ return unless $latest;
+
+ my $rs = $self->result_source->schema->resultset("AdminLog");
+ my $log = $rs->search({
+ object_id => $latest->id,
+ object_type => 'moderation',
+ })->first;
+ return $log if $log;
+
+ return $self->admin_log_entries->search({ action => 'moderation' }, { order_by => { -desc => 'id' } })->first;
+}
+
+=head2 moderation_history
+
+Returns all moderation history, most recent first.
+
+=cut
+
+sub moderation_history {
+ my $self = shift;
+ return $self->moderation_original_datas->search(
+ $self->moderation_filter,
+ { order_by => { -desc => 'id' } })->all;
+}
+
+1;