diff options
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Admin')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Bodies.pm | 317 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm | 2 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm | 81 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/ManifestTheme.pm | 99 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Reports.pm | 523 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm | 140 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Roles.pm | 102 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Stats.pm | 46 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Templates.pm | 181 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Triage.pm | 163 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin/Users.pm | 313 |
11 files changed, 1572 insertions, 395 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm index 0e47d2238..6ae068cd9 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm @@ -51,27 +51,7 @@ sub index : Path : Args(0) { my $posted = $c->get_param('posted') || ''; if ( $posted eq 'body' ) { - $c->forward('check_for_super_user'); - $c->forward('/auth/check_csrf_token'); - - my $values = $c->forward('body_params'); - unless ( keys %{$c->stash->{body_errors}} ) { - my $body = $c->model('DB::Body')->create( $values->{params} ); - if ($values->{extras}) { - $body->set_extra_metadata( $_ => $values->{extras}->{$_} ) - for keys %{$values->{extras}}; - $body->update; - } - my @area_ids = $c->get_param_list('area_ids'); - foreach (@area_ids) { - $c->model('DB::BodyArea')->create( { body => $body, area_id => $_ } ); - } - - $c->stash->{object} = $body; - $c->stash->{translation_col} = 'name'; - $c->forward('update_translations'); - $c->stash->{updated} = _('New body added'); - } + $c->forward('update_body', [ undef, _('New body added') ]); } $c->forward( '/admin/fetch_all_bodies' ); @@ -147,6 +127,8 @@ sub edit : Chained('body') : PathPart('') : Args(0) { # to display email addresses as text $c->stash->{template} = 'admin/bodies/body.html'; $c->forward('/admin/fetch_contacts'); + $c->stash->{contacts} = [ $c->stash->{contacts}->all ]; + $c->forward('/report/stash_category_groups', [ $c->stash->{contacts}, 0 ]); return 1; } @@ -158,7 +140,8 @@ sub category : Chained('body') : PathPart('') { $c->forward( '/auth/get_csrf_token' ); my $contact = $c->stash->{body}->contacts->search( { category => $category } )->first; - $c->stash->{contact} = $contact; + $c->detach( '/page_error_404_not_found', [] ) unless $contact; + $c->stash->{contact} = $c->stash->{current_contact} = $contact; $c->stash->{translation_col} = 'category'; $c->stash->{object} = $c->stash->{contact}; @@ -220,140 +203,199 @@ sub check_for_super_user : Private { sub update_contacts : Private { my ( $self, $c ) = @_; - my $posted = $c->get_param('posted'); - my $editor = $c->forward('/admin/get_user'); - + my $posted = $c->get_param('posted') || ''; if ( $posted eq 'new' ) { - $c->forward('/auth/check_csrf_token'); + $c->forward('update_contact'); + } elsif ( $posted eq 'update' ) { + $c->forward('confirm_contacts'); + } elsif ( $posted eq 'body' ) { + $c->forward('update_body', [ $c->stash->{body}, _('Values updated') ]); + } +} - my %errors; +sub update_contact : Private { + my ( $self, $c ) = @_; - my $category = $self->trim( $c->get_param('category') ); - $errors{category} = _("Please choose a category") unless $category; - $errors{note} = _('Please enter a message') unless $c->get_param('note'); + my $editor = $c->forward('/admin/get_user'); + $c->forward('/auth/check_csrf_token'); - my $contact = $c->model('DB::Contact')->find_or_new( - { - body_id => $c->stash->{body_id}, - category => $category, - } - ); + my %errors; - my $email = $c->get_param('email'); - $email =~ s/\s+//g; - my $send_method = $c->get_param('send_method') || $contact->send_method || $contact->body->send_method || ""; - unless ( $send_method eq 'Open311' ) { - $errors{email} = _('Please enter a valid email') unless is_valid_email_list($email) || $email eq 'REFUSED'; - } + my $current_category = $c->get_param('current_category') || ''; + my $current_contact = $c->model('DB::Contact')->find({ + body_id => $c->stash->{body_id}, + category => $current_category, + }); + $c->stash->{current_contact} = $current_contact; - $contact->email( $email ); - $contact->state( $c->get_param('state') ); - $contact->non_public( $c->get_param('non_public') ? 1 : 0 ); - $contact->note( $c->get_param('note') ); - $contact->whenedited( \'current_timestamp' ); - $contact->editor( $editor ); - $contact->endpoint( $c->get_param('endpoint') ); - $contact->jurisdiction( $c->get_param('jurisdiction') ); - $contact->api_key( $c->get_param('api_key') ); - $contact->send_method( $c->get_param('send_method') ); - - # Set flags in extra to the appropriate values - if ( $c->get_param('photo_required') ) { - $contact->set_extra_metadata_if_undefined( photo_required => 1 ); - } - else { - $contact->unset_extra_metadata( 'photo_required' ); - } - if ( $c->get_param('inspection_required') ) { - $contact->set_extra_metadata( inspection_required => 1 ); - } - else { - $contact->unset_extra_metadata( 'inspection_required' ); - } - if ( $c->get_param('reputation_threshold') ) { - $contact->set_extra_metadata( reputation_threshold => int($c->get_param('reputation_threshold')) ); + my $category = $self->trim( $c->get_param('category') ); + $errors{category} = _("Please choose a category") unless $category; + $errors{note} = _('Please enter a message') unless $c->get_param('note') || FixMyStreet->config('STAGING_SITE'); + + my $contact = $c->model('DB::Contact')->find_or_new( + { + body_id => $c->stash->{body_id}, + category => $category, } - if ( my $group = $c->get_param('group') ) { - $contact->set_extra_metadata( group => $group ); - } else { + ); + if ($current_contact && $contact->id && $contact->id != $current_contact->id) { + $errors{category} = _('You cannot rename a category to an existing category'); + } elsif ($current_contact && !$contact->id) { + # Changed name + $contact = $current_contact; + $c->model('DB::Problem')->to_body($c->stash->{body_id})->search({ category => $current_category })->update({ category => $category }); + $contact->category($category); + } + + my $email = $c->get_param('email'); + $email =~ s/\s+//g; + my $send_method = $c->get_param('send_method') || $contact->body->send_method || ""; + my $email_unchanged = $contact->email && $email && $contact->email eq $email; + unless ( $send_method eq 'Open311' || $email_unchanged ) { + $errors{email} = _('Please enter a valid email') unless is_valid_email_list($email) || $email eq 'REFUSED'; + } + + $contact->email( $email ); + $contact->state( $c->get_param('state') ); + $contact->non_public( $c->get_param('non_public') ? 1 : 0 ); + $contact->note( $c->get_param('note') ); + $contact->whenedited( \'current_timestamp' ); + $contact->editor( $editor ); + $contact->endpoint( $c->get_param('endpoint') ); + $contact->jurisdiction( $c->get_param('jurisdiction') ); + $contact->api_key( $c->get_param('api_key') ); + $contact->send_method( $c->get_param('send_method') ); + + # Set flags in extra to the appropriate values + if ( $c->get_param('photo_required') ) { + $contact->set_extra_metadata_if_undefined( photo_required => 1 ); + } else { + $contact->unset_extra_metadata( 'photo_required' ); + } + if ( $c->get_param('open311_protect') ) { + $contact->set_extra_metadata( open311_protect => 1 ); + } else { + $contact->unset_extra_metadata( 'open311_protect' ); + } + if ( my @group = $c->get_param_list('group') ) { + @group = grep { $_ } @group; + if (scalar @group == 0) { $contact->unset_extra_metadata( 'group' ); + } else { + $contact->set_extra_metadata( group => \@group ); } + } else { + $contact->unset_extra_metadata( 'group' ); + } + + $c->forward('/admin/update_extra_fields', [ $contact ]); + $c->forward('contact_cobrand_extra_fields', [ $contact, \%errors ]); + + # Special form disabling form + if ($c->get_param('disable')) { + my $msg = $c->get_param('disable_message'); + $msg = FixMyStreet::Template::sanitize($msg); + $errors{category} = _('Please enter a message') unless $msg; + my $meta = { + code => '_fms_disable_', + variable => 'false', + protected => 'true', + disable_form => 'true', + description => $msg, + }; + $contact->update_extra_field($meta); + } else { + $contact->remove_extra_field('_fms_disable_'); + } + + if ( %errors ) { + $c->stash->{updated} = _('Please correct the errors below'); + $c->stash->{contact} = $contact; + $c->stash->{errors} = \%errors; + } elsif ( $contact->in_storage ) { + $c->stash->{updated} = _('Values updated'); + $c->forward('/admin/log_edit', [ $contact->id, 'category', 'edit' ]); + # NB: History is automatically stored by a trigger in the database + $contact->update; + } else { + $c->stash->{updated} = _('New category contact added'); + $contact->insert; + $c->forward('/admin/log_edit', [ $contact->id, 'category', 'add' ]); + } + unless ( %errors ) { + $c->stash->{translation_col} = 'category'; + $c->stash->{object} = $contact; + $c->forward('update_translations'); + } - $c->forward('/admin/update_extra_fields', [ $contact ]); - $c->forward('contact_cobrand_extra_fields', [ $contact ]); +} - if ( %errors ) { - $c->stash->{updated} = _('Please correct the errors below'); - $c->stash->{contact} = $contact; - $c->stash->{errors} = \%errors; - } elsif ( $contact->in_storage ) { - $c->stash->{updated} = _('Values updated'); +sub confirm_contacts : Private { + my ( $self, $c ) = @_; - # NB: History is automatically stored by a trigger in the database - $contact->update; - } else { - $c->stash->{updated} = _('New category contact added'); - $contact->insert; + $c->forward('/auth/check_csrf_token'); + + my @categories = $c->get_param_list('confirmed'); + + my $contacts = $c->model('DB::Contact')->search( + { + body_id => $c->stash->{body_id}, + category => { -in => \@categories }, } + ); - unless ( %errors ) { - $c->stash->{translation_col} = 'category'; - $c->stash->{object} = $contact; - $c->forward('update_translations'); + my $editor = $c->forward('/admin/get_user'); + $contacts->update( + { + state => 'confirmed', + whenedited => \'current_timestamp', + note => 'Confirmed', + editor => $editor, } + ); - } elsif ( $posted eq 'update' ) { - $c->forward('/auth/check_csrf_token'); + $c->forward('/admin/log_edit', [ $c->stash->{body_id}, 'body', 'edit' ]); + $c->stash->{updated} = _('Values updated'); +} - my @categories = $c->get_param_list('confirmed'); +sub update_body : Private { + my ($self, $c, $body, $msg) = @_; - my $contacts = $c->model('DB::Contact')->search( - { - body_id => $c->stash->{body_id}, - category => { -in => \@categories }, - } - ); - - $contacts->update( - { - state => 'confirmed', - whenedited => \'current_timestamp', - note => 'Confirmed', - editor => $editor, - } - ); + $c->forward('check_for_super_user'); + $c->forward('/auth/check_csrf_token'); - $c->stash->{updated} = _('Values updated'); - } elsif ( $posted eq 'body' ) { - $c->forward('check_for_super_user'); - $c->forward('/auth/check_csrf_token'); - - my $values = $c->forward( 'body_params' ); - unless ( keys %{$c->stash->{body_errors}} ) { - $c->stash->{body}->update( $values->{params} ); - if ($values->{extras}) { - $c->stash->{body}->set_extra_metadata( $_ => $values->{extras}->{$_} ) - for keys %{$values->{extras}}; - $c->stash->{body}->update; - } - my @current = $c->stash->{body}->body_areas->all; - my %current = map { $_->area_id => 1 } @current; - my @area_ids = $c->get_param_list('area_ids'); - foreach (@area_ids) { - $c->model('DB::BodyArea')->find_or_create( { body => $c->stash->{body}, area_id => $_ } ); - delete $current{$_}; - } - # Remove any others - $c->stash->{body}->body_areas->search( { area_id => [ keys %current ] } )->delete; + my $values = $c->forward('body_params'); + return if %{$c->stash->{body_errors}}; - $c->stash->{translation_col} = 'name'; - $c->stash->{object} = $c->stash->{body}; - $c->forward('update_translations'); + if ($body) { + $body->update( $values->{params} ); + $c->forward('/admin/log_edit', [ $body->id, 'body', 'edit' ]); + } else { + $body = $c->model('DB::Body')->create( $values->{params} ); + $c->forward('/admin/log_edit', [ $body->id, 'body', 'add' ]); + } - $c->stash->{updated} = _('Values updated'); - } + if ($values->{extras}) { + $body->set_extra_metadata( $_ => $values->{extras}->{$_} ) + for keys %{$values->{extras}}; + $body->update; } + my @current = $body->body_areas->all; + my %current = map { $_->area_id => 1 } @current; + my @area_ids = $c->get_param_list('area_ids'); + foreach (@area_ids) { + $c->model('DB::BodyArea')->find_or_create( { body => $body, area_id => $_ } ); + delete $current{$_}; + } + # Remove any others + $body->body_areas->search( { area_id => [ keys %current ] } )->delete; + + $c->stash->{translation_col} = 'name'; + $c->stash->{object} = $body; + $c->forward('update_translations'); + + $c->stash->{updated} = $msg; } sub body_params : Private { @@ -375,9 +417,13 @@ sub body_params : Private { ); my %params = map { $_ => $c->get_param($_) || $defaults{$_} } keys %defaults; $c->forward('check_body_params', [ \%params ]); + my @extras = qw/fetch_all_problems/; + my $cobrand_extras = $c->cobrand->call_hook('body_extra_fields'); + push @extras, @$cobrand_extras if $cobrand_extras; + %defaults = map { $_ => '' } @extras; - my %extras = map { $_ => $c->get_param($_) || $defaults{$_} } @extras; + my %extras = map { $_ => $c->get_param("extra[$_]") || $defaults{$_} } @extras; return { params => \%params, extras => \%extras }; } @@ -392,12 +438,13 @@ sub check_body_params : Private { } sub contact_cobrand_extra_fields : Private { - my ( $self, $c, $contact ) = @_; + my ( $self, $c, $contact, $errors ) = @_; my $extra_fields = $c->cobrand->call_hook('contact_extra_fields'); foreach ( @$extra_fields ) { $contact->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); } + $c->cobrand->call_hook(contact_extra_fields_validation => $contact, $errors); } sub fetch_translations : Private { diff --git a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm index ed9b40fd0..6c1a25e5a 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm @@ -76,7 +76,7 @@ sub edit : Path : Args(2) { my @new_contact_ids = $c->get_param_list('categories'); @new_contact_ids = @{ mySociety::ArrayUtils::intersection(\@live_contact_ids, \@new_contact_ids) }; $defect_type->contact_defect_types->search({ - contact_id => { '!=' => \@new_contact_ids }, + contact_id => { -not_in => \@new_contact_ids }, })->delete; foreach my $contact_id (@new_contact_ids) { $defect_type->contact_defect_types->find_or_create({ diff --git a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm deleted file mode 100644 index 0026acb9c..000000000 --- a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm +++ /dev/null @@ -1,81 +0,0 @@ -package FixMyStreet::App::Controller::Admin::ExorDefects; -use Moose; -use namespace::autoclean; - -use DateTime; -use Try::Tiny; -use FixMyStreet::Integrations::ExorRDI; -use FixMyStreet::DateRange; - -BEGIN { extends 'Catalyst::Controller'; } - - -sub index : Path : Args(0) { - my ( $self, $c ) = @_; - - foreach (qw(error_message start_date end_date user_id)) { - if ( defined $c->flash->{$_} ) { - $c->stash->{$_} = $c->flash->{$_}; - } - } - - my @inspectors = $c->cobrand->users->search({ - 'user_body_permissions.permission_type' => 'report_inspect' - }, { - join => 'user_body_permissions', - distinct => 1, - } - )->all; - $c->stash->{inspectors} = \@inspectors; - - # Default start/end date is today - my $now = DateTime->now( time_zone => - FixMyStreet->time_zone || FixMyStreet->local_time_zone ); - $c->stash->{start_date} ||= $now; - $c->stash->{end_date} ||= $now; - -} - -sub download : Path('download') : Args(0) { - my ( $self, $c ) = @_; - - if ( !$c->cobrand->can('exor_rdi_link_id') ) { - # This only works on the Oxfordshire cobrand currently. - $c->detach( '/page_error_404_not_found', [] ); - } - - my $range = FixMyStreet::DateRange->new( - start_date => $c->get_param('start_date'), - end_date => $c->get_param('end_date'), - parser => DateTime::Format::Strptime->new( pattern => '%Y-%m-%d' ), - ); - - my $params = { - start_date => $range->start, - inspection_date => $range->start, - end_date => $range->end, - user => $c->get_param('user_id'), - mark_as_processed => 0, - }; - my $rdi = FixMyStreet::Integrations::ExorRDI->new($params); - - try { - my $out = $rdi->construct; - $c->res->content_type('text/csv; charset=utf-8'); - $c->res->header('content-disposition' => "attachment; filename=" . $rdi->filename); - $c->res->body( $out ); - } catch { - die $_ unless $_ =~ /FixMyStreet::Integrations::ExorRDI::Error/; - if ($params->{user}) { - $c->flash->{error_message} = _("No inspections by that inspector in the selected date range."); - } else { - $c->flash->{error_message} = _("No inspections in the selected date range."); - } - $c->flash->{start_date} = $params->{start_date}; - $c->flash->{end_date} = $params->{end_date}; - $c->flash->{user_id} = $params->{user}; - $c->res->redirect( $c->uri_for( '' ) ); - }; -} - -1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/ManifestTheme.pm b/perllib/FixMyStreet/App/Controller/Admin/ManifestTheme.pm new file mode 100644 index 000000000..9e3bdc33e --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/ManifestTheme.pm @@ -0,0 +1,99 @@ +package FixMyStreet::App::Controller::Admin::ManifestTheme; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +use FixMyStreet::App::Form::ManifestTheme; + +sub auto :Private { + my ($self, $c) = @_; + + if ( $c->cobrand->moniker eq 'fixmystreet' ) { + $c->stash(rs => $c->model('DB::ManifestTheme')->search_rs({}), show_all => 1); + } else { + $c->stash(rs => $c->model('DB::ManifestTheme')->search_rs({ cobrand => $c->cobrand->moniker })); + } +} + +sub index :Path :Args(0) { + my ( $self, $c ) = @_; + + unless ( $c->stash->{show_all} ) { + if ( $c->stash->{rs}->count ) { + $c->res->redirect($c->uri_for($self->action_for('edit'), [ $c->stash->{rs}->first->cobrand ])); + } else { + $c->res->redirect($c->uri_for($self->action_for('create'))); + } + $c->detach; + } +} + +sub item :PathPart('admin/manifesttheme') :Chained :CaptureArgs(1) { + my ($self, $c, $cobrand) = @_; + + my $obj = $c->stash->{rs}->find({ cobrand => $cobrand }) + or $c->detach('/page_error_404_not_found', []); + $c->stash(obj => $obj); +} + +sub edit :PathPart('') :Chained('item') :Args(0) { + my ($self, $c) = @_; + + my $form = $self->form($c, $c->stash->{obj}); + + # We need to do this after form processing, in case a form POST has deleted + # an icon. + $c->stash->{editing_manifest_theme} = $c->forward('/offline/_find_manifest_theme', [ $c->stash->{obj}->cobrand, 1 ]); + + return $form; +} + + +sub create :Local :Args(0) { + my ($self, $c) = @_; + + unless ( $c->stash->{show_all} || $c->stash->{rs}->count == 0) { + $c->res->redirect($c->uri_for($self->action_for('edit'), [ $c->stash->{rs}->first->cobrand ])); + $c->detach; + } + + my $theme = $c->stash->{rs}->new_result({}); + return $self->form($c, $theme); +} + +sub form { + my ($self, $c, $theme) = @_; + + if ($c->get_param('delete_theme')) { + $c->forward('_delete_all_manifest_icons'); + $c->forward('/offline/_clear_manifest_theme_cache', [ $theme->cobrand ]); + $theme->delete; + $c->forward('/admin/log_edit', [ $theme->id, 'manifesttheme', 'delete' ]); + $c->response->redirect($c->uri_for($self->action_for('index'))); + $c->detach; + } + + my $action = $theme->in_storage ? 'edit' : 'add'; + my $form = FixMyStreet::App::Form::ManifestTheme->new( cobrand => $c->cobrand->moniker ); + $c->stash(template => 'admin/manifesttheme/form.html', form => $form); + my $params = $c->req->params; + $params->{icon} = $c->req->upload('icon') if $params->{icon}; + $form->process(item => $theme, params => $params); + return unless $form->validated; + + $c->forward('/admin/log_edit', [ $theme->id, 'manifesttheme', $action ]); + $c->forward('/offline/_clear_manifest_theme_cache', [ $theme->cobrand ]); + $c->response->redirect($c->uri_for($self->action_for('index'))); +} + +sub _delete_all_manifest_icons :Private { + my ($self, $c) = @_; + + my $theme = $c->forward('/offline/_find_manifest_theme', [ $c->stash->{obj}->cobrand, 1 ]); + foreach my $icon ( @{ $theme->{icons} } ) { + unlink FixMyStreet->path_to('web', $icon->{src}); + } +} + +1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/Reports.pm b/perllib/FixMyStreet/App/Controller/Admin/Reports.pm new file mode 100644 index 000000000..7300fe676 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/Reports.pm @@ -0,0 +1,523 @@ +package FixMyStreet::App::Controller::Admin::Reports; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +use List::MoreUtils 'uniq'; +use FixMyStreet::SMS; +use Utils; + +=head1 NAME + +FixMyStreet::App::Controller::Admin::Reports - Catalyst Controller + +=head1 DESCRIPTION + +Admin pages + +=head1 METHODS + +=cut + +sub index : Path { + my ( $self, $c ) = @_; + + $c->stash->{edit_body_contacts} = 1 + if grep { $_ eq 'body' } keys %{$c->stash->{allowed_pages}}; + + my $query = {}; + if ( $c->cobrand->moniker eq 'zurich' ) { + my $type = $c->stash->{admin_type}; + my $body = $c->stash->{body}; + if ( $type eq 'dm' ) { + my @children = map { $_->id } $body->bodies->all; + my @all = (@children, $body->id); + $query = { bodies_str => \@all }; + } elsif ( $type eq 'sdm' ) { + $query = { bodies_str => $body->id }; + } + } + + my $order = $c->get_param('o') || 'id'; + my $dir = defined $c->get_param('d') ? $c->get_param('d') : 1; + $c->stash->{order} = $order; + $c->stash->{dir} = $dir; + $order = $dir ? { -desc => "me.$order" } : "me.$order"; + + my $p_page = $c->get_param('p') || 1; + my $u_page = $c->get_param('u') || 1; + + return if $c->cobrand->call_hook(report_search_query => $query, $p_page, $u_page, $order); + + if (my $search = $c->get_param('search')) { + $search = $self->trim($search); + + # In case an email address, wrapped in <...> + if ($search =~ /^<(.*)>$/) { + my $possible_email = $1; + my $parsed = FixMyStreet::SMS->parse_username($possible_email); + $search = $possible_email if $parsed->{email}; + } + + $c->stash->{searched} = $search; + + my $search_n = 0; + $search_n = int($search) if $search =~ /^\d+$/; + + my $like_search = "%$search%"; + + my $parsed = FixMyStreet::SMS->parse_username($search); + my $valid_phone = $parsed->{phone}; + my $valid_email = $parsed->{email}; + + if ($valid_email) { + $query->{'-or'} = [ + 'user.email' => { ilike => $like_search }, + ]; + } elsif ($valid_phone) { + $query->{'-or'} = [ + 'user.phone' => { ilike => $like_search }, + ]; + } elsif ($search =~ /^id:(\d+)$/) { + $query->{'-or'} = [ + 'me.id' => int($1), + ]; + } elsif ($search =~ /^area:(\d+)$/) { + $query->{'-or'} = [ + 'me.areas' => { like => "%,$1,%" } + ]; + } elsif ($search =~ /^ref:(\d+)$/) { + $query->{'-or'} = [ + 'me.external_id' => { like => "%$1%" } + ]; + } else { + $query->{'-or'} = [ + 'me.id' => $search_n, + 'user.email' => { ilike => $like_search }, + 'user.phone' => { ilike => $like_search }, + 'me.external_id' => { ilike => $like_search }, + 'me.name' => { ilike => $like_search }, + 'me.title' => { ilike => $like_search }, + detail => { ilike => $like_search }, + bodies_str => { like => $like_search }, + cobrand_data => { like => $like_search }, + ]; + } + + my $problems = $c->cobrand->problems->search( + $query, + { + join => 'user', + '+columns' => 'user.email', + rows => 50, + order_by => $order, + } + )->page( $p_page ); + + $c->stash->{problems} = [ $problems->all ]; + $c->stash->{problems_pager} = $problems->pager; + + if ($valid_email) { + $query = [ + 'user.email' => { ilike => $like_search }, + ]; + } elsif ($valid_phone) { + $query = [ + 'user.phone' => { ilike => $like_search }, + ]; + } elsif ($search =~ /^id:(\d+)$/) { + $query = [ + 'me.id' => int($1), + 'me.problem_id' => int($1), + ]; + } elsif ($search =~ /^area:(\d+)$/) { + $query = []; + } else { + $query = [ + 'me.id' => $search_n, + 'problem.id' => $search_n, + 'user.email' => { ilike => $like_search }, + 'user.phone' => { ilike => $like_search }, + 'me.name' => { ilike => $like_search }, + text => { ilike => $like_search }, + 'me.cobrand_data' => { ilike => $like_search }, + ]; + } + + if (@$query) { + my $updates = $c->cobrand->updates->search( + { + -or => $query, + }, + { + '+columns' => ['user.email'], + join => 'user', + prefetch => [qw/problem/], + rows => 50, + order_by => { -desc => 'me.id' } + } + )->page( $u_page ); + $c->stash->{updates} = [ $updates->all ]; + $c->stash->{updates_pager} = $updates->pager; + } + + } else { + + my $problems = $c->cobrand->problems->search( + $query, + { order_by => $order, rows => 50 } + )->page( $p_page ); + $c->stash->{problems} = [ $problems->all ]; + $c->stash->{problems_pager} = $problems->pager; + } +} + +sub edit_display : Private { + my ( $self, $c ) = @_; + + my $problem = $c->stash->{problem}; + + $c->stash->{page} = 'admin'; + FixMyStreet::Map::display_map( + $c, + latitude => $problem->latitude, + longitude => $problem->longitude, + pins => $problem->used_map + ? [ { + latitude => $problem->latitude, + longitude => $problem->longitude, + colour => $c->cobrand->pin_colour($problem, 'admin'), + type => 'big', + draggable => 1, + } ] + : [], + print_report => 1, + ); +} + +sub edit : Path('/admin/report_edit') : Args(1) { + my ( $self, $c, $id ) = @_; + + my $problem = $c->cobrand->problems->search( { id => $id } )->first; + + $c->detach( '/page_error_404_not_found', [] ) + unless $problem; + + unless ( + $c->cobrand->moniker eq 'zurich' + || $c->user->has_permission_to(report_edit => $problem->bodies_str_ids) + ) { + $c->detach( '/page_error_403_access_denied', [] ); + } + + $c->stash->{problem} = $problem; + if ( $problem->extra ) { + my @fields; + if ( my $fields = $problem->get_extra_fields ) { + for my $field ( @{$fields} ) { + my $name = $field->{description} ? + "$field->{description} ($field->{name})" : + "$field->{name}"; + push @fields, { name => $name, val => $field->{value} }; + } + } + my $extra = $problem->get_extra_metadata; + if ( $extra->{duplicates} ) { + push @fields, { name => 'Duplicates', val => join( ',', @{ $problem->get_extra_metadata('duplicates') } ) }; + delete $extra->{duplicates}; + } + for my $key ( keys %$extra ) { + push @fields, { name => $key, val => $extra->{$key} }; + } + + $c->stash->{extra_fields} = \@fields; + } + + $c->forward('/auth/get_csrf_token'); + + $c->forward('categories_for_point'); + + $c->forward('alerts_for_report'); + + $c->forward('/admin/check_username_for_abuse', [ $problem->user ] ); + + $c->stash->{updates} = + [ $c->model('DB::Comment') + ->search( { problem_id => $problem->id }, { order_by => [ 'created', 'id' ] } ) + ->all ]; + + if (my $rotate_photo_param = $c->forward('/admin/_get_rotate_photo_param')) { + $c->forward('/admin/rotate_photo', [$problem, @$rotate_photo_param]); + $c->detach('edit_display'); + } + + if ( $c->cobrand->moniker eq 'zurich' ) { + my $done = $c->cobrand->admin_report_edit(); + $c->detach('edit_display') if $done; + } + + if ( $c->get_param('resend') && !$c->cobrand->call_hook('disable_resend_button') ) { + $c->forward('/auth/check_csrf_token'); + + $problem->resend; + $problem->update(); + $c->stash->{status_message} = _('That problem will now be resent.'); + + $c->forward( '/admin/log_edit', [ $id, 'problem', 'resend' ] ); + } + elsif ( $c->get_param('mark_sent') ) { + $c->forward('/auth/check_csrf_token'); + $problem->update({ whensent => \'current_timestamp' })->discard_changes; + $c->stash->{status_message} = _('That problem has been marked as sent.'); + $c->forward( '/admin/log_edit', [ $id, 'problem', 'marked sent' ] ); + } + elsif ( $c->get_param('flaguser') ) { + $c->forward('/admin/users/flag'); + $c->stash->{problem}->discard_changes; + } + elsif ( $c->get_param('removeuserflag') ) { + $c->forward('/admin/users/flag_remove'); + $c->stash->{problem}->discard_changes; + } + elsif ( $c->get_param('banuser') ) { + $c->forward('/admin/users/ban'); + } + elsif ( $c->get_param('submit') ) { + $c->forward('/auth/check_csrf_token'); + + my $old_state = $problem->state; + + my %columns = ( + flagged => $c->get_param('flagged') ? 1 : 0, + non_public => $c->get_param('non_public') ? 1 : 0, + ); + foreach (qw/state anonymous title detail name external_id external_body external_team/) { + $columns{$_} = $c->get_param($_); + } + + # Look this up here for moderation line to use + my $remove_photo_param = $c->forward('/admin/_get_remove_photo_param'); + + if ($columns{title} ne $problem->title || $columns{detail} ne $problem->detail || + $columns{anonymous} ne $problem->anonymous || $remove_photo_param) { + $problem->create_related( moderation_original_data => { + title => $problem->title, + detail => $problem->detail, + photo => $problem->photo, + anonymous => $problem->anonymous, + category => $problem->category, + $problem->extra ? (extra => $problem->extra) : (), + }); + } + + $problem->set_inflated_columns(\%columns); + + if ($c->get_param('closed_updates')) { + $problem->set_extra_metadata(closed_updates => 1); + } else { + $problem->unset_extra_metadata('closed_updates'); + } + + $c->forward( '/admin/reports/edit_category', [ $problem, $problem->state ne $old_state ] ); + $c->forward('/admin/update_user', [ $problem ]); + + # Deal with photos + if ($remove_photo_param) { + $c->forward('/admin/remove_photo', [ $problem, $remove_photo_param ]); + } + + if ($problem->state eq 'hidden' || $problem->non_public) { + $problem->get_photoset->delete_cached(plus_updates => 1); + } + + if ( $problem->is_visible() and $old_state eq 'unconfirmed' ) { + $problem->confirmed( \'current_timestamp' ); + } + + $problem->lastupdate( \'current_timestamp' ); + $problem->update; + + if ( $problem->state ne $old_state ) { + $c->forward( '/admin/log_edit', [ $id, 'problem', 'state_change' ] ); + + my $name = $c->user->moderating_user_name; + my $extra = { is_superuser => 1 }; + if ($c->user->from_body) { + delete $extra->{is_superuser}; + $extra->{is_body_user} = $c->user->from_body->id; + } + my $timestamp = \'current_timestamp'; + $problem->add_to_comments( { + text => $c->stash->{update_text} || '', + created => $timestamp, + confirmed => $timestamp, + user_id => $c->user->id, + name => $name, + mark_fixed => 0, + anonymous => 0, + state => 'confirmed', + problem_state => $problem->state, + extra => $extra + } ); + } + $c->forward( '/admin/log_edit', [ $id, 'problem', 'edit' ] ); + + $c->stash->{status_message} = _('Updated!'); + + # do this here otherwise lastupdate and confirmed times + # do not display correctly + $problem->discard_changes; + } + + $c->detach('edit_display'); +} + +=head2 edit_category + +Handles changing a problem's category and the complexity that comes with it. +Returns 1 if category changed, 0 if no change. + +=cut + +sub edit_category : Private { + my ($self, $c, $problem, $no_comment) = @_; + + if ((my $category = $c->get_param('category')) ne $problem->category) { + my $force_resend = $c->cobrand->call_hook('category_change_force_resend', $problem->category, $category); + my $disable_resend = $c->cobrand->call_hook('disable_resend'); + my $category_old = $problem->category; + $problem->category($category); + my @contacts = grep { $_->category eq $problem->category } @{$c->stash->{contacts}}; + my @new_body_ids = map { $_->body_id } @contacts; + # If the report has changed bodies (and not to a subset!) we need to resend it + my %old_map = map { $_ => 1 } @{$problem->bodies_str_ids}; + if (!$disable_resend && grep !$old_map{$_}, @new_body_ids) { + $problem->resend; + } + # If the send methods of the old/new contacts differ we need to resend the report + my @new_send_methods = uniq map { + ( $_->body->can_be_devolved && $_->send_method ) ? + $_->send_method : $_->body->send_method + ? $_->body->send_method + : $c->cobrand->_fallback_body_sender()->{method}; + } @contacts; + my %old_send_methods = map { $_ => 1 } split /,/, ($problem->send_method_used || "Email"); + if (!$disable_resend && grep !$old_send_methods{$_}, @new_send_methods) { + $problem->resend; + } + if ($force_resend) { + $problem->resend; + } + + $problem->bodies_str(join( ',', @new_body_ids )); + my $update_text = '*' . sprintf(_('Category changed from ‘%s’ to ‘%s’'), $category_old, $category) . '*'; + if ($no_comment) { + $c->stash->{update_text} = $update_text; + } else { + $problem->add_to_comments({ + text => $update_text, + 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 => 0, + }); + } + $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'category_change' ] ); + return 1; + } + return 0; +} + +=head2 edit_location + +Handles changing a problem's location and the complexity that comes with it. +For now, we reject the new location if the new location and old locations aren't +covered by the same body. + +Returns 2 if the new position (if any) is acceptable and changed, +1 if acceptable and unchanged, undef otherwise. + +NB: This must be called before edit_category, as that might modify +$problem->bodies_str. + +=cut + +sub edit_location : Private { + my ($self, $c, $problem) = @_; + + return 1 unless $c->forward('/location/determine_location_from_coords'); + + my ($lat, $lon) = map { Utils::truncate_coordinate($_) } $problem->latitude, $problem->longitude; + if ( $c->stash->{latitude} != $lat || $c->stash->{longitude} != $lon ) { + # The two actions below change the stash, setting things up for e.g. a + # new report. But here we're only doing it in order to check the found + # bodies match; we don't want to overwrite the existing report data if + # this lookup is bad. So let's save the stash and restore it after the + # comparison. + my $safe_stash = { %{$c->stash} }; + $c->stash->{fetch_all_areas} = 1; + $c->stash->{area_check_action} = 'admin'; + $c->forward('/council/load_and_check_areas', []); + $c->forward('/report/new/setup_categories_and_bodies'); + my %allowed_bodies = map { $_ => 1 } @{$problem->bodies_str_ids}; + my @new_bodies = keys %{$c->stash->{bodies_to_list}}; + my $bodies_match = grep { exists( $allowed_bodies{$_} ) } @new_bodies; + $c->stash($safe_stash); + return unless $bodies_match; + $problem->latitude($c->stash->{latitude}); + $problem->longitude($c->stash->{longitude}); + my $areas = $c->stash->{all_areas_mapit}; + $problem->areas( ',' . join( ',', sort keys %$areas ) . ',' ); + return 2; + } + return 1; +} + +sub categories_for_point : Private { + my ($self, $c) = @_; + + $c->stash->{report} = $c->stash->{problem}; + # We have a report, stash its location + $c->forward('/report/new/determine_location_from_report'); + # Look up the areas for this location + my $prefetched_all_areas = [ grep { $_ } split ',', $c->stash->{report}->areas ]; + $c->forward('/around/check_location_is_acceptable', [ $prefetched_all_areas ]); + # As with a new report, fetch the bodies/categories + $c->stash->{categories_for_point} = 1; + $c->forward('/report/new/setup_categories_and_bodies'); + + # Remove the "Pick a category" option + shift @{$c->stash->{category_options}} if @{$c->stash->{category_options}}; + + $c->stash->{categories_hash} = { map { $_->category => 1 } @{$c->stash->{category_options}} }; + + $c->forward('/admin/triage/setup_categories'); + +} + +sub alerts_for_report : Private { + my ($self, $c) = @_; + + $c->stash->{alert_count} = $c->model('DB::Alert')->search({ + alert_type => 'new_updates', + parameter => $c->stash->{report}->id, + confirmed => 1, + whendisabled => undef, + })->count(); +} + +sub trim { + my $self = shift; + my $e = shift; + $e =~ s/^\s+//; + $e =~ s/\s+$//; + return $e; +} + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm b/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm index 2613f6ae0..5e2908290 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm @@ -4,98 +4,94 @@ use namespace::autoclean; BEGIN { extends 'Catalyst::Controller'; } +use FixMyStreet::App::Form::ResponsePriority; -sub index : Path : Args(0) { - my ( $self, $c ) = @_; +sub auto :Private { + my ($self, $c) = @_; my $user = $c->user; - if ($user->is_superuser) { - $c->forward('/admin/fetch_all_bodies'); - } elsif ( $user->from_body ) { - $c->forward('load_user_body', [ $user->from_body->id ]); - $c->res->redirect( $c->uri_for( '', $c->stash->{body}->id ) ); - } else { - $c->detach( '/page_error_404_not_found' ); + $c->stash(rs => $c->model('DB::ResponsePriority')->search_rs(undef, { + prefetch => 'body', + order_by => ['body.name', 'me.name'] + })); + } elsif ($user->from_body) { + $c->stash(rs => $user->from_body->response_priorities->search_rs(undef, { + order_by => 'name' + })); } } -sub list : Path : Args(1) { - my ($self, $c, $body_id) = @_; - - $c->forward('load_user_body', [ $body_id ]); +sub index : Path : Args(0) { + my ( $self, $c ) = @_; - my @priorities = $c->stash->{body}->response_priorities->search( - undef, - { - order_by => 'name' - } + if (my $body_id = $c->get_param('body_id')) { + $c->res->redirect($c->uri_for($self->action_for('create'), [ $body_id ])); + $c->detach; + } + if ($c->user->is_superuser) { + $c->forward('/admin/fetch_all_bodies'); + } + $c->stash( + response_priorities => [ $c->stash->{rs}->all ], ); - - $c->stash->{response_priorities} = \@priorities; } -sub edit : Path : Args(2) { - my ( $self, $c, $body_id, $priority_id ) = @_; - - $c->forward('load_user_body', [ $body_id ]); +sub body :PathPart('admin/responsepriorities') :Chained :CaptureArgs(1) { + my ($self, $c, $body_id) = @_; - my $priority; - if ($priority_id eq 'new') { - $priority = $c->stash->{body}->response_priorities->new({}); - } - else { - $priority = $c->stash->{body}->response_priorities->find( $priority_id ) - or $c->detach( '/page_error_404_not_found' ); + my $user = $c->user; + if ($user->is_superuser) { + $c->stash->{body} = $c->model('DB::Body')->find($body_id); + } elsif ($user->from_body && $user->from_body->id == $body_id) { + $c->stash->{body} = $user->from_body; } - $c->forward('/admin/fetch_contacts'); - my @contacts = $priority->contacts->all; - my @live_contacts = $c->stash->{live_contacts}->all; - my %active_contacts = map { $_->id => 1 } @contacts; - my @all_contacts = map { { - id => $_->id, - category => $_->category, - active => $active_contacts{$_->id}, - } } @live_contacts; - $c->stash->{contacts} = \@all_contacts; - - if ($c->req->method eq 'POST') { - $priority->deleted( $c->get_param('deleted') ? 1 : 0 ); - $priority->name( $c->get_param('name') ); - $priority->description( $c->get_param('description') ); - $priority->external_id( $c->get_param('external_id') ); - $priority->is_default( $c->get_param('is_default') ? 1 : 0 ); - $priority->update_or_insert; - - my @live_contact_ids = map { $_->id } @live_contacts; - my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @live_contact_ids; - $priority->contact_response_priorities->search({ - contact_id => { '!=' => \@new_contact_ids }, - })->delete; - foreach my $contact_id (@new_contact_ids) { - $priority->contact_response_priorities->find_or_create({ - contact_id => $contact_id, - }); - } - - $c->res->redirect( $c->uri_for( '', $c->stash->{body}->id ) ); - } + $c->detach( '/page_error_404_not_found' ) unless $c->stash->{body}; +} - $c->stash->{response_priority} = $priority; +sub create :Chained('body') :Args(0) { + my ($self, $c) = @_; + + my $priority = $c->stash->{rs}->new_result({ body => $c->stash->{body} }); + return $self->form($c, $priority); } -sub load_user_body : Private { - my ($self, $c, $body_id) = @_; +sub item :PathPart('') :Chained('body') :CaptureArgs(1) { + my ($self, $c, $id) = @_; - my $has_permission = $c->user->has_body_permission_to('responsepriority_edit', $body_id); + my $obj = $c->stash->{rs}->find($id) + or $c->detach('/page_error_404_not_found', []); + $c->stash(obj => $obj); +} - unless ( $has_permission ) { - $c->detach( '/page_error_404_not_found' ); - } +sub edit :PathPart('') :Chained('item') :Args(0) { + my ($self, $c) = @_; + return $self->form($c, $c->stash->{obj}); +} + +sub form { + my ($self, $c, $priority) = @_; - $c->stash->{body} = $c->model('DB::Body')->find($body_id) - or $c->detach( '/page_error_404_not_found' ); + # Otherwise, the form includes contacts for *all* bodies + $c->forward('/admin/fetch_contacts'); + my @all_contacts = map { + { value => $_->id, label => $_->category } + } $c->stash->{live_contacts}->all; + + my $opts = { + field_list => [ + '+contacts' => { options => \@all_contacts }, + ], + body_id => $c->stash->{body}->id, + }; + + my $form = FixMyStreet::App::Form::ResponsePriority->new(%$opts); + $c->stash(template => 'admin/responsepriorities/edit.html', form => $form); + $form->process(item => $priority, params => $c->req->params); + return unless $form->validated; + + $c->response->redirect($c->uri_for($self->action_for('index'))); } __PACKAGE__->meta->make_immutable; diff --git a/perllib/FixMyStreet/App/Controller/Admin/Roles.pm b/perllib/FixMyStreet/App/Controller/Admin/Roles.pm new file mode 100644 index 000000000..279ee695c --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/Roles.pm @@ -0,0 +1,102 @@ +package FixMyStreet::App::Controller::Admin::Roles; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +use FixMyStreet::App::Form::Role; + +sub auto :Private { + my ($self, $c) = @_; + + my $user = $c->user; + if ($user->is_superuser) { + $c->stash(rs => $c->model('DB::Role')->search_rs({}, { + prefetch => 'body', + order_by => ['body.name', 'me.name'] + })); + } elsif ($user->from_body) { + $c->stash(rs => $user->from_body->roles->search_rs({}, { order_by => 'name' })); + } +} + +sub index :Path :Args(0) { + my ($self, $c) = @_; + + my $p = $c->cobrand->available_permissions; + my %labels; + foreach my $group (sort keys %$p) { + my $group_vals = $p->{$group}; + foreach (sort keys %$group_vals) { + $labels{$_} = $group_vals->{$_}; + } + } + + $c->stash( + roles => [ $c->stash->{rs}->all ], + labels => \%labels, + ); +} + +sub create :Local :Args(0) { + my ($self, $c) = @_; + + my $role = $c->stash->{rs}->new_result({}); + return $self->form($c, $role); +} + +sub item :PathPart('admin/roles') :Chained :CaptureArgs(1) { + my ($self, $c, $id) = @_; + + my $obj = $c->stash->{rs}->find($id) + or $c->detach('/page_error_404_not_found', []); + $c->stash(obj => $obj); +} + +sub edit :PathPart('') :Chained('item') :Args(0) { + my ($self, $c) = @_; + return $self->form($c, $c->stash->{obj}); +} + +sub form { + my ($self, $c, $role) = @_; + + if ($c->get_param('delete_role')) { + $role->delete; + $c->forward('/admin/log_edit', [ $role->id, 'role', 'delete' ]); + $c->response->redirect($c->uri_for($self->action_for('index'))); + $c->detach; + } + + my $perms = []; + my $p = $c->cobrand->available_permissions; + foreach my $group (sort keys %$p) { + my $group_vals = $p->{$group}; + my @foo; + foreach (sort keys %$group_vals) { + push @foo, { value => $_, label => $group_vals->{$_} }; + } + push @$perms, { group => $group, options => \@foo }; + } + my $opts = { + field_list => [ + '+permissions' => { options => $perms }, + ], + }; + + if (!$c->user->is_superuser && $c->user->from_body) { + push @{$opts->{field_list}}, '+body', { inactive => 1 }; + $opts->{body_id} = $c->user->from_body->id; + } + + my $action = $role->in_storage ? 'edit' : 'add'; + my $form = FixMyStreet::App::Form::Role->new(%$opts); + $c->stash(template => 'admin/roles/form.html', form => $form); + $form->process(item => $role, params => $c->req->params); + return unless $form->validated; + + $c->forward('/admin/log_edit', [ $role->id, 'role', $action ]); + $c->response->redirect($c->uri_for($self->action_for('index'))); +} + +1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/Stats.pm b/perllib/FixMyStreet/App/Controller/Admin/Stats.pm index 5f82094d6..03b529a55 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/Stats.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/Stats.pm @@ -7,6 +7,52 @@ BEGIN { extends 'Catalyst::Controller'; } sub index : Path : Args(0) { my ( $self, $c ) = @_; return $c->cobrand->admin_stats() if $c->cobrand->moniker eq 'zurich'; + $c->forward('gather'); +} + +sub gather : Private { + my ($self, $c) = @_; + + $c->forward('state'); # Problem/update stats used on that page + $c->forward('/admin/fetch_all_bodies'); # For body stat + + my $alerts = $c->model('DB::Alert')->summary_report_alerts( $c->cobrand->restriction ); + + my %alert_counts = + map { $_->confirmed => $_->get_column('confirmed_count') } $alerts->all; + + $alert_counts{0} ||= 0; + $alert_counts{1} ||= 0; + + $c->stash->{alerts} = \%alert_counts; + + my $contacts = $c->model('DB::Contact')->summary_count(); + + my %contact_counts = + map { $_->state => $_->get_column('state_count') } $contacts->all; + + $contact_counts{confirmed} ||= 0; + $contact_counts{unconfirmed} ||= 0; + $contact_counts{total} = $contact_counts{confirmed} + $contact_counts{unconfirmed}; + + $c->stash->{contacts} = \%contact_counts; + + my $questionnaires = $c->model('DB::Questionnaire')->summary_count( $c->cobrand->restriction ); + + my %questionnaire_counts = map { + $_->get_column('answered') => $_->get_column('questionnaire_count') + } $questionnaires->all; + $questionnaire_counts{1} ||= 0; + $questionnaire_counts{0} ||= 0; + + $questionnaire_counts{total} = + $questionnaire_counts{0} + $questionnaire_counts{1}; + $c->stash->{questionnaires_pc} = + $questionnaire_counts{total} + ? sprintf( '%.1f', + $questionnaire_counts{1} / $questionnaire_counts{total} * 100 ) + : _('n/a'); + $c->stash->{questionnaires} = \%questionnaire_counts; } sub state : Local : Args(0) { diff --git a/perllib/FixMyStreet/App/Controller/Admin/Templates.pm b/perllib/FixMyStreet/App/Controller/Admin/Templates.pm new file mode 100644 index 000000000..efff1b488 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/Templates.pm @@ -0,0 +1,181 @@ +package FixMyStreet::App::Controller::Admin::Templates; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +=head1 NAME + +FixMyStreet::App::Controller::Admin::Templates - Catalyst Controller + +=head1 DESCRIPTION + +Admin pages for response templates + +=head1 METHODS + +=cut + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + + my $user = $c->user; + + if ($user->is_superuser) { + $c->forward('/admin/fetch_all_bodies'); + } elsif ( $user->from_body ) { + $c->forward('load_template_body', [ $user->from_body->id ]); + $c->res->redirect( $c->uri_for_action( '/admin/templates/view', $c->stash->{body}->id ) ); + } else { + $c->detach( '/page_error_404_not_found', [] ); + } +} + +sub view : Path : Args(1) { + my ($self, $c, $body_id) = @_; + + $c->forward('load_template_body', [ $body_id ]); + + my @templates = $c->stash->{body}->response_templates->search( + undef, + { + order_by => 'title' + } + ); + + $c->stash->{response_templates} = \@templates; +} + +sub edit : Path : Args(2) { + my ( $self, $c, $body_id, $template_id ) = @_; + + $c->forward('load_template_body', [ $body_id ]); + + my $template; + if ($template_id eq 'new') { + $template = $c->stash->{body}->response_templates->new({}); + } + else { + $template = $c->stash->{body}->response_templates->find( $template_id ) + or $c->detach( '/page_error_404_not_found', [] ); + } + + $c->forward('/admin/fetch_contacts'); + my @contacts = $template->contacts->all; + my @live_contacts = $c->stash->{live_contacts}->all; + my %active_contacts = map { $_->id => 1 } @contacts; + my @all_contacts = map { { + id => $_->id, + category => $_->category_display, + active => $active_contacts{$_->id}, + email => $_->email, + group => $_->get_extra_metadata('group') // '', + } } @live_contacts; + $c->stash->{contacts} = \@all_contacts; + $c->forward('/report/stash_category_groups', [ \@all_contacts, 1 ]) if $c->cobrand->enable_category_groups; + + # bare block to use 'last' if form is invalid. + if ($c->req->method eq 'POST') { { + if ($c->get_param('delete_template') && $c->get_param('delete_template') eq _("Delete template")) { + $template->contact_response_templates->delete_all; + $template->delete; + $c->forward('/admin/log_edit', [ $template->id, 'template', 'delete' ]); + } else { + my @live_contact_ids = map { $_->id } @live_contacts; + my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @live_contact_ids; + my %new_contacts = map { $_ => 1 } @new_contact_ids; + for my $contact (@all_contacts) { + $contact->{active} = $new_contacts{$contact->{id}}; + } + + $template->title( $c->get_param('title') ); + my $query = { title => $template->title }; + if ($template->in_storage) { + $query->{id} = { '!=', $template->id }; + } + if ($c->stash->{body}->response_templates->search($query)->count) { + $c->stash->{errors} ||= {}; + $c->stash->{errors}->{title} = _("There is already a template with that title."); + } + + $template->text( $c->get_param('text') ); + $template->state( $c->get_param('state') ); + $template->external_status_code( $c->get_param('external_status_code') ); + + if ( $template->state && $template->external_status_code ) { + $c->stash->{errors} ||= {}; + $c->stash->{errors}->{state} = _("State and external status code cannot be used simultaneously."); + $c->stash->{errors}->{external_status_code} = _("State and external status code cannot be used simultaneously."); + } + + $template->auto_response( $c->get_param('auto_response') && ( $template->state || $template->external_status_code ) ? 1 : 0 ); + if ($template->auto_response) { + my @check_contact_ids = @new_contact_ids; + # If the new template has not specific categories (i.e. it + # applies to all categories) then we need to check each of those + # category ids for existing auto-response templates. + if (!scalar @check_contact_ids) { + @check_contact_ids = @live_contact_ids; + } + my $query = { + 'auto_response' => 1, + 'contact.id' => [ @check_contact_ids, undef ], + -or => { + $template->state ? ('me.state' => $template->state) : (), + $template->external_status_code ? ('me.external_status_code' => $template->external_status_code) : (), + }, + }; + if ($template->in_storage) { + $query->{'me.id'} = { '!=', $template->id }; + } + if ($c->stash->{body}->response_templates->search($query, { + join => { 'contact_response_templates' => 'contact' }, + })->count) { + $c->stash->{errors} ||= {}; + $c->stash->{errors}->{auto_response} = _("There is already an auto-response template for this category/state."); + } + } + + last if $c->stash->{errors}; + + $template->update_or_insert; + $template->contact_response_templates->search({ + contact_id => { -not_in => \@new_contact_ids }, + })->delete; + foreach my $contact_id (@new_contact_ids) { + $template->contact_response_templates->find_or_create({ + contact_id => $contact_id, + }); + } + my $action = $template_id eq 'new' ? 'add' : 'edit'; + $c->forward('/admin/log_edit', [ $template->id, 'template', $action ]); + } + + $c->res->redirect( $c->uri_for_action( '/admin/templates/view', $c->stash->{body}->id ) ); + } } + + $c->stash->{response_template} = $template; +} + +sub load_template_body : Private { + my ($self, $c, $body_id) = @_; + + my $zurich_user = $c->user->from_body && $c->cobrand->moniker eq 'zurich'; + my $has_permission = $c->user->has_body_permission_to('template_edit', $body_id); + + unless ( $zurich_user || $has_permission ) { + $c->detach( '/page_error_404_not_found', [] ); + } + + # Regular users can only view their own body's templates + if ( !$c->user->is_superuser && $body_id ne $c->user->from_body->id ) { + $c->res->redirect( $c->uri_for_action( '/admin/templates/view', $c->user->from_body->id ) ); + } + + $c->stash->{body} = $c->model('DB::Body')->find($body_id) + or $c->detach( '/page_error_404_not_found', [] ); +} + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/Triage.pm b/perllib/FixMyStreet/App/Controller/Admin/Triage.pm new file mode 100644 index 000000000..428c35073 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/Triage.pm @@ -0,0 +1,163 @@ +package FixMyStreet::App::Controller::Admin::Triage; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +=head1 NAME + +FixMyStreet::App::Controller::Admin::Triage - Catalyst Controller + +=head1 DESCRIPTION + +Admin pages for triaging reports. + +This allows reports to be triaged before being sent to the council. It works +by having a set of categories with a send_method of Triage which sets the report +state to 'for_triage'. Any reports with the state are then show on '/admin/triage' +which is available to users with the 'triage' permission. + +Clicking on reports on this list will then allow a user to change the category of +the report to one that has an alternative send method, which will trigger the report +to be resent. + +In order for this to work additional work needs to be done to the cobrand to only +display triageable categories to the user. + +=head1 METHODS + +=cut + +sub auto : Private { + my ( $self, $c ) = @_; + + unless ( $c->user->has_body_permission_to('triage') ) { + $c->detach('/page_error_403_access_denied', []); + } +} + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + + # default sort to oldest + unless ( $c->get_param('sort') ) { + $c->set_param('sort', 'created-asc'); + } + $c->stash->{body} = $c->forward('/reports/body_find', [ $c->cobrand->council_area ]); + $c->forward( 'stash_report_filter_status' ); + $c->forward('/reports/stash_report_sort', [ $c->cobrand->reports_ordering ]); + $c->forward( '/reports/load_and_group_problems' ); + $c->stash->{page} = 'reports'; # So the map knows to make clickable pins + + if ($c->get_param('ajax')) { + my $ajax_template = $c->stash->{ajax_template} || 'reports/_problem-list.html'; + $c->detach('/reports/ajax', [ $ajax_template ]); + } + + my @categories = $c->stash->{body}->contacts->not_deleted->search( undef, { + columns => [ 'id', 'category', 'extra' ], + distinct => 1, + } )->all_sorted; + $c->stash->{filter_categories} = \@categories; + $c->stash->{filter_category} = { map { $_ => 1 } $c->get_param_list('filter_category', 1) }; + my $pins = $c->stash->{pins} || []; + + my %map_params = ( + latitude => @$pins ? $pins->[0]{latitude} : 0, + longitude => @$pins ? $pins->[0]{longitude} : 0, + area => [ $c->stash->{wards} ? map { $_->{id} } @{$c->stash->{wards}} : keys %{$c->stash->{body}->areas} ], + any_zoom => 1, + ); + FixMyStreet::Map::display_map( + $c, %map_params, pins => $pins, + ); +} + +sub stash_report_filter_status : Private { + my ( $self, $c ) = @_; + $c->stash->{filter_problem_states} = { 'for triage' => 1 }; + return 1; +} + +sub setup_categories : Private { + my ( $self, $c ) = @_; + + if ( $c->stash->{problem}->state eq 'for triage' ) { + $c->stash->{holding_options} = [ grep { $_->send_method && $_->send_method eq 'Triage' } @{$c->stash->{category_options}} ]; + $c->stash->{holding_categories} = { map { $_->category => 1 } @{$c->stash->{holding_options}} }; + $c->stash->{end_options} = [ grep { !$_->send_method || $_->send_method ne 'Triage' } @{$c->stash->{category_options}} ]; + $c->stash->{end_categories} = { map { $_->category => 1 } @{$c->stash->{end_options}} }; + delete $c->stash->{categories_hash}; + my %category_groups = (); + for my $category (@{$c->stash->{end_options}}) { + my $group = $category->{group} // $category->get_extra_metadata('group') // ['']; + # this could be an array ref or a string + my @groups = ref $group eq 'ARRAY' ? @$group : ($group); + push( @{$category_groups{$_}}, $category ) for @groups; + } + my @category_groups = (); + for my $group ( grep { $_ ne _('Other') } sort keys %category_groups ) { + push @category_groups, { name => $group, categories => $category_groups{$group} }; + } + $c->stash->{end_groups} = \@category_groups; + } + + return 1; +} + +sub update : Private { + my ($self, $c) = @_; + + my $problem = $c->stash->{problem}; + + my $current_category = $problem->category; + my $new_category = $c->get_param('category'); + + my $changed = $c->forward('/admin/reports/edit_category', [ $problem, 1 ] ); + + if ( $changed ) { + $c->stash->{problem}->update( { state => 'confirmed' } ); + $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'triage' ] ); + + my $name = $c->user->moderating_user_name; + my $extra = { is_superuser => 1 }; + if ($c->user->from_body) { + delete $extra->{is_superuser}; + $extra->{is_body_user} = $c->user->from_body->id; + } + + $extra->{triage_report} = 1; + $extra->{holding_category} = $current_category; + $extra->{new_category} = $new_category; + + my $timestamp = \'current_timestamp'; + my $comment = $problem->add_to_comments( { + text => "Report triaged from $current_category to $new_category", + created => $timestamp, + confirmed => $timestamp, + user_id => $c->user->id, + name => $name, + mark_fixed => 0, + anonymous => 0, + state => 'confirmed', + problem_state => $problem->state, + extra => $extra, + whensent => \'current_timestamp', + } ); + + my @alerts = FixMyStreet::DB->resultset('Alert')->search( { + alert_type => 'new_updates', + parameter => $problem->id, + confirmed => 1, + } ); + + for my $alert (@alerts) { + my $alerts_sent = FixMyStreet::DB->resultset('AlertSent')->find_or_create( { + alert_id => $alert->id, + parameter => $comment->id, + } ); + } + } +} + +1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/Users.pm b/perllib/FixMyStreet/App/Controller/Admin/Users.pm index bcbc808ed..046e19126 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/Users.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/Users.pm @@ -27,37 +27,69 @@ Admin pages for editing users sub index :Path : Args(0) { my ( $self, $c ) = @_; - $c->detach('add') if $c->req->method eq 'POST'; # Add a user - - if (my $search = $c->get_param('search')) { - $search = $self->trim($search); - $search =~ s/^<(.*)>$/$1/; # In case email wrapped in <...> - $c->stash->{searched} = $search; - - my $isearch = '%' . $search . '%'; - my $search_n = 0; - $search_n = int($search) if $search =~ /^\d+$/; - - my $users = $c->cobrand->users->search( - { - -or => [ - email => { ilike => $isearch }, - phone => { ilike => $isearch }, - name => { ilike => $isearch }, - from_body => $search_n, - ] + if ($c->req->method eq 'POST') { + my @uids = $c->get_param_list('uid'); + my @role_ids = $c->get_param_list('roles'); + my $user_rs = FixMyStreet::DB->resultset("User")->search({ id => \@uids }); + foreach my $user ($user_rs->all) { + $user->admin_user_body_permissions->delete; + $user->user_roles->search({ + role_id => { -not_in => \@role_ids }, + })->delete; + foreach my $role (@role_ids) { + $user->user_roles->find_or_create({ + role_id => $role, + }); } - ); + } + $c->stash->{status_message} = _('Updated!'); + } + + my $search = $c->get_param('search'); + my $role = $c->get_param('role'); + if ($search || $role) { + my $users = $c->cobrand->users; + my $isearch; + if ($search) { + $search = $self->trim($search); + $search =~ s/^<(.*)>$/$1/; # In case email wrapped in <...> + $c->stash->{searched} = $search; + + $isearch = '%' . $search . '%'; + my $search_n = 0; + $search_n = int($search) if $search =~ /^\d+$/; + + $users = $users->search( + { + -or => [ + email => { ilike => $isearch }, + phone => { ilike => $isearch }, + name => { ilike => $isearch }, + from_body => $search_n, + ] + } + ); + } + if ($role) { + $c->stash->{role_selected} = $role; + $users = $users->search({ + role_id => $role, + }, { + join => 'user_roles', + }); + } + my @users = $users->all; $c->stash->{users} = [ @users ]; - $c->forward('/admin/add_flags', [ { email => { ilike => $isearch } } ]); + if ($search) { + $c->forward('/admin/add_flags', [ { email => { ilike => $isearch } } ]); + } } else { $c->forward('/auth/get_csrf_token'); $c->forward('/admin/fetch_all_bodies'); $c->cobrand->call_hook('admin_user_edit_extra_data'); - # Admin users by default my $users = $c->cobrand->users->search( { from_body => { '!=', undef } }, @@ -67,6 +99,14 @@ sub index :Path : Args(0) { $c->stash->{users} = \@users; } + my $rs; + if ($c->user->is_superuser) { + $rs = $c->model('DB::Role')->search_rs({}, { join => 'body', order_by => ['body.name', 'me.name'] }); + } elsif ($c->user->from_body) { + $rs = $c->user->from_body->roles->search_rs({}, { order_by => 'name' }); + } + $c->stash->{roles} = [ $rs->all ]; + return 1; } @@ -113,9 +153,7 @@ sub add : Local : Args(0) { $c->stash->{field_errors}->{username} = _('User already exists'); } - return if %{$c->stash->{field_errors}}; - - my $user = $c->model('DB::User')->create( { + my $user = $c->model('DB::User')->new( { name => $c->get_param('name'), email => $email ? $email : undef, email_verified => $email && $email_v ? 1 : 0, @@ -127,28 +165,48 @@ sub add : Local : Args(0) { is_superuser => ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0, } ); $c->stash->{user} = $user; + + return if %{$c->stash->{field_errors}}; + $c->forward('user_cobrand_extra_fields'); - $user->update; + $user->insert; - $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'add' ] ); $c->flash->{status_message} = _("Updated!"); - $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $user->id ) ); + $c->detach('post_edit_redirect', [ $user ]); } -sub edit : Path : Args(1) { - my ( $self, $c, $id ) = @_; +sub fetch_body_roles : Private { + my ($self, $c, $body ) = @_; - $c->forward('/auth/get_csrf_token'); + my $roles = $body->roles->search(undef, { order_by => 'name' }); + unless ($roles) { + delete $c->stash->{roles}; # Body doesn't have any roles + return; + } + + $c->stash->{roles} = [ $roles->all ]; +} + +sub user : Chained('/') PathPart('admin/users') : CaptureArgs(1) { + my ( $self, $c, $id ) = @_; my $user = $c->cobrand->users->find( { id => $id } ); $c->detach( '/page_error_404_not_found', [] ) unless $user; + $c->stash->{user} = $user; unless ( $c->user->has_body_permission_to('user_edit') || $c->cobrand->moniker eq 'zurich' ) { $c->detach('/page_error_403_access_denied', []); } +} - $c->stash->{user} = $user; +sub edit : Chained('user') : PathPart('') : Args(0) { + my ( $self, $c ) = @_; + + $c->forward('/auth/get_csrf_token'); + + my $user = $c->stash->{user}; $c->forward( '/admin/check_username_for_abuse', [ $user ] ); if ( $user->from_body && $c->user->has_permission_to('user_manage_permissions', $user->from_body->id) ) { @@ -157,11 +215,11 @@ sub edit : Path : Args(1) { $c->forward('/admin/fetch_all_bodies'); $c->forward('/admin/fetch_body_areas', [ $user->from_body ]) if $user->from_body; + $c->forward('fetch_body_roles', [ $user->from_body ]) if $user->from_body; $c->cobrand->call_hook('admin_user_edit_extra_data'); if ( defined $c->flash->{status_message} ) { - $c->stash->{status_message} = - '<p><em>' . $c->flash->{status_message} . '</em></p>'; + $c->stash->{status_message} = $c->flash->{status_message}; } $c->forward('/auth/check_csrf_token') if $c->get_param('submit'); @@ -179,14 +237,12 @@ sub edit : Path : Args(1) { } elsif ( $c->get_param('submit') and $c->get_param('send_login_email') ) { my $email = lc $c->get_param('email'); my %args = ( email => $email ); - $args{user_id} = $id if $user->email ne $email || !$user->email_verified; + $args{user_id} = $user->id if $user->email ne $email || !$user->email_verified; $c->forward('send_login_email', [ \%args ]); } elsif ( $c->get_param('update_alerts') ) { $c->forward('update_alerts'); } elsif ( $c->get_param('submit') ) { - my $edited = 0; - my $name = $c->get_param('name'); my $email = lc $c->get_param('email'); my $phone = $c->get_param('phone'); @@ -228,19 +284,10 @@ sub edit : Path : Args(1) { return if %{$c->stash->{field_errors}}; - if ( ($user->email || "") ne $email || - $user->name ne $name || - ($user->phone || "") ne $phone || - ($user->from_body && $c->get_param('body') && $user->from_body->id ne $c->get_param('body')) || - (!$user->from_body && $c->get_param('body')) - ) { - $edited = 1; - } - if ($existing_user_cobrand) { $existing_user->adopt($user); - $c->forward( '/admin/log_edit', [ $id, 'user', 'merge' ] ); - return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $existing_user->id ) ); + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'merge' ] ); + return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', [ $existing_user->id ] ) ); } $user->email($email) if !$existing_email; @@ -270,26 +317,45 @@ sub edit : Path : Args(1) { # If so, we need to re-fetch areas so the UI is up to date. if ( $user->from_body && $user->from_body->id ne $c->stash->{fetched_areas_body_id} ) { $c->forward('/admin/fetch_body_areas', [ $user->from_body ]); + $c->forward('fetch_body_roles', [ $user->from_body ]); } if (!$user->from_body) { # Non-staff users aren't allowed any permissions or to be in an area $user->admin_user_body_permissions->delete; + $user->user_roles->delete; $user->area_ids(undef); delete $c->stash->{areas}; + delete $c->stash->{roles}; delete $c->stash->{fetched_areas_body_id}; } elsif ($c->stash->{available_permissions}) { - my @all_permissions = map { keys %$_ } values %{ $c->stash->{available_permissions} }; - my @user_permissions = grep { $c->get_param("permissions[$_]") ? 1 : undef } @all_permissions; - $user->admin_user_body_permissions->search({ - body_id => $user->from_body->id, - permission_type => { '!=' => \@user_permissions }, - })->delete; - foreach my $permission_type (@user_permissions) { - $user->user_body_permissions->find_or_create({ + my %valid_roles = map { $_->id => 1 } @{$c->stash->{roles}}; + my @role_ids = grep { $valid_roles{$_} } $c->get_param_list('roles'); + if (@role_ids) { + # Roles take precedence over permissions + $user->admin_user_body_permissions->delete; + $user->user_roles->search({ + role_id => { -not_in => \@role_ids }, + })->delete; + foreach my $role (@role_ids) { + $user->user_roles->find_or_create({ + role_id => $role, + }); + } + } else { + $user->user_roles->delete; + my @all_permissions = map { keys %$_ } values %{ $c->stash->{available_permissions} }; + my @user_permissions = grep { $c->get_param("permissions[$_]") ? 1 : undef } @all_permissions; + $user->admin_user_body_permissions->search({ body_id => $user->from_body->id, - permission_type => $permission_type, - }); + permission_type => { -not_in => \@user_permissions }, + })->delete; + foreach my $permission_type (@user_permissions) { + $user->user_body_permissions->find_or_create({ + body_id => $user->from_body->id, + permission_type => $permission_type, + }); + } } } @@ -299,35 +365,6 @@ sub edit : Path : Args(1) { $user->area_ids( @area_ids ? \@area_ids : undef ); } - # Handle 'trusted' flag(s) - my @trusted_bodies = $c->get_param_list('trusted_bodies'); - if ( $c->user->is_superuser ) { - $user->user_body_permissions->search({ - body_id => { '!=' => \@trusted_bodies }, - permission_type => 'trusted', - })->delete; - foreach my $body_id (@trusted_bodies) { - $user->user_body_permissions->find_or_create({ - body_id => $body_id, - permission_type => 'trusted', - }); - } - } elsif ( $c->user->from_body ) { - my %trusted = map { $_ => 1 } @trusted_bodies; - my $body_id = $c->user->from_body->id; - if ( $trusted{$body_id} ) { - $user->user_body_permissions->find_or_create({ - body_id => $body_id, - permission_type => 'trusted', - }); - } else { - $user->user_body_permissions->search({ - body_id => $body_id, - permission_type => 'trusted', - })->delete; - } - } - # Update the categories this user operates in if ( $user->from_body ) { $c->stash->{body} = $user->from_body; @@ -336,14 +373,15 @@ sub edit : Path : Args(1) { my @live_contact_ids = map { $_->id } @live_contacts; my @new_contact_ids = grep { $c->get_param("contacts[$_]") } @live_contact_ids; $user->set_extra_metadata('categories', \@new_contact_ids); + } else { + $user->unset_extra_metadata('categories'); } $user->update; - if ($edited) { - $c->forward( '/admin/log_edit', [ $id, 'user', 'edit' ] ); - } + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); $c->flash->{status_message} = _("Updated!"); - return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $user->id ) ); + + $c->detach('post_edit_redirect', [ $user ]); } if ( $user->from_body ) { @@ -358,8 +396,10 @@ sub edit : Path : Args(1) { id => $_->id, category => $_->category, active => $active_contacts{$_->id}, + group => $_->get_extra_metadata('group') // '', } } @live_contacts; $c->stash->{contacts} = \@all_contacts; + $c->forward('/report/stash_category_groups', [ \@all_contacts, 1 ]) if $c->cobrand->enable_category_groups; } # this goes after in case we've delete any alerts @@ -370,6 +410,50 @@ sub edit : Path : Args(1) { 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 ) = @_; + + # User may not be visible on this cobrand, e.g. if their from_body + # wasn't set. + if ( $c->cobrand->users->find( { id => $user->id } ) ) { + return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', [ $user->id ] ) ); + } else { + return $c->res->redirect( $c->uri_for_action( 'admin/users/index' ) ); + } +} + sub import :Local { my ( $self, $c, $id ) = @_; @@ -387,11 +471,9 @@ sub import :Local { my $csv = Text::CSV->new({ binary => 1}); my $fh = $c->req->upload('csvfile')->fh; - $csv->getline($fh); # discard the header - while (my $row = $csv->getline($fh)) { - my ($name, $email, $from_body, $permissions) = @$row; - $email = lc Utils::trim_text($email); - my @permissions = split(/:/, $permissions); + $csv->header($fh); + while (my $row = $csv->getline_hr($fh)) { + my $email = lc Utils::trim_text($row->{email}); my $user = FixMyStreet::DB->resultset("User")->find_or_new({ email => $email, email_verified => 1 }); if ($user->in_storage) { @@ -399,16 +481,29 @@ sub import :Local { next; } - $user->name($name); - $user->from_body($from_body || undef); - $user->update_or_insert; + $user->name($row->{name}); + $user->from_body($row->{from_body} || undef); + $user->password($row->{passwordhash}, 1) if $row->{passwordhash}; + $user->insert; - my @user_permissions = grep { $available_permissions{$_} } @permissions; - foreach my $permission_type (@user_permissions) { - $user->user_body_permissions->find_or_create({ - body_id => $user->from_body->id, - permission_type => $permission_type, - }); + if ($row->{roles}) { + my @roles = split(/:/, $row->{roles}); + foreach my $role (@roles) { + $role = FixMyStreet::DB->resultset("Role")->find({ + body_id => $user->from_body->id, + name => $role, + }) or next; + $user->add_to_roles($role); + } + } else { + my @permissions = split(/:/, $row->{permissions}); + my @user_permissions = grep { $available_permissions{$_} } @permissions; + foreach my $permission_type (@user_permissions) { + $user->user_body_permissions->find_or_create({ + body_id => $user->from_body->id, + permission_type => $permission_type, + }); + } } push @{$c->stash->{new_users}}, $user; @@ -497,7 +592,7 @@ sub user_hide_everywhere : Private { my ( $self, $c, $user ) = @_; my $problems = $user->problems->search({ state => { '!=' => 'hidden' } }); while (my $problem = $problems->next) { - $problem->get_photoset->delete_cached; + $problem->get_photoset->delete_cached(plus_updates => 1); $problem->update({ state => 'hidden' }); } my $updates = $user->comments->search({ state => { '!=' => 'hidden' } }); @@ -538,6 +633,7 @@ sub user_remove_account : Private { my ( $self, $c, $user ) = @_; $c->forward('user_logout_everywhere', [ $user ]); $user->anonymize_account; + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); $c->stash->{status_message} = _('That user’s personal details have been removed.'); } @@ -565,6 +661,7 @@ sub ban : Private { $c->stash->{status_message} = _('User already in abuse list'); } else { $abuse->insert; + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); $c->stash->{status_message} = _('User added to abuse list'); } $c->stash->{username_in_abuse} = 1; @@ -575,6 +672,7 @@ sub ban : Private { $c->stash->{status_message} = _('User already in abuse list'); } else { $abuse->insert; + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); $c->stash->{status_message} = _('User added to abuse list'); } $c->stash->{username_in_abuse} = 1; @@ -596,6 +694,7 @@ sub unban : Private { my $abuse = $c->model('DB::Abuse')->search({ email => \@username }); if ( $abuse ) { $abuse->delete; + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); $c->stash->{status_message} = _('user removed from abuse list'); } else { $c->stash->{status_message} = _('user not in abuse list'); @@ -625,6 +724,7 @@ sub flag : Private { } else { $user->flagged(1); $user->update; + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); $c->stash->{status_message} = _('User flagged'); } @@ -654,6 +754,7 @@ sub flag_remove : Private { } else { $user->flagged(0); $user->update; + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); $c->stash->{status_message} = _('User flag removed'); } |