diff options
Diffstat (limited to 'perllib/FixMyStreet/DB')
-rw-r--r-- | perllib/FixMyStreet/DB/Factories.pm | 1 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/RABXColumn.pm | 18 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/AdminLog.pm | 81 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Body.pm | 45 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Comment.pm | 17 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Contact.pm | 55 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/ManifestTheme.pm | 47 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm | 6 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Problem.pm | 102 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Role.pm | 53 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/User.pm | 147 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/UserRole.pm | 50 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Alert.pm | 7 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Comment.pm | 7 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Contact.pm | 64 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Problem.pm | 31 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/State.pm | 6 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Schema.pm | 1 |
18 files changed, 629 insertions, 109 deletions
diff --git a/perllib/FixMyStreet/DB/Factories.pm b/perllib/FixMyStreet/DB/Factories.pm index 5af9ed38f..05c56e394 100644 --- a/perllib/FixMyStreet/DB/Factories.pm +++ b/perllib/FixMyStreet/DB/Factories.pm @@ -1,6 +1,7 @@ use strict; use warnings; use v5.14; +use utf8; use FixMyStreet::DB; diff --git a/perllib/FixMyStreet/DB/RABXColumn.pm b/perllib/FixMyStreet/DB/RABXColumn.pm index d14b48dc8..76eb21030 100644 --- a/perllib/FixMyStreet/DB/RABXColumn.pm +++ b/perllib/FixMyStreet/DB/RABXColumn.pm @@ -52,6 +52,8 @@ set_filtered_column behaviour to not trust the cache. sub rabx_column { my ($class, $col) = @_; + my $data_type = $class->column_info($col)->{data_type}; + # Apply the filtering for this column $class->filter_column( $col => { @@ -59,6 +61,10 @@ sub rabx_column { my $self = shift; my $ser = shift; return undef unless defined $ser; + # Some RABX columns are text, when they should be bytea. For + # these we must re-encode the string returned from the + # database, so that it is decoded again by RABX. + utf8::encode($ser) if $data_type ne 'bytea'; my $h = new IO::String($ser); return RABX::wire_rd($h); }, @@ -68,6 +74,10 @@ sub rabx_column { my $ser = ''; my $h = new IO::String($ser); RABX::wire_wr( $data, $h ); + # Some RABX columns are text, when they should be bytea. For + # these, we must re-decode the string encoded by RABX, so that + # it is encoded again when saved to the db. + utf8::decode($ser) if $data_type ne 'bytea'; return $ser; }, } @@ -77,14 +87,6 @@ sub rabx_column { $RABX_COLUMNS{ _get_class_identifier($class) }{$col} = 1; } -# The underlying column should always be UTF-8 encoded bytes. -sub get_column { - my ($self, $col) = @_; - my $res = $self->next::method ($col); - utf8::encode($res) if $RABX_COLUMNS{_get_class_identifier($self)}{$col} && utf8::is_utf8($res); - return $res; -} - sub set_filtered_column { my ($self, $col, $val) = @_; diff --git a/perllib/FixMyStreet/DB/Result/AdminLog.pm b/perllib/FixMyStreet/DB/Result/AdminLog.pm index 221690405..4c89138c9 100644 --- a/perllib/FixMyStreet/DB/Result/AdminLog.pm +++ b/perllib/FixMyStreet/DB/Result/AdminLog.pm @@ -61,4 +61,85 @@ __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; + } + if ($type eq 'manifesttheme') { + my $theme = $self->object; + return "/admin/manifesttheme/" . $theme->cobrand; + } + 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', + manifesttheme => 'cobrand', + }; + 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', + manifesttheme => 'ManifestTheme', + }; + $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/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm index 663181746..95debc910 100644 --- a/perllib/FixMyStreet/DB/Result/Body.pm +++ b/perllib/FixMyStreet/DB/Result/Body.pm @@ -117,6 +117,12 @@ __PACKAGE__->has_many( { cascade_copy => 0, cascade_delete => 0 }, ); __PACKAGE__->has_many( + "roles", + "FixMyStreet::DB::Result::Role", + { "foreign.body_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); +__PACKAGE__->has_many( "user_body_permissions", "FixMyStreet::DB::Result::UserBodyPermission", { "foreign.body_id" => "self.id" }, @@ -130,8 +136,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8CuxbffDaYS7TFlgff1nEg +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-05-23 18:03:28 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9sFgYQ9qhnZNcz3kUFYuvg __PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); __PACKAGE__->rabx_column('extra'); @@ -187,11 +193,7 @@ sub first_area_children { my $cobrand = $self->result_source->schema->cobrand; - my $children = FixMyStreet::MapIt::call('area/children', $body_area->area_id, - type => $cobrand->area_types_children, - ); - - return $children; + return $cobrand->fetch_area_children($body_area->area_id); } =head2 get_cobrand_handler @@ -209,6 +211,33 @@ sub get_cobrand_handler { return FixMyStreet::Cobrand->body_handler($self->areas); } +=item + +If get_cobrand_handler returns a cobrand, and that cobrand +has a council_name, use it in preference to the body name. + +=cut + +sub cobrand_name { + my $self = shift; + + # Because TfL covers all the boroughs in London, get_cobrand_handler + # may return another London cobrand if it is listed before tfl in + # ALLOWED_COBRANDS, because one of this body's area_ids will also + # match that cobrand's council_area_id. This leads to odd things like + # councils_text_all.html showing a message like "These will be sent to + # Bromley Council" when making a report within Westminster on the TfL + # cobrand. + # If the current body is TfL then we always want to show TfL as the cobrand name. + return $self->name if $self->name eq 'TfL' || $self->name eq 'Highways England'; + + my $handler = $self->get_cobrand_handler; + if ($handler && $handler->can('council_name')) { + return $handler->council_name; + } + return $self->name; +} + sub calculate_average { my ($self, $threshold) = @_; $threshold ||= 0; @@ -224,7 +253,7 @@ sub calculate_average { 'problem.state' => [ FixMyStreet::DB::Result::Problem->visible_states() ], }, { select => [ - { extract => "epoch from me.confirmed-problem.confirmed", -as => 'time' }, + { extract => \"epoch from me.confirmed-problem.confirmed", -as => 'time' }, ], as => [ qw/time/ ], rows => 100, diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm index 5d0253ef4..b217bf96c 100644 --- a/perllib/FixMyStreet/DB/Result/Comment.pm +++ b/perllib/FixMyStreet/DB/Result/Comment.pm @@ -101,6 +101,7 @@ __PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); __PACKAGE__->rabx_column('extra'); use Moo; +use FixMyStreet::Template::SafeString; use namespace::clean -except => [ 'meta' ]; use FixMyStreet::Template; @@ -201,7 +202,7 @@ sub moderation_filter { =head2 meta_line Returns a string to be used on a report update, describing some of the metadata -about an update +about an update. Can include HTML. =cut @@ -225,10 +226,15 @@ sub meta_line { } else { $body = $self->user->body; } + $body = FixMyStreet::Template::html_filter($body); if ($body eq 'Bromley Council') { $body = "$body <img src='/cobrands/bromley/favicon.png' alt=''>"; } elsif ($body eq 'Royal Borough of Greenwich') { $body = "$body <img src='/cobrands/greenwich/favicon.png' alt=''>"; + } elsif ($body eq 'Hounslow Borough Council') { + $body = 'Hounslow Highways'; + } elsif ($body eq 'Isle of Wight Council') { + $body = 'Island Roads'; } } my $cobrand_always_view_body_user = $c->cobrand->call_hook("always_view_body_contribute_details"); @@ -255,7 +261,7 @@ sub meta_line { $meta .= ', ' . _( 'and a defect raised' ); } - return $meta; + return FixMyStreet::Template::SafeString->new($meta); }; sub problem_state_processed { @@ -272,7 +278,11 @@ sub problem_state_display { return '' unless $state; my $cobrand_name = $c->cobrand->moniker; - $cobrand_name = 'bromley' if $self->problem->to_body_named('Bromley'); + my $names = join(',,', @{$self->problem->body_names}); + if ($names =~ /(Bromley|Isle of Wight|TfL)/) { + ($cobrand_name = lc $1) =~ s/ //g; + } + return FixMyStreet::DB->resultset("State")->display($state, 1, $cobrand_name); } @@ -282,6 +292,7 @@ sub is_latest { { problem_id => $self->problem_id, state => 'confirmed' }, { order_by => [ { -desc => 'confirmed' }, { -desc => 'id' } ] } )->first; + return unless $latest_update; return $latest_update->id == $self->id; } diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm index 17620f279..affc6d480 100644 --- a/perllib/FixMyStreet/DB/Result/Contact.pm +++ b/perllib/FixMyStreet/DB/Result/Contact.pm @@ -93,12 +93,34 @@ __PACKAGE__->many_to_many( response_templates => 'contact_response_templates', ' __PACKAGE__->many_to_many( response_priorities => 'contact_response_priorities', 'response_priority' ); __PACKAGE__->many_to_many( defect_types => 'contact_defect_types', 'defect_type' ); +__PACKAGE__->might_have( + "translations", + "FixMyStreet::DB::Result::Translation", + sub { + my $args = shift; + return { + "$args->{foreign_alias}.object_id" => { -ident => "$args->{self_alias}.id" }, + "$args->{foreign_alias}.tbl" => { '=' => \"?" }, + "$args->{foreign_alias}.col" => { '=' => \"?" }, + "$args->{foreign_alias}.lang" => { '=' => \"?" }, + }; + }, + { cascade_copy => 0, cascade_delete => 0 }, +); + sub category_display { my $self = shift; - $self->translate_column('category'); + $self->get_extra_metadata('display_name') || $self->translate_column('category'); } -sub get_metadata_for_editing { +sub groups { + my $self = shift; + my $groups = $self->get_extra_metadata('group') || []; + $groups = [ $groups ] unless ref $groups eq 'ARRAY'; + return $groups; +} + +sub get_all_metadata { my $self = shift; my @metadata = @{$self->get_extra_fields}; @@ -111,9 +133,19 @@ sub get_metadata_for_editing { return \@metadata; } +sub get_metadata_for_editing { + my $self = shift; + my $metadata = $self->get_all_metadata; + + # Ignore the special admin-form-created entry + my @metadata = grep { $_->{code} ne '_fms_disable_' } @$metadata; + + return \@metadata; +} + sub get_metadata_for_input { my $self = shift; - my $metadata = $self->get_metadata_for_editing; + my $metadata = $self->get_all_metadata; # Also ignore any we have with a 'server_set' automated attribute my @metadata = grep { !$_->{automated} || $_->{automated} ne 'server_set' } @$metadata; @@ -121,9 +153,26 @@ sub get_metadata_for_input { return \@metadata; } +sub get_metadata_for_storage { + my $self = shift; + my $metadata = $self->get_metadata_for_input; + + # Also ignore any that were only for textual display + my @metadata = grep { ($_->{variable} || '') ne 'false' } @$metadata; + + return \@metadata; +} + sub id_field { my $self = shift; return $self->get_extra_metadata('id_field') || 'fixmystreet_id'; } +sub disable_form_field { + my $self = shift; + my $metadata = $self->get_all_metadata; + my ($field) = grep { $_->{code} eq '_fms_disable_' } @$metadata; + return $field; +} + 1; diff --git a/perllib/FixMyStreet/DB/Result/ManifestTheme.pm b/perllib/FixMyStreet/DB/Result/ManifestTheme.pm new file mode 100644 index 000000000..a2f49eacb --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/ManifestTheme.pm @@ -0,0 +1,47 @@ +use utf8; +package FixMyStreet::DB::Result::ManifestTheme; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; +__PACKAGE__->load_components( + "FilterColumn", + "FixMyStreet::InflateColumn::DateTime", + "FixMyStreet::EncodedColumn", +); +__PACKAGE__->table("manifest_theme"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "manifest_theme_id_seq", + }, + "cobrand", + { data_type => "text", is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 0 }, + "short_name", + { data_type => "text", is_nullable => 0 }, + "background_colour", + { data_type => "text", is_nullable => 1 }, + "theme_colour", + { data_type => "text", is_nullable => 1 }, + "images", + { data_type => "text[]", is_nullable => 1 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->add_unique_constraint("manifest_theme_cobrand_key", ["cobrand"]); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2020-01-30 14:30:42 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Sgbva7nEVkjqG/+lQL/ryw + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm index 18d2a7683..1805e1fd2 100644 --- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm +++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm @@ -74,6 +74,7 @@ __PACKAGE__->belongs_to( # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:FLKiZELcfBcc9VwHU2MZYQ use Moo; +use FixMyStreet::Template::SafeString; use Text::Diff; use Data::Dumper; @@ -147,11 +148,12 @@ sub compare_photo { push @deleted, $diff->Items(1); push @added, $diff->Items(2); } - return (join ', ', map { + my $s = (join ', ', map { "<del style='background-color:#fcc'>$_</del>"; } @deleted) . (join ', ', map { "<ins style='background-color:#cfc'>$_</ins>"; } @added); + return FixMyStreet::Template::SafeString->new($s); } sub compare_extra { @@ -212,7 +214,7 @@ sub string_diff { $string .= $inserted; } } - return $string; + return FixMyStreet::Template::SafeString->new($string); } 1; diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index dc45091ee..37563d327 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -201,6 +201,8 @@ use Moo; use namespace::clean -except => [ 'meta' ]; use Utils; use FixMyStreet::Map::FMS; +use FixMyStreet::Template; +use FixMyStreet::Template::SafeString; use LWP::Simple qw($ua); use RABX; use URI; @@ -338,6 +340,7 @@ around service => sub { sub title_safe { my $self = shift; return _('Awaiting moderation') if $self->cobrand eq 'zurich' && $self->state eq 'submitted'; + return sprintf("%s problem", $self->category) if $self->cobrand eq 'tfl' && $self->result_source->schema->cobrand->moniker ne 'tfl'; return $self->title; } @@ -362,6 +365,9 @@ sub check_for_errors { $errors{title} = _('Please enter a subject') unless $self->title =~ m/\S/; + $errors{title} = _('Please make sure you are not including an email address') + if mySociety::EmailUtil::is_valid_email($self->title); + $errors{detail} = _('Please enter some details') unless $self->detail =~ m/\S/; @@ -373,13 +379,6 @@ sub check_for_errors { $errors{name} = _('Please enter your name'); } - if ( $self->category - && $self->category eq _('-- Pick a category --') ) - { - $errors{category} = _('Please choose a category'); - $self->category(undef); - } - return \%errors; } @@ -408,7 +407,28 @@ sub confirm { sub category_display { my $self = shift; - $self->translate_column('category'); + my $contact = $self->category_row; + return $self->category unless $contact; # Fallback; shouldn't happen, but some tests + return $contact->category_display; +} + +=head2 category_row + +Returns the corresponding Contact object for this problem's category and body. +If the report was sent to multiple bodies, only returns the first. + +=cut + +sub category_row { + my $self = shift; + my $schema = $self->result_source->schema; + my $body_id = $self->bodies_str_ids->[0]; + return unless $body_id && $body_id =~ /^[0-9]+$/; + my $contact = $schema->resultset("Contact")->find({ + body_id => $body_id, + category => $self->category, + }); + return $contact; } sub bodies_str_ids { @@ -505,6 +525,31 @@ sub tokenised_url { return "/M/". $token->token; } +has view_token => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + my $token = FixMyStreet::DB->resultset('Token')->create({ + scope => 'alert_to_reporter', + data => { id => $self->id } + }); + }, +); + +=head2 view_url + +Return a url for this problem report that will always show it +(even if e.g. a private report) but does not log the user in. + +=cut + +sub view_url { + my $self = shift; + return $self->url unless $self->non_public; + return "/R/" . $self->view_token->token; +} + =head2 is_hidden Returns 1 if the problem is in an hidden state otherwise 0. @@ -652,16 +697,16 @@ sub body { my $cache = $problem->result_source->schema->cache; return $cache->{bodies}{$problem->external_body} //= $c->model('DB::Body')->find({ id => $problem->external_body }); } else { - $body = $problem->external_body; + $body = FixMyStreet::Template::html_filter($problem->external_body); } } else { my $bodies = $problem->bodies; my @body_names = sort map { my $name = $_->name; if ($c and FixMyStreet->config('AREA_LINKS_FROM_PROBLEMS')) { - '<a href="' . $_->url . '">' . $name . '</a>'; + '<a href="' . $_->url . '">' . FixMyStreet::Template::html_filter($name) . '</a>'; } else { - $name; + FixMyStreet::Template::html_filter($name); } } values %$bodies; if ( scalar @body_names > 2 ) { @@ -671,7 +716,7 @@ sub body { $body = join( _(' and '), @body_names); } } - return $body; + return FixMyStreet::Template::SafeString->new($body); } @@ -755,23 +800,26 @@ sub defect_types { # Note: this only makes sense when called on a problem that has been sent! sub can_display_external_id { my $self = shift; - if ($self->external_id && $self->send_method_used && $self->to_body_named('Oxfordshire|Lincolnshire')) { + if ($self->external_id && $self->to_body_named('Oxfordshire|Lincolnshire|Isle of Wight|East Sussex')) { return 1; } return 0; } +# This can return HTML and is safe, so returns a FixMyStreet::Template::SafeString sub duration_string { my ( $problem, $c ) = @_; my $body = $c->cobrand->call_hook(link_to_council_cobrand => $problem) || $problem->body($c); my $handler = $c->cobrand->call_hook(get_body_handler_for_problem => $problem); if ( $handler && $handler->call_hook('is_council_with_case_management') ) { - return sprintf(_('Received by %s moments later'), $body); + my $s = sprintf(_('Received by %s moments later'), $body); + return FixMyStreet::Template::SafeString->new($s); } return unless $problem->whensent; - return sprintf(_('Sent to %s %s later'), $body, + my $s = sprintf(_('Sent to %s %s later'), $body, Utils::prettify_duration($problem->whensent->epoch - $problem->confirmed->epoch, 'minute') ); + return FixMyStreet::Template::SafeString->new($s); } sub local_coords { @@ -889,6 +937,8 @@ bodies by some mechanism. Right now that mechanism is Open311. sub updates_sent_to_body { my $self = shift; + + return 1 if $self->to_body_named('TfL'); return unless $self->send_method_used && $self->send_method_used =~ /Open311/; # Some bodies only send updates *to* FMS, they don't receive updates. @@ -1015,11 +1065,12 @@ sub pin_data { problem => $self, draggable => $opts{draggable}, type => $opts{type}, + base_url => $c->cobrand->relative_url_for_report($self), } }; sub static_map { - my ($self) = @_; + my ($self, %params) = @_; return unless $IM; @@ -1027,7 +1078,11 @@ sub static_map { unless $FixMyStreet::Map::map_class->isa("FixMyStreet::Map::OSM"); my $map_data = $FixMyStreet::Map::map_class->generate_map_data( - { cobrand => $self->get_cobrand_logged }, + { + cobrand => $self->get_cobrand_logged, + distance => 1, # prevents the call to Gaze which isn't necessary + $params{zoom} ? ( zoom => $params{zoom} ) : (), + }, latitude => $self->latitude, longitude => $self->longitude, pins => $self->used_map @@ -1084,7 +1139,7 @@ sub static_map { $image->Extent( geometry => '512x384', gravity => 'NorthWest'); $image->Extent( geometry => '512x320', gravity => 'SouthWest'); - $image->Scale( geometry => "310x200>" ); + $image->Scale( geometry => "310x200>" ) unless $params{full_size}; my @blobs = $image->ImageToBlob(magick => 'jpeg'); undef $image; @@ -1161,4 +1216,15 @@ has inspection_log_entry => ( }, ); +has alerts => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + return $self->result_source->schema->resultset('Alert')->search({ + alert_type => 'new_updates', parameter => $self->id + }); + }, +); + 1; diff --git a/perllib/FixMyStreet/DB/Result/Role.pm b/perllib/FixMyStreet/DB/Result/Role.pm new file mode 100644 index 000000000..e35b0b195 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/Role.pm @@ -0,0 +1,53 @@ +use utf8; +package FixMyStreet::DB::Result::Role; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; +__PACKAGE__->load_components( + "FilterColumn", + "FixMyStreet::InflateColumn::DateTime", + "FixMyStreet::EncodedColumn", +); +__PACKAGE__->table("roles"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "roles_id_seq", + }, + "body_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 1 }, + "permissions", + { data_type => "text[]", is_nullable => 1 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->add_unique_constraint("roles_body_id_name_key", ["body_id", "name"]); +__PACKAGE__->belongs_to( + "body", + "FixMyStreet::DB::Result::Body", + { id => "body_id" }, + { is_deferrable => 0, on_delete => "CASCADE,", on_update => "NO ACTION" }, +); +__PACKAGE__->has_many( + "user_roles", + "FixMyStreet::DB::Result::UserRole", + { "foreign.role_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-05-23 18:03:28 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:KkzVQZuzExH8PhZLJsnZgg + +__PACKAGE__->many_to_many( users => 'user_roles', 'user' ); + +1; diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm index d01ba92d0..b0a05d0b7 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -24,22 +24,30 @@ __PACKAGE__->add_columns( }, "email", { data_type => "text", is_nullable => 1 }, - "email_verified", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "name", { data_type => "text", is_nullable => 1 }, "phone", { data_type => "text", is_nullable => 1 }, - "phone_verified", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "password", { data_type => "text", default_value => "", is_nullable => 0 }, - "from_body", - { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, "flagged", { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "from_body", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, + "title", + { data_type => "text", is_nullable => 1 }, + "facebook_id", + { data_type => "bigint", is_nullable => 1 }, + "twitter_id", + { data_type => "bigint", is_nullable => 1 }, "is_superuser", { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "extra", + { data_type => "text", is_nullable => 1 }, + "email_verified", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "phone_verified", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "created", { data_type => "timestamp", @@ -54,16 +62,10 @@ __PACKAGE__->add_columns( is_nullable => 0, original => { default_value => \"now()" }, }, - "title", - { data_type => "text", is_nullable => 1 }, - "twitter_id", - { data_type => "bigint", is_nullable => 1 }, - "facebook_id", - { data_type => "bigint", is_nullable => 1 }, - "extra", - { data_type => "text", is_nullable => 1 }, "area_ids", { data_type => "integer[]", is_nullable => 1 }, + "oidc_ids", + { data_type => "text[]", is_nullable => 1 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->add_unique_constraint("users_facebook_id_key", ["facebook_id"]); @@ -121,10 +123,16 @@ __PACKAGE__->has_many( { "foreign.user_id" => "self.id" }, { cascade_copy => 0, cascade_delete => 0 }, ); +__PACKAGE__->has_many( + "user_roles", + "FixMyStreet::DB::Result::UserRole", + { "foreign.user_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-04-25 12:06:39 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BCCqv3JCec8psuRk/SdCJQ +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-06-20 16:31:44 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Ryb6giJm/7N7svg/d+2GeA # These are not fully unique constraints (they only are when the *_verified # is true), but this is managed in ResultSet::User's find() wrapper. @@ -136,6 +144,7 @@ __PACKAGE__->rabx_column('extra'); use Moo; use Text::CSV; +use List::MoreUtils 'uniq'; use FixMyStreet::SMS; use mySociety::EmailUtil; use namespace::clean -except => [ 'meta' ]; @@ -143,6 +152,7 @@ use namespace::clean -except => [ 'meta' ]; with 'FixMyStreet::Roles::Extra'; __PACKAGE__->many_to_many( planned_reports => 'user_planned_reports', 'report' ); +__PACKAGE__->many_to_many( roles => 'user_roles', 'role' ); sub cost { FixMyStreet->test_mode ? 1 : 12; @@ -153,10 +163,30 @@ __PACKAGE__->add_columns( encode_column => 1, encode_class => 'Crypt::Eksblowfish::Bcrypt', encode_args => { cost => cost() }, - encode_check_method => 'check_password', + encode_check_method => '_check_password', }, ); +sub check_password { + my $self = shift; + my $cobrand = $self->result_source->schema->cobrand; + if ($cobrand->moniker eq 'tfl') { + my $col_v = $self->get_extra_metadata('tfl_password'); + return unless defined $col_v; + $self->_column_encoders->{password}->($_[0], $col_v) eq $col_v; + } else { + $self->_check_password(@_); + } +} + +around password => sub { + my ($orig, $self) = (shift, shift); + if (@_) { + $self->set_extra_metadata(last_password_change => time()); + } + $self->$orig(@_); +}; + =head2 username Returns a verified email or phone for this user, preferring email, @@ -188,6 +218,15 @@ sub latest_anonymity { return $obj ? $obj->anonymous : 0; } +sub latest_visible_problem { + my $self = shift; + return $self->problems->search({ + state => [ FixMyStreet::DB::Result::Problem->visible_states() ] + }, { + order_by => { -desc => 'id' } + })->single; +} + =head2 check_for_errors $error_hashref = $user->check_for_errors(); @@ -298,7 +337,11 @@ sub body { sub moderating_user_name { my $self = shift; - return $self->body || _('an administrator'); + my $body = $self->body; + if ( $body && $body eq 'Isle of Wight Council' ) { + $body = 'Island Roads'; + } + return $body || _('an administrator'); } =head2 belongs_to_body @@ -375,7 +418,18 @@ has body_permissions => ( lazy => 1, default => sub { my $self = shift; - return [ $self->user_body_permissions->all ]; + my $perms = []; + foreach my $role ($self->roles->all) { + push @$perms, map { { + body_id => $role->body_id, + permission => $_, + } } @{$role->permissions}; + } + push @$perms, map { { + body_id => $_->body_id, + permission => $_->permission_type, + } } $self->user_body_permissions->all; + return $perms; }, ); @@ -392,8 +446,8 @@ sub permissions { return unless $self->belongs_to_body($body_id); - my @permissions = grep { $_->body_id == $self->from_body->id } @{$self->body_permissions}; - return { map { $_->permission_type => 1 } @permissions }; + my @permissions = grep { $_->{body_id} == $self->from_body->id } @{$self->body_permissions}; + return { map { $_->{permission} => 1 } @permissions }; } sub has_permission_to { @@ -404,18 +458,15 @@ sub has_permission_to { my $cobrand = $self->result_source->schema->cobrand; my $cobrand_perms = $cobrand->available_permissions; my %available = map { %$_ } values %$cobrand_perms; - # The 'trusted' permission is never set in the cobrand's - # available_permissions (see note there in Default.pm) so include it here. - $available{trusted} = 1; return 0 unless $available{$permission_type}; return 1 if $self->is_superuser; - return 0 if !$body_ids || (ref $body_ids && !@$body_ids); - $body_ids = [ $body_ids ] unless ref $body_ids; + return 0 if !$body_ids || (ref $body_ids eq 'ARRAY' && !@$body_ids); + $body_ids = [ $body_ids ] unless ref $body_ids eq 'ARRAY'; my %body_ids = map { $_ => 1 } @$body_ids; foreach (@{$self->body_permissions}) { - return 1 if $_->permission_type eq $permission_type && $body_ids{$_->body_id}; + return 1 if $_->{permission} eq $permission_type && $body_ids{$_->{body_id}}; } return 0; } @@ -464,7 +515,7 @@ sub admin_user_body_permissions { sub has_2fa { my $self = shift; - return $self->is_superuser && $self->get_extra_metadata('2fa_secret'); + return $self->get_extra_metadata('2fa_secret'); } sub contributing_as { @@ -516,6 +567,7 @@ sub anonymize_account { title => undef, twitter_id => undef, facebook_id => undef, + oidc_ids => undef, }); } @@ -565,14 +617,6 @@ sub is_planned_report { return scalar grep { $_->report_id == $id } @{$self->active_user_planned_reports}; } -sub update_reputation { - my ( $self, $change ) = @_; - - my $reputation = $self->get_extra_metadata('reputation') || 0; - $self->set_extra_metadata( reputation => $reputation + $change); - $self->update; -} - has categories => ( is => 'ro', lazy => 1, @@ -621,4 +665,35 @@ sub in_area { return $self->areas_hash->{$area}; } +has roles_hash => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + my %ids = map { $_->role_id => 1 } $self->user_roles->all; + return \%ids; + }, +); + +sub in_role { + my ($self, $role) = @_; + return $self->roles_hash->{$role}; +} + +sub add_oidc_id { + my ($self, $oidc_id) = @_; + + my $oidc_ids = $self->oidc_ids || []; + my @oidc_ids = uniq ( $oidc_id, @$oidc_ids ); + $self->oidc_ids(\@oidc_ids); +} + +sub remove_oidc_id { + my ($self, $oidc_id) = @_; + + my $oidc_ids = $self->oidc_ids || []; + my @oidc_ids = grep { $_ ne $oidc_id } @$oidc_ids; + $self->oidc_ids(scalar @oidc_ids ? \@oidc_ids : undef); +} + 1; diff --git a/perllib/FixMyStreet/DB/Result/UserRole.pm b/perllib/FixMyStreet/DB/Result/UserRole.pm new file mode 100644 index 000000000..9186e2aa1 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/UserRole.pm @@ -0,0 +1,50 @@ +use utf8; +package FixMyStreet::DB::Result::UserRole; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; +__PACKAGE__->load_components( + "FilterColumn", + "FixMyStreet::InflateColumn::DateTime", + "FixMyStreet::EncodedColumn", +); +__PACKAGE__->table("user_roles"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "user_roles_id_seq", + }, + "role_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "user_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->belongs_to( + "role", + "FixMyStreet::DB::Result::Role", + { id => "role_id" }, + { is_deferrable => 0, on_delete => "CASCADE,", on_update => "NO ACTION" }, +); +__PACKAGE__->belongs_to( + "user", + "FixMyStreet::DB::Result::User", + { id => "user_id" }, + { is_deferrable => 0, on_delete => "CASCADE,", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-05-23 16:52:59 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:1BSR4j0o5PApKEZmzVAnLg + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/perllib/FixMyStreet/DB/ResultSet/Alert.pm b/perllib/FixMyStreet/DB/ResultSet/Alert.pm index c61053fff..ddf80bc52 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Alert.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Alert.pm @@ -7,11 +7,6 @@ use warnings; sub timeline_created { my ( $rs, $restriction ) = @_; - my $prefetch = - $rs->result_source->storage->sql_maker->quote_char ? - [ qw/alert_type user/ ] : - [ qw/alert_type/ ]; - return $rs->search( { whensubscribed => { '>=', \"current_timestamp-'7 days'::interval" }, @@ -19,7 +14,7 @@ sub timeline_created { %{ $restriction }, }, { - prefetch => $prefetch, + prefetch => [ qw/alert_type user/ ], } ); } diff --git a/perllib/FixMyStreet/DB/ResultSet/Comment.pm b/perllib/FixMyStreet/DB/ResultSet/Comment.pm index b9a3df62d..034b86a40 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Comment.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Comment.pm @@ -13,18 +13,13 @@ sub to_body { sub timeline { my ( $rs ) = @_; - my $prefetch = - $rs->result_source->storage->sql_maker->quote_char ? - [ qw/user/ ] : - []; - return $rs->search( { 'me.state' => 'confirmed', 'me.created' => { '>=', \"current_timestamp-'7 days'::interval" }, }, { - prefetch => $prefetch, + prefetch => 'user', } ); } diff --git a/perllib/FixMyStreet/DB/ResultSet/Contact.pm b/perllib/FixMyStreet/DB/ResultSet/Contact.pm index 8ef6d1ac5..801d20cc0 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Contact.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Contact.pm @@ -3,6 +3,7 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; +use POSIX qw(strcoll); sub me { join('.', shift->current_source_alias, shift || q{}) } @@ -16,7 +17,12 @@ Filter down to not deleted contacts (so active or inactive). sub not_deleted { my $rs = shift; - return $rs->search( { $rs->me('state') => { '!=' => 'deleted' } } ); + return $rs->search( { $rs->me('state') => { -not_in => [ 'deleted', 'staff' ] } } ); +} + +sub not_deleted_admin { + my $rs = shift; + return $rs->search( { $rs->me('state') => { -not_in => [ 'deleted' ] } } ); } sub active { @@ -24,6 +30,53 @@ sub active { $rs->search( { $rs->me('state') => [ 'unconfirmed', 'confirmed' ] } ); } +sub for_new_reports { + my ($rs, $c, $bodies) = @_; + my $params = { + $rs->me('body_id') => [ keys %$bodies ], + }; + + if ($c->user_exists && $c->user->is_superuser) { + # Everything normal OR any staff states + $params->{$rs->me('state')} = [ 'unconfirmed', 'confirmed', 'staff' ]; + } elsif ($c->user_exists && $c->user->from_body) { + # Everything normal OR staff state in the user body + $params->{'-or'} = [ + $rs->me('state') => [ 'unconfirmed', 'confirmed' ], + { + $rs->me('body_id') => $c->user->from_body->id, + $rs->me('state') => 'staff', + }, + ]; + } else { + $params->{$rs->me('state')} = [ 'unconfirmed', 'confirmed' ]; + } + + $rs->search($params, { prefetch => 'body' }); +} + +sub translated { + my $rs = shift; + my $schema = $rs->result_source->schema; + $rs->search(undef, { + '+columns' => { 'msgstr' => 'translations.msgstr' }, + join => 'translations', + bind => [ 'category', $schema->lang, 'contact' ], + }); +} + +sub all_sorted { + my $rs = shift; + + my @contacts = $rs->translated->all; + @contacts = sort { + my $a_name = $a->get_extra_metadata('display_name') || $a->get_column('msgstr') || $a->category; + my $b_name = $b->get_extra_metadata('display_name') || $b->get_column('msgstr') || $b->category; + strcoll($a_name, $b_name) + } @contacts; + return @contacts; +} + sub summary_count { my ( $rs, $restriction ) = @_; @@ -37,4 +90,13 @@ sub summary_count { ); } +sub group_lookup { + my $rs = shift; + map { + my $group = $_->get_extra_metadata('group') || ''; + $group = join(',', ref $group ? @$group : $group); + $_->category => $group + } $rs->all; +} + 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/Problem.pm b/perllib/FixMyStreet/DB/ResultSet/Problem.pm index 37fc34057..e23cf78e1 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Problem.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Problem.pm @@ -141,7 +141,7 @@ sub _recent { $query->{photo} = { '!=', undef } if $photos; my $attrs = { - order_by => { -desc => 'coalesce(confirmed, created)' }, + order_by => { -desc => \'coalesce(confirmed, created)' }, rows => $num, }; @@ -155,10 +155,11 @@ sub _recent { } else { $probs = Memcached::get($key); if ($probs) { - # Need to reattach schema so that confirmed column gets reinflated. - $probs->[0]->result_source->schema( $rs->result_source->schema ) if $probs->[0]; - # Catch any cached ones since hidden - $probs = [ grep { $_->photo && ! $_->is_hidden } @$probs ]; + # Need to refetch to check if hidden since cached + $probs = [ $rs->search({ + id => [ map { $_->id } @$probs ], + %$query, + }, $attrs)->all ]; } else { $probs = [ $rs->search( $query, $attrs )->all ]; Memcached::set($key, $probs, _cache_timeout()); @@ -207,11 +208,6 @@ sub around_map { sub timeline { my ( $rs ) = @_; - my $prefetch = - $rs->result_source->storage->sql_maker->quote_char ? - [ qw/user/ ] : - []; - return $rs->search( { -or => { @@ -221,7 +217,7 @@ sub timeline { } }, { - prefetch => $prefetch, + prefetch => 'user', } ); } @@ -245,12 +241,9 @@ sub unique_users { return $rs->search( { state => [ FixMyStreet::DB::Result::Problem->visible_states() ], }, { - select => [ { distinct => 'user_id' } ], - as => [ 'user_id' ] - } )->as_subselect_rs->search( undef, { - select => [ { count => 'user_id' } ], - as => [ 'count' ] - } )->first->get_column('count'); + columns => [ 'user_id' ], + distinct => 1, + } ); } sub categories_summary { @@ -273,7 +266,9 @@ sub categories_summary { sub include_comment_counts { my $rs = shift; my $order_by = $rs->{attrs}{order_by}; - return $rs unless ref $order_by eq 'HASH' && $order_by->{-desc} eq 'comment_count'; + return $rs unless + (ref $order_by eq 'ARRAY' && ref $order_by->[0] eq 'HASH' && $order_by->[0]->{-desc} eq 'comment_count') + || (ref $order_by eq 'HASH' && $order_by->{-desc} eq 'comment_count'); $rs->search({}, { '+select' => [ { "" => \'(select count(*) from comment where problem_id=me.id and state=\'confirmed\')', diff --git a/perllib/FixMyStreet/DB/ResultSet/State.pm b/perllib/FixMyStreet/DB/ResultSet/State.pm index 3e6169aeb..4f98efbf2 100644 --- a/perllib/FixMyStreet/DB/ResultSet/State.pm +++ b/perllib/FixMyStreet/DB/ResultSet/State.pm @@ -1,6 +1,7 @@ package FixMyStreet::DB::ResultSet::State; use base 'DBIx::Class::ResultSet'; +use utf8; use strict; use warnings; use Memcached; @@ -74,8 +75,13 @@ sub display { return $unchanging->{$label} if $unchanging->{$label}; if ($cobrand && $label eq 'not responsible') { return 'third party responsibility' if $cobrand eq 'bromley'; + return "not Island Roads’ responsibility" if $cobrand eq 'isleofwight'; + return "not TfL’s responsibility" if $cobrand eq 'tfl'; return _("not the council's responsibility"); } + if ($cobrand && $cobrand eq 'oxfordshire' && $label eq 'unable to fix') { + return 'Investigation complete'; + } my ($state) = $rs->_filter(sub { $_->label eq $label }); return $label unless $state; $state->name($translate_now->{$label}) if $translate_now->{$label}; diff --git a/perllib/FixMyStreet/DB/Schema.pm b/perllib/FixMyStreet/DB/Schema.pm index be39069d8..e39a8422e 100644 --- a/perllib/FixMyStreet/DB/Schema.pm +++ b/perllib/FixMyStreet/DB/Schema.pm @@ -21,6 +21,7 @@ __PACKAGE__->load_namespaces( use Moo; use FixMyStreet; +__PACKAGE__->storage_type('::DBI::PgServerCursor'); __PACKAGE__->connection(FixMyStreet->dbic_connect_info); has lang => ( is => 'rw' ); |