diff options
37 files changed, 1435 insertions, 1336 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 085fcecb1..c7d5d4bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,11 @@ - Delete cache photos upon photo moderation. #2374 - Remove any use of `my $x if $foo`. #2377 - Fix saving of inspect form data offline. - - Add CSRF and time to contact form. - - Make sure admin metadata dropdown index numbers are updated too. - - Fix issue with Open311 codes starting with ‘_’. - - Add parameter to URL when “Show older” clicked. + - Add CSRF and time to contact form. #2388 + - Make sure admin metadata dropdown index numbers are updated too. #2369 + - Fix issue with Open311 codes starting with ‘_’. #2391 + - Add parameter to URL when “Show older” clicked. #2397 + - Don't ask for email on alert signup if logged in. #2402 - Development improvements: - Make front page cache time configurable. - Better working of /fakemapit/ under https. diff --git a/locale/sv_SE.UTF-8/LC_MESSAGES/FixMyStreet.po b/locale/sv_SE.UTF-8/LC_MESSAGES/FixMyStreet.po index b89f239d6..34771d588 100644 --- a/locale/sv_SE.UTF-8/LC_MESSAGES/FixMyStreet.po +++ b/locale/sv_SE.UTF-8/LC_MESSAGES/FixMyStreet.po @@ -6,20 +6,22 @@ # Translators: # mySociety <transifex@mysociety.org>, 2018 # Jon Kristensen <info@jonkri.com>, 2018 +# Joe Siltberg <joe@fixamingata.se>, 2019 # msgid "" msgstr "" "Project-Id-Version: 1.0\n" "Report-Msgid-Bugs-To: matthew@mysociety.org\n" "POT-Creation-Date: 2018-12-19 17:14+0000\n" -"PO-Revision-Date: 2018-12-21 13:37+0000\n" -"Last-Translator: Jon Kristensen <info@jonkri.com>, 2018\n" +"PO-Revision-Date: 2019-02-21 12:33+0100\n" +"Last-Translator: Joe Siltberg <joe@fixamingata.se>, 2019\n" "Language-Team: Swedish (Sweden) (https://www.transifex.com/mysociety/teams/12067/sv_SE/)\n" "Language: sv_SE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.0.6\n" #: perllib/FixMyStreet/DB/Result/Problem.pm:678 #: perllib/FixMyStreet/DB/Result/Problem.pm:680 @@ -181,7 +183,7 @@ msgstr "(vi har också RSS-flöden för problem inom %s)" #: perllib/FixMyStreet/App/Controller/Report/New.pm:711 #: perllib/FixMyStreet/DB/Result/Problem.pm:386 msgid "-- Pick a category --" -msgstr "-- Välj en katagori --" +msgstr "-- Välj en kategori --" #: templates/web/base/report/new/category_extras_fields.html:18 msgid "-- Pick an option --" @@ -3335,7 +3337,7 @@ msgstr "Rapporterat tidigare" #: perllib/FixMyStreet/DB/Result/Problem.pm:640 #: templates/web/base/contact/index.html:60 msgid "Reported by %s at %s" -msgstr "Rapporterat av %s den %s" +msgstr "Rapporterat av %s, %s" #: templates/web/zurich/admin/report_edit-sdm.html:60 #: templates/web/zurich/admin/report_edit.html:88 @@ -3352,7 +3354,7 @@ msgstr "Anonym rapport i kategori %s, %s" #: perllib/FixMyStreet/DB/Result/Problem.pm:637 msgid "Reported in the %s category by %s at %s" -msgstr "Rapporterat i kategori %s av %s den %s" +msgstr "Rapporterat i kategori %s av %s, %s" #: perllib/FixMyStreet/DB/Result/Problem.pm:610 msgid "Reported via %s anonymously at %s" @@ -3360,7 +3362,7 @@ msgstr "Rapporterat av %s anonymt %s" #: perllib/FixMyStreet/DB/Result/Problem.pm:634 msgid "Reported via %s by %s at %s" -msgstr "Rapporterat av %s av %s den %s" +msgstr "Rapporterat av %s av %s, %s" #: perllib/FixMyStreet/DB/Result/Problem.pm:607 msgid "Reported via %s in the %s category anonymously at %s" @@ -3368,7 +3370,7 @@ msgstr "Rapporterat av %s i kategorin %s, anonymt %s" #: perllib/FixMyStreet/DB/Result/Problem.pm:629 msgid "Reported via %s in the %s category by %s at %s" -msgstr "Rapporterat av %s i kategorin %s av %s den %s" +msgstr "Rapporterat av %s i kategorin %s av %s, %s" #: templates/web/zurich/admin/report_edit-sdm.html:38 #: templates/web/zurich/admin/report_edit.html:57 diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index 84651ad07..0c37eeb27 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -190,318 +190,6 @@ sub timeline : Path( 'timeline' ) : Args(0) { return 1; } -sub bodies : Path('bodies') : Args(0) { - my ( $self, $c ) = @_; - - if (my $body_id = $c->get_param('body')) { - return $c->res->redirect( $c->uri_for( 'body', $body_id ) ); - } - - if (!$c->user->is_superuser && $c->user->from_body && $c->cobrand->moniker ne 'zurich') { - return $c->res->redirect( $c->uri_for( 'body', $c->user->from_body->id ) ); - } - - $c->forward( '/auth/get_csrf_token' ); - - my $edit_activity = $c->model('DB::ContactsHistory')->search( - undef, - { - select => [ 'editor', { count => 'contacts_history_id', -as => 'c' } ], - as => [ 'editor', 'c' ], - group_by => ['editor'], - order_by => { -desc => 'c' } - } - ); - - $c->stash->{edit_activity} = $edit_activity; - - $c->forward( 'fetch_languages' ); - $c->forward( 'fetch_translations' ); - - 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( 'fetch_all_bodies' ); - - my $contacts = $c->model('DB::Contact')->search( - undef, - { - select => [ 'body_id', { count => 'id' }, { count => \'case when state = \'deleted\' then 1 else null end' }, - { count => \'case when state = \'confirmed\' then 1 else null end' } ], - as => [qw/body_id c deleted confirmed/], - group_by => [ 'body_id' ], - result_class => 'DBIx::Class::ResultClass::HashRefInflator' - } - ); - - my %council_info = map { $_->{body_id} => $_ } $contacts->all; - - $c->stash->{counts} = \%council_info; - - $c->forward( 'body_form_dropdowns' ); - - return 1; -} - -sub body_form_dropdowns : Private { - my ( $self, $c ) = @_; - - my $areas; - my $whitelist = $c->config->{MAPIT_ID_WHITELIST}; - - if ( $whitelist && ref $whitelist eq 'ARRAY' && @$whitelist ) { - $areas = mySociety::MaPit::call('areas', $whitelist); - } else { - $areas = mySociety::MaPit::call('areas', $c->cobrand->area_types); - } - - # Some cobrands may want to add extra areas at runtime beyond those - # available via MAPIT_WHITELIST or MAPIT_TYPES. This can be used for, - # e.g., parish councils on a particular council cobrand. - $areas = $c->cobrand->call_hook("add_extra_areas" => $areas) || $areas; - - $c->stash->{areas} = [ sort { strcoll($a->{name}, $b->{name}) } values %$areas ]; - - my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders }; - $c->stash->{send_methods} = \@methods; -} - -sub check_for_super_user : Private { - my ( $self, $c ) = @_; - - my $superuser = $c->user->is_superuser; - # Zurich currently has its own way of defining superusers - $superuser ||= $c->cobrand->moniker eq 'zurich' && $c->stash->{admin_type} eq 'super'; - - unless ( $superuser ) { - $c->detach('/page_error_403_access_denied', []); - } -} - -sub update_contacts : Private { - my ( $self, $c ) = @_; - - my $posted = $c->get_param('posted'); - my $editor = $c->forward('get_user'); - - if ( $posted eq 'new' ) { - $c->forward('/auth/check_csrf_token'); - - my %errors; - - 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 $contact = $c->model('DB::Contact')->find_or_new( - { - body_id => $c->stash->{body_id}, - category => $category, - } - ); - - 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'; - } - - $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')) ); - } - if ( my $group = $c->get_param('group') ) { - $contact->set_extra_metadata( group => $group ); - } else { - $contact->unset_extra_metadata( 'group' ); - } - - - $c->forward('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'); - - # NB: History is automatically stored by a trigger in the database - $contact->update; - } else { - $c->stash->{updated} = _('New category contact added'); - $contact->insert; - } - - unless ( %errors ) { - $c->stash->{translation_col} = 'category'; - $c->stash->{object} = $contact; - $c->forward('update_translations'); - } - - } elsif ( $posted eq 'update' ) { - $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 }, - } - ); - - $contacts->update( - { - state => 'confirmed', - whenedited => \'current_timestamp', - note => 'Confirmed', - editor => $editor, - } - ); - - $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; - - $c->stash->{translation_col} = 'name'; - $c->stash->{object} = $c->stash->{body}; - $c->forward('update_translations'); - - $c->stash->{updated} = _('Values updated'); - } - } -} - -sub update_translations : Private { - my ( $self, $c ) = @_; - - foreach my $lang (keys(%{$c->stash->{languages}})) { - my $id = $c->get_param('translation_id_' . $lang); - my $text = $c->get_param('translation_' . $lang); - if ($id) { - my $translation = $c->model('DB::Translation')->find( - { - id => $id, - } - ); - - if ($text) { - $translation->msgstr($text); - $translation->update; - } else { - $translation->delete; - } - } elsif ($text) { - my $col = $c->stash->{translation_col}; - $c->stash->{object}->add_translation_for( - $col, $lang, $text - ); - } - } -} - -sub body_params : Private { - my ( $self, $c ) = @_; - - my @fields = qw/name endpoint jurisdiction api_key send_method external_url/; - my %defaults = map { $_ => '' } @fields; - %defaults = ( %defaults, - send_comments => 0, - fetch_problems => 0, - convert_latlong => 0, - blank_updates_permitted => 0, - suppress_alerts => 0, - comment_user_id => undef, - send_extended_statuses => 0, - can_be_devolved => 0, - parent => undef, - deleted => 0, - ); - my %params = map { $_ => $c->get_param($_) || $defaults{$_} } keys %defaults; - $c->forward('check_body_params', [ \%params ]); - my @extras = qw/fetch_all_problems/; - %defaults = map { $_ => '' } @extras; - my %extras = map { $_ => $c->get_param($_) || $defaults{$_} } @extras; - return { params => \%params, extras => \%extras }; -} - -sub check_body_params : Private { - my ( $self, $c, $params ) = @_; - - $c->stash->{body_errors} ||= {}; - - unless ($params->{name}) { - $c->stash->{body_errors}->{name} = _('Please enter a name for this body'); - } -} - sub fetch_contacts : Private { my ( $self, $c ) = @_; @@ -533,125 +221,6 @@ sub fetch_languages : Private { return 1; } -sub fetch_translations : Private { - my ( $self, $c ) = @_; - - my $translations = {}; - if ($c->get_param('posted')) { - foreach my $lang (keys %{$c->stash->{languages}}) { - if (my $msgstr = $c->get_param('translation_' . $lang)) { - $translations->{$lang} = { msgstr => $msgstr }; - } - if (my $id = $c->get_param('translation_id_' . $lang)) { - $translations->{$lang}->{id} = $id; - } - } - } elsif ($c->stash->{object}) { - my @translations = $c->stash->{object}->translation_for($c->stash->{translation_col})->all; - - foreach my $tx (@translations) { - $translations->{$tx->lang} = { id => $tx->id, msgstr => $tx->msgstr }; - } - } - - $c->stash->{translations} = $translations; -} - -sub lookup_body : Private { - my ( $self, $c, $body_id ) = @_; - - $c->stash->{body_id} = $body_id; - my $body = $c->model('DB::Body')->find($body_id); - $c->detach( '/page_error_404_not_found', [] ) - unless $body; - $c->stash->{body} = $body; -} - -sub body : Chained('/') : PathPart('admin/body') : CaptureArgs(1) { - my ( $self, $c, $body_id ) = @_; - - $c->forward('lookup_body'); - my $body = $c->stash->{body}; - - if ($body->body_areas->first) { - my $example_postcode = mySociety::MaPit::call('area/example_postcode', $body->body_areas->first->area_id); - if ($example_postcode && ! ref $example_postcode) { - $c->stash->{example_pc} = $example_postcode; - } - } -} - -sub edit_body : Chained('body') : PathPart('') : Args(0) { - my ( $self, $c ) = @_; - - unless ($c->user->has_permission_to('category_edit', $c->stash->{body_id})) { - $c->forward('check_for_super_user'); - } - - $c->forward( '/auth/get_csrf_token' ); - $c->forward( 'fetch_all_bodies' ); - $c->forward( 'body_form_dropdowns' ); - $c->forward('fetch_languages'); - - if ( $c->get_param('posted') ) { - $c->forward('update_contacts'); - } - - $c->stash->{object} = $c->stash->{body}; - $c->stash->{translation_col} = 'name'; - - # if there's a contact then it's because we're displaying error - # messages about adding a contact so grabbing translations will - # fetch the contact submitted translations. So grab them, stash - # them and then clear posted so we can fetch the body translations - if ($c->stash->{contact}) { - $c->forward('fetch_translations'); - $c->stash->{contact_translations} = $c->stash->{translations}; - } - $c->set_param('posted', ''); - - $c->forward('fetch_translations'); - - # don't set this last as fetch_contacts might over-ride it - # to display email addresses as text - $c->stash->{template} = 'admin/body.html'; - $c->forward('fetch_contacts'); - - return 1; -} - -sub category : Chained('body') : PathPart('') { - my ( $self, $c, @category ) = @_; - my $category = join( '/', @category ); - - $c->forward( '/auth/get_csrf_token' ); - $c->stash->{template} = 'admin/category_edit.html'; - - my $contact = $c->stash->{body}->contacts->search( { category => $category } )->first; - $c->stash->{contact} = $contact; - - $c->stash->{translation_col} = 'category'; - $c->stash->{object} = $c->stash->{contact}; - - $c->forward('fetch_languages'); - $c->forward('fetch_translations'); - - my $history = $c->model('DB::ContactsHistory')->search( - { - body_id => $c->stash->{body_id}, - category => $c->stash->{contact}->category - }, - { - order_by => ['contacts_history_id'] - }, - ); - $c->stash->{history} = $history; - my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders }; - $c->stash->{send_methods} = \@methods; - - return 1; -} - sub reports : Path('reports') { my ( $self, $c ) = @_; @@ -927,15 +496,15 @@ sub report_edit : Path('report_edit') : Args(1) { $c->forward( 'log_edit', [ $id, 'problem', 'marked sent' ] ); } elsif ( $c->get_param('flaguser') ) { - $c->forward('flag_user'); + $c->forward('users/flag'); $c->stash->{problem}->discard_changes; } elsif ( $c->get_param('removeuserflag') ) { - $c->forward('remove_user_flag'); + $c->forward('users/flag_remove'); $c->stash->{problem}->discard_changes; } elsif ( $c->get_param('banuser') ) { - $c->forward('ban_user'); + $c->forward('users/ban'); } elsif ( $c->get_param('submit') ) { $c->forward('/auth/check_csrf_token'); @@ -1292,50 +861,6 @@ sub load_template_body : Private { or $c->detach( '/page_error_404_not_found', [] ); } -sub users: Path('users') : Args(0) { - my ( $self, $c ) = @_; - - 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, - ] - } - ); - my @users = $users->all; - $c->stash->{users} = [ @users ]; - $c->forward('add_flags', [ { email => { ilike => $isearch } } ]); - - } else { - $c->forward('/auth/get_csrf_token'); - $c->forward('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 } }, - { order_by => 'name' } - ); - my @users = $users->all; - $c->stash->{users} = \@users; - } - - return 1; -} - sub update_edit : Path('update_edit') : Args(1) { my ( $self, $c, $id ) = @_; @@ -1356,14 +881,14 @@ sub update_edit : Path('update_edit') : Args(1) { $c->forward('check_username_for_abuse', [ $update->user ] ); if ( $c->get_param('banuser') ) { - $c->forward('ban_user'); + $c->forward('users/ban'); } elsif ( $c->get_param('flaguser') ) { - $c->forward('flag_user'); + $c->forward('users/flag'); $c->stash->{update}->discard_changes; } elsif ( $c->get_param('removeuserflag') ) { - $c->forward('remove_user_flag'); + $c->forward('users/flag_remove'); $c->stash->{update}->discard_changes; } elsif ( $c->get_param('submit') ) { @@ -1430,382 +955,6 @@ sub update_edit : Path('update_edit') : Args(1) { return 1; } -sub phone_check : Private { - my ($self, $c, $phone) = @_; - my $parsed = FixMyStreet::SMS->parse_username($phone); - if ($parsed->{phone} && $parsed->{may_be_mobile}) { - return $parsed->{username}; - } elsif ($parsed->{phone}) { - $c->stash->{field_errors}->{phone} = _('Please enter a mobile number'); - } else { - $c->stash->{field_errors}->{phone} = _('Please check your phone number is correct'); - } -} - -sub user_add : Path('user_edit') : Args(0) { - my ( $self, $c ) = @_; - - $c->stash->{template} = 'admin/user_edit.html'; - $c->forward('/auth/get_csrf_token'); - $c->forward('fetch_all_bodies'); - $c->cobrand->call_hook('admin_user_edit_extra_data'); - - return unless $c->get_param('submit'); - - $c->forward('/auth/check_csrf_token'); - - $c->stash->{field_errors} = {}; - my $email = lc $c->get_param('email'); - my $phone = $c->get_param('phone'); - my $email_v = $c->get_param('email_verified'); - my $phone_v = $c->get_param('phone_verified'); - - if ($email && !is_valid_email($email)) { - $c->stash->{field_errors}->{email} = _('Please enter a valid email'); - } - unless ($c->get_param('name')) { - $c->stash->{field_errors}->{name} = _('Please enter a name'); - } - - unless ($email || $phone) { - $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number'); - } - if (!$email_v && !$phone_v) { - $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone'); - } - - if ($phone_v) { - my $parsed_phone = $c->forward('phone_check', [ $phone ]); - $phone = $parsed_phone if $parsed_phone; - } - - my $existing_email = $email_v && $c->model('DB::User')->find( { email => $email } ); - my $existing_phone = $phone_v && $c->model('DB::User')->find( { phone => $phone } ); - if ($existing_email || $existing_phone) { - $c->stash->{field_errors}->{username} = _('User already exists'); - } - - return if %{$c->stash->{field_errors}}; - - my $user = $c->model('DB::User')->create( { - name => $c->get_param('name'), - email => $email ? $email : undef, - email_verified => $email && $email_v ? 1 : 0, - phone => $phone || undef, - phone_verified => $phone && $phone_v ? 1 : 0, - from_body => $c->get_param('body') || undef, - flagged => $c->get_param('flagged') || 0, - # Only superusers can create superusers - is_superuser => ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0, - } ); - $c->stash->{user} = $user; - $c->forward('user_cobrand_extra_fields'); - $user->update; - - $c->forward( 'log_edit', [ $user->id, 'user', 'edit' ] ); - - $c->flash->{status_message} = _("Updated!"); - $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) ); -} - -sub user_edit : Path('user_edit') : Args(1) { - my ( $self, $c, $id ) = @_; - - $c->forward('/auth/get_csrf_token'); - - my $user = $c->cobrand->users->find( { id => $id } ); - $c->detach( '/page_error_404_not_found', [] ) unless $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; - $c->forward( 'check_username_for_abuse', [ $user ] ); - - if ( $user->from_body && $c->user->has_permission_to('user_manage_permissions', $user->from_body->id) ) { - $c->stash->{available_permissions} = $c->cobrand->available_permissions; - } - - $c->forward('fetch_all_bodies'); - $c->forward('fetch_body_areas', [ $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->forward('/auth/check_csrf_token') if $c->get_param('submit'); - - if ( $c->get_param('submit') and $c->get_param('unban') ) { - $c->forward('unban_user', [ $user ]); - } elsif ( $c->get_param('submit') and $c->get_param('logout_everywhere') ) { - $c->forward('user_logout_everywhere', [ $user ]); - } elsif ( $c->get_param('submit') and $c->get_param('anon_everywhere') ) { - $c->forward('user_anon_everywhere', [ $user ]); - } elsif ( $c->get_param('submit') and $c->get_param('hide_everywhere') ) { - $c->forward('user_hide_everywhere', [ $user ]); - } elsif ( $c->get_param('submit') and $c->get_param('remove_account') ) { - $c->forward('user_remove_account', [ $user ]); - } 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; - $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'); - my $email_v = $c->get_param('email_verified') || 0; - my $phone_v = $c->get_param('phone_verified') || 0; - - $c->stash->{field_errors} = {}; - - unless ($email || $phone) { - $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number'); - } - if (!$email_v && !$phone_v) { - $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone'); - } - if ($email && !is_valid_email($email)) { - $c->stash->{field_errors}->{email} = _('Please enter a valid email'); - } - - if ($phone_v) { - my $parsed_phone = $c->forward('phone_check', [ $phone ]); - $phone = $parsed_phone if $parsed_phone; - } - - unless ($name) { - $c->stash->{field_errors}->{name} = _('Please enter a name'); - } - - my $email_params = { email => $email, email_verified => 1, id => { '!=', $user->id } }; - my $phone_params = { phone => $phone, phone_verified => 1, id => { '!=', $user->id } }; - my $existing_email = $email_v && $c->model('DB::User')->search($email_params)->first; - my $existing_phone = $phone_v && $c->model('DB::User')->search($phone_params)->first; - my $existing_user = $existing_email || $existing_phone; - my $existing_email_cobrand = $email_v && $c->cobrand->users->search($email_params)->first; - my $existing_phone_cobrand = $phone_v && $c->cobrand->users->search($phone_params)->first; - my $existing_user_cobrand = $existing_email_cobrand || $existing_phone_cobrand; - if ($existing_phone_cobrand && $existing_email_cobrand && $existing_email_cobrand->id != $existing_phone_cobrand->id) { - $c->stash->{field_errors}->{username} = _('User already exists'); - } - - 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( 'log_edit', [ $id, 'user', 'merge' ] ); - return $c->res->redirect( $c->uri_for( 'user_edit', $existing_user->id ) ); - } - - $user->email($email) if !$existing_email; - $user->phone($phone) if !$existing_phone; - $user->email_verified( $email_v ); - $user->phone_verified( $phone_v ); - $user->name( $name ); - - $user->flagged( $c->get_param('flagged') || 0 ); - # Only superusers can grant superuser status - $user->is_superuser( ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0 ); - # Superusers can set from_body to any value, but other staff can only - # set from_body to the same value as their own from_body. - if ( $c->user->is_superuser || $c->cobrand->moniker eq 'zurich' ) { - $user->from_body( $c->get_param('body') || undef ); - } elsif ( $c->user->has_body_permission_to('user_assign_body') ) { - if ($c->get_param('body') && $c->get_param('body') eq $c->user->from_body->id ) { - $user->from_body( $c->user->from_body ); - } else { - $user->from_body( undef ); - } - } - - $c->forward('user_cobrand_extra_fields'); - - # Has the user's from_body changed since we fetched areas (if we ever did)? - # 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('fetch_body_areas', [ $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->area_ids(undef); - delete $c->stash->{areas}; - 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({ - body_id => $user->from_body->id, - permission_type => $permission_type, - }); - } - } - - if ( $user->from_body && $c->user->has_permission_to('user_assign_areas', $user->from_body->id) ) { - my %valid_areas = map { $_->{id} => 1 } @{ $c->stash->{areas} }; - my @area_ids = grep { $valid_areas{$_} } $c->get_param_list('area_ids'); - $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; - $c->forward('fetch_contacts'); - my @live_contacts = $c->stash->{live_contacts}->all; - 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); - } - - $user->update; - if ($edited) { - $c->forward( 'log_edit', [ $id, 'user', 'edit' ] ); - } - $c->flash->{status_message} = _("Updated!"); - return $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) ); - } - - if ( $user->from_body ) { - unless ( $c->stash->{live_contacts} ) { - $c->stash->{body} = $user->from_body; - $c->forward('fetch_contacts'); - } - my @contacts = @{$user->get_extra_metadata('categories') || []}; - my %active_contacts = map { $_ => 1 } @contacts; - my @live_contacts = $c->stash->{live_contacts}->all; - my @all_contacts = map { { - id => $_->id, - category => $_->category, - active => $active_contacts{$_->id}, - } } @live_contacts; - $c->stash->{contacts} = \@all_contacts; - } - - # this goes after in case we've delete any alerts - unless ( $c->cobrand->moniker eq 'zurich' ) { - $c->forward('user_alert_details'); - } - - return 1; -} - -sub user_import : Path('user_import') { - my ( $self, $c, $id ) = @_; - - $c->forward('/auth/get_csrf_token'); - return unless $c->user_exists && $c->user->is_superuser; - - if ($c->req->method eq 'POST') { - $c->forward('/auth/check_csrf_token'); - $c->stash->{new_users} = []; - $c->stash->{existing_users} = []; - - my @all_permissions = map { keys %$_ } values %{ $c->cobrand->available_permissions }; - my %available_permissions = map { $_ => 1 } @all_permissions; - - 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); - - my $user = FixMyStreet::DB->resultset("User")->find_or_new({ email => $email, email_verified => 1 }); - if ($user->in_storage) { - push @{$c->stash->{existing_users}}, $user; - next; - } - - $user->name($name); - $user->from_body($from_body || undef); - $user->update_or_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, - }); - } - - push @{$c->stash->{new_users}}, $user; - } - - } -} - -sub contact_cobrand_extra_fields : Private { - my ( $self, $c, $contact ) = @_; - - my $extra_fields = $c->cobrand->call_hook('contact_extra_fields'); - foreach ( @$extra_fields ) { - $contact->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); - } -} - -sub user_cobrand_extra_fields : Private { - my ( $self, $c ) = @_; - - my @extra_fields = @{ $c->cobrand->call_hook('user_extra_fields') || [] }; - foreach ( @extra_fields ) { - $c->stash->{user}->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); - } -} - sub add_flags : Private { my ( $self, $c, $search ) = @_; @@ -1873,28 +1022,6 @@ sub get_user : Private { return $user; } -sub user_alert_details : Private { - my ( $self, $c ) = @_; - - my @alerts = $c->stash->{user}->alerts({}, { prefetch => 'alert_type' })->all; - $c->stash->{alerts} = \@alerts; - - my @wards; - - for my $alert (@alerts) { - if ($alert->alert_type->ref eq 'ward_problems') { - push @wards, $alert->parameter2; - } - } - - if (@wards) { - $c->stash->{alert_areas} = mySociety::MaPit::call('areas', join(',', @wards) ); - } - - my %body_names = map { $_->{id} => $_->{name} } @{ $c->stash->{bodies} }; - $c->stash->{body_names} = \%body_names; -} - =item log_edit $c->forward( 'log_edit', [ $object_id, $object_type, $action_performed ] ); @@ -1926,209 +1053,6 @@ sub log_edit : Private { )->insert(); } -=head2 ban_user - -Add the user's email address/phone number to the abuse table if they are not -already in there and sets status_message accordingly. - -=cut - -sub ban_user : Private { - my ( $self, $c ) = @_; - - my $user; - if ($c->stash->{problem}) { - $user = $c->stash->{problem}->user; - } elsif ($c->stash->{update}) { - $user = $c->stash->{update}->user; - } - return unless $user; - - if ($user->email_verified && $user->email) { - my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->email }); - if ( $abuse->in_storage ) { - $c->stash->{status_message} = _('User already in abuse list'); - } else { - $abuse->insert; - $c->stash->{status_message} = _('User added to abuse list'); - } - $c->stash->{username_in_abuse} = 1; - } - if ($user->phone_verified && $user->phone) { - my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->phone }); - if ( $abuse->in_storage ) { - $c->stash->{status_message} = _('User already in abuse list'); - } else { - $abuse->insert; - $c->stash->{status_message} = _('User added to abuse list'); - } - $c->stash->{username_in_abuse} = 1; - } - return 1; -} - -sub update_alerts : Private { - my ($self, $c) = @_; - - my $changes; - for my $alert ( $c->stash->{user}->alerts ) { - my $edit_option = $c->get_param('edit_alert[' . $alert->id . ']'); - next unless $edit_option; - $changes = 1; - if ( $edit_option eq 'delete' ) { - $alert->delete; - } elsif ( $edit_option eq 'disable' ) { - $alert->disable; - } elsif ( $edit_option eq 'enable' ) { - $alert->confirm; - } - } - $c->flash->{status_message} = _("Updated!") if $changes; -} - -sub user_logout_everywhere : Private { - my ( $self, $c, $user ) = @_; - my $sessions = $user->get_extra_metadata('sessions'); - foreach (grep { $_ ne $c->sessionid } @$sessions) { - $c->delete_session_data("session:$_"); - } - $c->stash->{status_message} = _('That user has been logged out.'); -} - -sub user_anon_everywhere : Private { - my ( $self, $c, $user ) = @_; - $user->problems->update({anonymous => 1}); - $user->comments->update({anonymous => 1}); - $c->stash->{status_message} = _('That user has been made anonymous on all reports and updates.'); -} - -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->update({ state => 'hidden' }); - } - my $updates = $user->comments->search({ state => { '!=' => 'hidden' } }); - while (my $update = $updates->next) { - $update->hide; - } - $c->stash->{status_message} = _('That user’s reports and updates have been hidden.'); -} - -sub send_login_email : Private { - my ( $self, $c, $args ) = @_; - - my $token_data = { - email => $args->{email}, - }; - - $token_data->{old_user_id} = $args->{user_id} if $args->{user_id}; - $token_data->{name} = $args->{name} if $args->{name}; - - my $token_obj = $c->model('DB::Token')->create({ - scope => 'email_sign_in', - data => $token_data, - }); - - $c->stash->{token} = $token_obj->token; - my $template = 'login.txt'; - - # do not use relative URIs in the email, obvs. - $c->uri_disposition('absolute'); - $c->send_email( $template, { to => $args->{email} } ); - - $c->stash->{status_message} = _('The user has been sent a login email'); -} - -# Anonymize and remove name from all problems/updates, disable all alerts. -# Remove their account's email address, phone number, password, etc. -sub user_remove_account : Private { - my ( $self, $c, $user ) = @_; - $c->forward('user_logout_everywhere', [ $user ]); - $user->anonymize_account; - $c->stash->{status_message} = _('That user’s personal details have been removed.'); -} - -sub unban_user : Private { - my ( $self, $c, $user ) = @_; - - my @username; - if ($user->email_verified && $user->email) { - push @username, $user->email; - } - if ($user->phone_verified && $user->phone) { - push @username, $user->phone; - } - if (@username) { - my $abuse = $c->model('DB::Abuse')->search({ email => \@username }); - if ( $abuse ) { - $abuse->delete; - $c->stash->{status_message} = _('user removed from abuse list'); - } else { - $c->stash->{status_message} = _('user not in abuse list'); - } - $c->stash->{username_in_abuse} = 0; - } -} - -=head2 flag_user - -Sets the flag on a user - -=cut - -sub flag_user : Private { - my ( $self, $c ) = @_; - - my $user; - if ($c->stash->{problem}) { - $user = $c->stash->{problem}->user; - } elsif ($c->stash->{update}) { - $user = $c->stash->{update}->user; - } - - if ( !$user ) { - $c->stash->{status_message} = _('Could not find user'); - } else { - $user->flagged(1); - $user->update; - $c->stash->{status_message} = _('User flagged'); - } - - $c->stash->{user_flagged} = 1; - - return 1; -} - -=head2 remove_user_flag - -Remove the flag on a user - -=cut - -sub remove_user_flag : Private { - my ( $self, $c ) = @_; - - my $user; - if ($c->stash->{problem}) { - $user = $c->stash->{problem}->user; - } elsif ($c->stash->{update}) { - $user = $c->stash->{update}->user; - } - - if ( !$user ) { - $c->stash->{status_message} = _('Could not find user'); - } else { - $user->flagged(0); - $user->update; - $c->stash->{status_message} = _('User flag removed'); - } - - return 1; -} - - =head2 check_username_for_abuse $c->forward('check_username_for_abuse', [ $user ] ); diff --git a/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm new file mode 100644 index 000000000..8d3f0bb0d --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/Bodies.pm @@ -0,0 +1,475 @@ +package FixMyStreet::App::Controller::Admin::Bodies; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +use POSIX qw(strcoll); +use mySociety::EmailUtil qw(is_valid_email_list); +use FixMyStreet::SendReport; + +=head1 NAME + +FixMyStreet::App::Controller::Admin::Bodies - Catalyst Controller + +=head1 DESCRIPTION + +Admin pages + +=head1 METHODS + +=cut + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + + if (my $body_id = $c->get_param('body')) { + return $c->res->redirect( $c->uri_for( 'body', $body_id ) ); + } + + if (!$c->user->is_superuser && $c->user->from_body && $c->cobrand->moniker ne 'zurich') { + return $c->res->redirect( $c->uri_for( 'body', $c->user->from_body->id ) ); + } + + $c->forward( '/auth/get_csrf_token' ); + + my $edit_activity = $c->model('DB::ContactsHistory')->search( + undef, + { + select => [ 'editor', { count => 'contacts_history_id', -as => 'c' } ], + as => [ 'editor', 'c' ], + group_by => ['editor'], + order_by => { -desc => 'c' } + } + ); + + $c->stash->{edit_activity} = $edit_activity; + + $c->forward( '/admin/fetch_languages' ); + $c->forward( 'fetch_translations' ); + + 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( '/admin/fetch_all_bodies' ); + + my $contacts = $c->model('DB::Contact')->search( + undef, + { + select => [ 'body_id', { count => 'id' }, { count => \'case when state = \'deleted\' then 1 else null end' }, + { count => \'case when state = \'confirmed\' then 1 else null end' } ], + as => [qw/body_id c deleted confirmed/], + group_by => [ 'body_id' ], + result_class => 'DBIx::Class::ResultClass::HashRefInflator' + } + ); + + my %council_info = map { $_->{body_id} => $_ } $contacts->all; + + $c->stash->{counts} = \%council_info; + + $c->forward( 'body_form_dropdowns' ); + + return 1; +} + +sub body : Chained('/') : PathPart('admin/body') : CaptureArgs(1) { + my ( $self, $c, $body_id ) = @_; + + $c->stash->{body_id} = $body_id; + my $body = $c->model('DB::Body')->find($body_id); + $c->detach( '/page_error_404_not_found', [] ) unless $body; + $c->stash->{body} = $body; + + if ($body->body_areas->first) { + my $example_postcode = mySociety::MaPit::call('area/example_postcode', $body->body_areas->first->area_id); + if ($example_postcode && ! ref $example_postcode) { + $c->stash->{example_pc} = $example_postcode; + } + } +} + +sub edit : Chained('body') : PathPart('') : Args(0) { + my ( $self, $c ) = @_; + + unless ($c->user->has_permission_to('category_edit', $c->stash->{body_id})) { + $c->forward('check_for_super_user'); + } + + $c->forward( '/auth/get_csrf_token' ); + $c->forward( '/admin/fetch_all_bodies' ); + $c->forward( 'body_form_dropdowns' ); + $c->forward('/admin/fetch_languages'); + + if ( $c->get_param('posted') ) { + $c->forward('update_contacts'); + } + + $c->stash->{object} = $c->stash->{body}; + $c->stash->{translation_col} = 'name'; + + # if there's a contact then it's because we're displaying error + # messages about adding a contact so grabbing translations will + # fetch the contact submitted translations. So grab them, stash + # them and then clear posted so we can fetch the body translations + if ($c->stash->{contact}) { + $c->forward('fetch_translations'); + $c->stash->{contact_translations} = $c->stash->{translations}; + } + $c->set_param('posted', ''); + + $c->forward('fetch_translations'); + + # don't set this last as fetch_contacts might over-ride it + # to display email addresses as text + $c->stash->{template} = 'admin/bodies/body.html'; + $c->forward('/admin/fetch_contacts'); + + return 1; +} + +sub category : Chained('body') : PathPart('') { + my ( $self, $c, @category ) = @_; + my $category = join( '/', @category ); + + $c->forward( '/auth/get_csrf_token' ); + + my $contact = $c->stash->{body}->contacts->search( { category => $category } )->first; + $c->stash->{contact} = $contact; + + $c->stash->{translation_col} = 'category'; + $c->stash->{object} = $c->stash->{contact}; + + $c->forward('/admin/fetch_languages'); + $c->forward('fetch_translations'); + + my $history = $c->model('DB::ContactsHistory')->search( + { + body_id => $c->stash->{body_id}, + category => $c->stash->{contact}->category + }, + { + order_by => ['contacts_history_id'] + }, + ); + $c->stash->{history} = $history; + my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders }; + $c->stash->{send_methods} = \@methods; + + return 1; +} + +sub body_form_dropdowns : Private { + my ( $self, $c ) = @_; + + my $areas; + my $whitelist = $c->config->{MAPIT_ID_WHITELIST}; + + if ( $whitelist && ref $whitelist eq 'ARRAY' && @$whitelist ) { + $areas = mySociety::MaPit::call('areas', $whitelist); + } else { + $areas = mySociety::MaPit::call('areas', $c->cobrand->area_types); + } + + # Some cobrands may want to add extra areas at runtime beyond those + # available via MAPIT_WHITELIST or MAPIT_TYPES. This can be used for, + # e.g., parish councils on a particular council cobrand. + $areas = $c->cobrand->call_hook("add_extra_areas" => $areas) || $areas; + + $c->stash->{areas} = [ sort { strcoll($a->{name}, $b->{name}) } values %$areas ]; + + my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders }; + $c->stash->{send_methods} = \@methods; +} + +sub check_for_super_user : Private { + my ( $self, $c ) = @_; + + my $superuser = $c->user->is_superuser; + # Zurich currently has its own way of defining superusers + $superuser ||= $c->cobrand->moniker eq 'zurich' && $c->stash->{admin_type} eq 'super'; + + unless ( $superuser ) { + $c->detach('/page_error_403_access_denied', []); + } +} + +sub update_contacts : Private { + my ( $self, $c ) = @_; + + my $posted = $c->get_param('posted'); + my $editor = $c->forward('/admin/get_user'); + + if ( $posted eq 'new' ) { + $c->forward('/auth/check_csrf_token'); + + my %errors; + + 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 $contact = $c->model('DB::Contact')->find_or_new( + { + body_id => $c->stash->{body_id}, + category => $category, + } + ); + + 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'; + } + + $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')) ); + } + if ( my $group = $c->get_param('group') ) { + $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 ]); + + 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'); + + # NB: History is automatically stored by a trigger in the database + $contact->update; + } else { + $c->stash->{updated} = _('New category contact added'); + $contact->insert; + } + + unless ( %errors ) { + $c->stash->{translation_col} = 'category'; + $c->stash->{object} = $contact; + $c->forward('update_translations'); + } + + } elsif ( $posted eq 'update' ) { + $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 }, + } + ); + + $contacts->update( + { + state => 'confirmed', + whenedited => \'current_timestamp', + note => 'Confirmed', + editor => $editor, + } + ); + + $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; + + $c->stash->{translation_col} = 'name'; + $c->stash->{object} = $c->stash->{body}; + $c->forward('update_translations'); + + $c->stash->{updated} = _('Values updated'); + } + } +} + +sub body_params : Private { + my ( $self, $c ) = @_; + + my @fields = qw/name endpoint jurisdiction api_key send_method external_url/; + my %defaults = map { $_ => '' } @fields; + %defaults = ( %defaults, + send_comments => 0, + fetch_problems => 0, + convert_latlong => 0, + blank_updates_permitted => 0, + suppress_alerts => 0, + comment_user_id => undef, + send_extended_statuses => 0, + can_be_devolved => 0, + parent => undef, + deleted => 0, + ); + my %params = map { $_ => $c->get_param($_) || $defaults{$_} } keys %defaults; + $c->forward('check_body_params', [ \%params ]); + my @extras = qw/fetch_all_problems/; + %defaults = map { $_ => '' } @extras; + my %extras = map { $_ => $c->get_param($_) || $defaults{$_} } @extras; + return { params => \%params, extras => \%extras }; +} + +sub check_body_params : Private { + my ( $self, $c, $params ) = @_; + + $c->stash->{body_errors} ||= {}; + + unless ($params->{name}) { + $c->stash->{body_errors}->{name} = _('Please enter a name for this body'); + } +} + +sub contact_cobrand_extra_fields : Private { + my ( $self, $c, $contact ) = @_; + + my $extra_fields = $c->cobrand->call_hook('contact_extra_fields'); + foreach ( @$extra_fields ) { + $contact->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); + } +} + +sub fetch_translations : Private { + my ( $self, $c ) = @_; + + my $translations = {}; + if ($c->get_param('posted')) { + foreach my $lang (keys %{$c->stash->{languages}}) { + if (my $msgstr = $c->get_param('translation_' . $lang)) { + $translations->{$lang} = { msgstr => $msgstr }; + } + if (my $id = $c->get_param('translation_id_' . $lang)) { + $translations->{$lang}->{id} = $id; + } + } + } elsif ($c->stash->{object}) { + my @translations = $c->stash->{object}->translation_for($c->stash->{translation_col})->all; + + foreach my $tx (@translations) { + $translations->{$tx->lang} = { id => $tx->id, msgstr => $tx->msgstr }; + } + } + + $c->stash->{translations} = $translations; +} + +sub update_translations : Private { + my ( $self, $c ) = @_; + + foreach my $lang (keys(%{$c->stash->{languages}})) { + my $id = $c->get_param('translation_id_' . $lang); + my $text = $c->get_param('translation_' . $lang); + if ($id) { + my $translation = $c->model('DB::Translation')->find( + { + id => $id, + } + ); + + if ($text) { + $translation->msgstr($text); + $translation->update; + } else { + $translation->delete; + } + } elsif ($text) { + my $col = $c->stash->{translation_col}; + $c->stash->{object}->add_translation_for( + $col, $lang, $text + ); + } + } +} + +sub trim { + my $self = shift; + my $e = shift; + $e =~ s/^\s+//; + $e =~ s/\s+$//; + return $e; +} + +=head1 AUTHOR + +Struan Donald + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/Users.pm b/perllib/FixMyStreet/App/Controller/Admin/Users.pm new file mode 100644 index 000000000..11a6d9962 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/Users.pm @@ -0,0 +1,684 @@ +package FixMyStreet::App::Controller::Admin::Users; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +use POSIX qw(strcoll); +use mySociety::EmailUtil qw(is_valid_email); +use Text::CSV; + +use FixMyStreet::SMS; +use Utils; + +=head1 NAME + +FixMyStreet::App::Controller::Admin::Users - Catalyst Controller + +=head1 DESCRIPTION + +Admin pages for editing users + +=head1 METHODS + +=cut + +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, + ] + } + ); + my @users = $users->all; + $c->stash->{users} = [ @users ]; + $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 } }, + { order_by => 'name' } + ); + my @users = $users->all; + $c->stash->{users} = \@users; + } + + return 1; +} + +sub add : Local : Args(0) { + my ( $self, $c ) = @_; + + $c->stash->{template} = 'admin/users/edit.html'; + $c->forward('/auth/get_csrf_token'); + $c->forward('/admin/fetch_all_bodies'); + $c->cobrand->call_hook('admin_user_edit_extra_data'); + + return unless $c->get_param('submit'); + + $c->forward('/auth/check_csrf_token'); + + $c->stash->{field_errors} = {}; + my $email = lc $c->get_param('email'); + my $phone = $c->get_param('phone'); + my $email_v = $c->get_param('email_verified'); + my $phone_v = $c->get_param('phone_verified'); + + if ($email && !is_valid_email($email)) { + $c->stash->{field_errors}->{email} = _('Please enter a valid email'); + } + unless ($c->get_param('name')) { + $c->stash->{field_errors}->{name} = _('Please enter a name'); + } + + unless ($email || $phone) { + $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number'); + } + if (!$email_v && !$phone_v) { + $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone'); + } + + if ($phone_v) { + my $parsed_phone = $c->forward('phone_check', [ $phone ]); + $phone = $parsed_phone if $parsed_phone; + } + + my $existing_email = $email_v && $c->model('DB::User')->find( { email => $email } ); + my $existing_phone = $phone_v && $c->model('DB::User')->find( { phone => $phone } ); + if ($existing_email || $existing_phone) { + $c->stash->{field_errors}->{username} = _('User already exists'); + } + + return if %{$c->stash->{field_errors}}; + + my $user = $c->model('DB::User')->create( { + name => $c->get_param('name'), + email => $email ? $email : undef, + email_verified => $email && $email_v ? 1 : 0, + phone => $phone || undef, + phone_verified => $phone && $phone_v ? 1 : 0, + from_body => $c->get_param('body') || undef, + flagged => $c->get_param('flagged') || 0, + # Only superusers can create superusers + is_superuser => ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0, + } ); + $c->stash->{user} = $user; + $c->forward('user_cobrand_extra_fields'); + $user->update; + + $c->forward( '/admin/log_edit', [ $user->id, 'user', 'edit' ] ); + + $c->flash->{status_message} = _("Updated!"); + $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $user->id ) ); +} + +sub edit : Path : Args(1) { + my ( $self, $c, $id ) = @_; + + $c->forward('/auth/get_csrf_token'); + + my $user = $c->cobrand->users->find( { id => $id } ); + $c->detach( '/page_error_404_not_found', [] ) unless $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; + $c->forward( '/admin/check_username_for_abuse', [ $user ] ); + + if ( $user->from_body && $c->user->has_permission_to('user_manage_permissions', $user->from_body->id) ) { + $c->stash->{available_permissions} = $c->cobrand->available_permissions; + } + + $c->forward('/admin/fetch_all_bodies'); + $c->forward('/admin/fetch_body_areas', [ $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->forward('/auth/check_csrf_token') if $c->get_param('submit'); + + if ( $c->get_param('submit') and $c->get_param('unban') ) { + $c->forward('unban', [ $user ]); + } elsif ( $c->get_param('submit') and $c->get_param('logout_everywhere') ) { + $c->forward('user_logout_everywhere', [ $user ]); + } elsif ( $c->get_param('submit') and $c->get_param('anon_everywhere') ) { + $c->forward('user_anon_everywhere', [ $user ]); + } elsif ( $c->get_param('submit') and $c->get_param('hide_everywhere') ) { + $c->forward('user_hide_everywhere', [ $user ]); + } elsif ( $c->get_param('submit') and $c->get_param('remove_account') ) { + $c->forward('user_remove_account', [ $user ]); + } 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; + $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'); + my $email_v = $c->get_param('email_verified') || 0; + my $phone_v = $c->get_param('phone_verified') || 0; + + $c->stash->{field_errors} = {}; + + unless ($email || $phone) { + $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number'); + } + if (!$email_v && !$phone_v) { + $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone'); + } + if ($email && !is_valid_email($email)) { + $c->stash->{field_errors}->{email} = _('Please enter a valid email'); + } + + if ($phone_v) { + my $parsed_phone = $c->forward('phone_check', [ $phone ]); + $phone = $parsed_phone if $parsed_phone; + } + + unless ($name) { + $c->stash->{field_errors}->{name} = _('Please enter a name'); + } + + my $email_params = { email => $email, email_verified => 1, id => { '!=', $user->id } }; + my $phone_params = { phone => $phone, phone_verified => 1, id => { '!=', $user->id } }; + my $existing_email = $email_v && $c->model('DB::User')->search($email_params)->first; + my $existing_phone = $phone_v && $c->model('DB::User')->search($phone_params)->first; + my $existing_user = $existing_email || $existing_phone; + my $existing_email_cobrand = $email_v && $c->cobrand->users->search($email_params)->first; + my $existing_phone_cobrand = $phone_v && $c->cobrand->users->search($phone_params)->first; + my $existing_user_cobrand = $existing_email_cobrand || $existing_phone_cobrand; + if ($existing_phone_cobrand && $existing_email_cobrand && $existing_email_cobrand->id != $existing_phone_cobrand->id) { + $c->stash->{field_errors}->{username} = _('User already exists'); + } + + 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 ) ); + } + + $user->email($email) if !$existing_email; + $user->phone($phone) if !$existing_phone; + $user->email_verified( $email_v ); + $user->phone_verified( $phone_v ); + $user->name( $name ); + + $user->flagged( $c->get_param('flagged') || 0 ); + # Only superusers can grant superuser status + $user->is_superuser( ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0 ); + # Superusers can set from_body to any value, but other staff can only + # set from_body to the same value as their own from_body. + if ( $c->user->is_superuser || $c->cobrand->moniker eq 'zurich' ) { + $user->from_body( $c->get_param('body') || undef ); + } elsif ( $c->user->has_body_permission_to('user_assign_body') ) { + if ($c->get_param('body') && $c->get_param('body') eq $c->user->from_body->id ) { + $user->from_body( $c->user->from_body ); + } else { + $user->from_body( undef ); + } + } + + $c->forward('user_cobrand_extra_fields'); + + # Has the user's from_body changed since we fetched areas (if we ever did)? + # 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 ]); + } + + 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->area_ids(undef); + delete $c->stash->{areas}; + 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({ + body_id => $user->from_body->id, + permission_type => $permission_type, + }); + } + } + + if ( $user->from_body && $c->user->has_permission_to('user_assign_areas', $user->from_body->id) ) { + my %valid_areas = map { $_->{id} => 1 } @{ $c->stash->{areas} }; + my @area_ids = grep { $valid_areas{$_} } $c->get_param_list('area_ids'); + $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; + $c->forward('/admin/fetch_contacts'); + my @live_contacts = $c->stash->{live_contacts}->all; + 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); + } + + $user->update; + if ($edited) { + $c->forward( '/admin/log_edit', [ $id, 'user', 'edit' ] ); + } + $c->flash->{status_message} = _("Updated!"); + return $c->res->redirect( $c->uri_for_action( 'admin/users/edit', $user->id ) ); + } + + if ( $user->from_body ) { + unless ( $c->stash->{live_contacts} ) { + $c->stash->{body} = $user->from_body; + $c->forward('/admin/fetch_contacts'); + } + my @contacts = @{$user->get_extra_metadata('categories') || []}; + my %active_contacts = map { $_ => 1 } @contacts; + my @live_contacts = $c->stash->{live_contacts}->all; + my @all_contacts = map { { + id => $_->id, + category => $_->category, + active => $active_contacts{$_->id}, + } } @live_contacts; + $c->stash->{contacts} = \@all_contacts; + } + + # this goes after in case we've delete any alerts + unless ( $c->cobrand->moniker eq 'zurich' ) { + $c->forward('user_alert_details'); + } + + return 1; +} + +sub import :Local { + my ( $self, $c, $id ) = @_; + + $c->forward('/auth/get_csrf_token'); + return unless $c->user_exists && $c->user->is_superuser; + + return unless $c->req->method eq 'POST'; + + $c->forward('/auth/check_csrf_token'); + $c->stash->{new_users} = []; + $c->stash->{existing_users} = []; + + my @all_permissions = map { keys %$_ } values %{ $c->cobrand->available_permissions }; + my %available_permissions = map { $_ => 1 } @all_permissions; + + 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); + + my $user = FixMyStreet::DB->resultset("User")->find_or_new({ email => $email, email_verified => 1 }); + if ($user->in_storage) { + push @{$c->stash->{existing_users}}, $user; + next; + } + + $user->name($name); + $user->from_body($from_body || undef); + $user->update_or_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, + }); + } + + push @{$c->stash->{new_users}}, $user; + } +} + +sub phone_check : Private { + my ($self, $c, $phone) = @_; + my $parsed = FixMyStreet::SMS->parse_username($phone); + if ($parsed->{phone} && $parsed->{may_be_mobile}) { + return $parsed->{username}; + } elsif ($parsed->{phone}) { + $c->stash->{field_errors}->{phone} = _('Please enter a mobile number'); + } else { + $c->stash->{field_errors}->{phone} = _('Please check your phone number is correct'); + } +} + +sub user_cobrand_extra_fields : Private { + my ( $self, $c ) = @_; + + my @extra_fields = @{ $c->cobrand->call_hook('user_extra_fields') || [] }; + foreach ( @extra_fields ) { + $c->stash->{user}->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); + } +} + +sub user_alert_details : Private { + my ( $self, $c ) = @_; + + my @alerts = $c->stash->{user}->alerts({}, { prefetch => 'alert_type' })->all; + $c->stash->{alerts} = \@alerts; + + my @wards; + + for my $alert (@alerts) { + if ($alert->alert_type->ref eq 'ward_problems') { + push @wards, $alert->parameter2; + } + } + + if (@wards) { + $c->stash->{alert_areas} = mySociety::MaPit::call('areas', join(',', @wards) ); + } + + my %body_names = map { $_->{id} => $_->{name} } @{ $c->stash->{bodies} }; + $c->stash->{body_names} = \%body_names; +} + +sub update_alerts : Private { + my ($self, $c) = @_; + + my $changes; + for my $alert ( $c->stash->{user}->alerts ) { + my $edit_option = $c->get_param('edit_alert[' . $alert->id . ']'); + next unless $edit_option; + $changes = 1; + if ( $edit_option eq 'delete' ) { + $alert->delete; + } elsif ( $edit_option eq 'disable' ) { + $alert->disable; + } elsif ( $edit_option eq 'enable' ) { + $alert->confirm; + } + } + $c->flash->{status_message} = _("Updated!") if $changes; +} + +sub user_logout_everywhere : Private { + my ( $self, $c, $user ) = @_; + my $sessions = $user->get_extra_metadata('sessions'); + foreach (grep { $_ ne $c->sessionid } @$sessions) { + $c->delete_session_data("session:$_"); + } + $c->stash->{status_message} = _('That user has been logged out.'); +} + +sub user_anon_everywhere : Private { + my ( $self, $c, $user ) = @_; + $user->problems->update({anonymous => 1}); + $user->comments->update({anonymous => 1}); + $c->stash->{status_message} = _('That user has been made anonymous on all reports and updates.'); +} + +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->update({ state => 'hidden' }); + } + my $updates = $user->comments->search({ state => { '!=' => 'hidden' } }); + while (my $update = $updates->next) { + $update->hide; + } + $c->stash->{status_message} = _('That user’s reports and updates have been hidden.'); +} + +sub send_login_email : Private { + my ( $self, $c, $args ) = @_; + + my $token_data = { + email => $args->{email}, + }; + + $token_data->{old_user_id} = $args->{user_id} if $args->{user_id}; + $token_data->{name} = $args->{name} if $args->{name}; + + my $token_obj = $c->model('DB::Token')->create({ + scope => 'email_sign_in', + data => $token_data, + }); + + $c->stash->{token} = $token_obj->token; + my $template = 'login.txt'; + + # do not use relative URIs in the email, obvs. + $c->uri_disposition('absolute'); + $c->send_email( $template, { to => $args->{email} } ); + + $c->stash->{status_message} = _('The user has been sent a login email'); +} + +# Anonymize and remove name from all problems/updates, disable all alerts. +# Remove their account's email address, phone number, password, etc. +sub user_remove_account : Private { + my ( $self, $c, $user ) = @_; + $c->forward('user_logout_everywhere', [ $user ]); + $user->anonymize_account; + $c->stash->{status_message} = _('That user’s personal details have been removed.'); +} + +=head2 ban + +Add the user's email address/phone number to the abuse table if they are not +already in there and sets status_message accordingly. + +=cut + +sub ban : Private { + my ( $self, $c ) = @_; + + my $user; + if ($c->stash->{problem}) { + $user = $c->stash->{problem}->user; + } elsif ($c->stash->{update}) { + $user = $c->stash->{update}->user; + } + return unless $user; + + if ($user->email_verified && $user->email) { + my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->email }); + if ( $abuse->in_storage ) { + $c->stash->{status_message} = _('User already in abuse list'); + } else { + $abuse->insert; + $c->stash->{status_message} = _('User added to abuse list'); + } + $c->stash->{username_in_abuse} = 1; + } + if ($user->phone_verified && $user->phone) { + my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->phone }); + if ( $abuse->in_storage ) { + $c->stash->{status_message} = _('User already in abuse list'); + } else { + $abuse->insert; + $c->stash->{status_message} = _('User added to abuse list'); + } + $c->stash->{username_in_abuse} = 1; + } + return 1; +} + +sub unban : Private { + my ( $self, $c, $user ) = @_; + + my @username; + if ($user->email_verified && $user->email) { + push @username, $user->email; + } + if ($user->phone_verified && $user->phone) { + push @username, $user->phone; + } + if (@username) { + my $abuse = $c->model('DB::Abuse')->search({ email => \@username }); + if ( $abuse ) { + $abuse->delete; + $c->stash->{status_message} = _('user removed from abuse list'); + } else { + $c->stash->{status_message} = _('user not in abuse list'); + } + $c->stash->{username_in_abuse} = 0; + } +} + +=head2 flag + +Sets the flag on a user + +=cut + +sub flag : Private { + my ( $self, $c ) = @_; + + my $user; + if ($c->stash->{problem}) { + $user = $c->stash->{problem}->user; + } elsif ($c->stash->{update}) { + $user = $c->stash->{update}->user; + } + + if ( !$user ) { + $c->stash->{status_message} = _('Could not find user'); + } else { + $user->flagged(1); + $user->update; + $c->stash->{status_message} = _('User flagged'); + } + + $c->stash->{user_flagged} = 1; + + return 1; +} + +=head2 flag_remove + +Remove the flag on a user + +=cut + +sub flag_remove : Private { + my ( $self, $c ) = @_; + + my $user; + if ($c->stash->{problem}) { + $user = $c->stash->{problem}->user; + } elsif ($c->stash->{update}) { + $user = $c->stash->{update}->user; + } + + if ( !$user ) { + $c->stash->{status_message} = _('Could not find user'); + } else { + $user->flagged(0); + $user->update; + $c->stash->{status_message} = _('User flag removed'); + } + + return 1; +} + + +sub trim { + my $self = shift; + my $e = shift; + $e =~ s/^\s+//; + $e =~ s/\s+$//; + return $e; +} + +=head1 AUTHOR + +mySociety + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/t/app/controller/admin/permissions.t b/t/app/controller/admin/permissions.t index 036ec4509..ff5a8ec4f 100644 --- a/t/app/controller/admin/permissions.t +++ b/t/app/controller/admin/permissions.t @@ -99,7 +99,7 @@ FixMyStreet::override_config { my $p = $perm ? 'with' : 'without'; my $r = $report->user eq $user2 ? 'with' : 'without'; subtest "User $u edit user for $b $p permission, $r cobrand relation" => sub { - $mech->get("/admin/user_edit/$user2_id"); + $mech->get("/admin/users/$user2_id"); my $success = $mech->res->is_success(); ok $result == 200 ? $success : !$success, "got correct response"; is $mech->res->code, $result, "got $result"; @@ -109,7 +109,7 @@ FixMyStreet::override_config { } subtest "Users can't edit users of their own council without permission" => sub { - $mech->get_ok("/admin/user_edit/$user2_id"); + $mech->get_ok("/admin/users/$user2_id"); $mech->submit_form_ok( { with_fields => { email => $user2->email, } } ); @@ -124,7 +124,7 @@ FixMyStreet::override_config { }); subtest "Users can edit users of their own council" => sub { - $mech->get_ok("/admin/user_edit/$user2_id"); + $mech->get_ok("/admin/users/$user2_id"); $mech->content_contains( $user2->name ); # We shouldn't be able to see the permissions tick boxes @@ -149,7 +149,7 @@ FixMyStreet::override_config { subtest "Users can edit permissions" => sub { is $user2->user_body_permissions->count, 0, 'user2 has no permissions'; - $mech->get_ok("/admin/user_edit/$user2_id"); + $mech->get_ok("/admin/users/$user2_id"); $mech->content_contains('Moderate report details'); $mech->submit_form_ok( { with_fields => { @@ -183,7 +183,7 @@ FixMyStreet::override_config { is $user2->user_body_permissions->count, 1, 'user2 has 1 permission'; $user2->update({ area_ids => [123] }); # Set to check cleared - $mech->get_ok("/admin/user_edit/$user2_id"); + $mech->get_ok("/admin/users/$user2_id"); $mech->content_contains('Moderate report details'); $mech->submit_form_ok( { with_fields => { diff --git a/t/app/controller/admin/users.t b/t/app/controller/admin/users.t index d82a7eaef..e2c922a23 100644 --- a/t/app/controller/admin/users.t +++ b/t/app/controller/admin/users.t @@ -19,7 +19,7 @@ subtest 'search abuse' => sub { }; subtest 'remove user from abuse list from edit user page' => sub { - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->content_contains('User in abuse table'); $mech->click_ok('unban'); @@ -31,7 +31,7 @@ subtest 'remove user from abuse list from edit user page' => sub { subtest 'remove user with phone account from abuse list from edit user page' => sub { my $abuse_user = $mech->create_user_ok('01234 456789'); my $abuse = FixMyStreet::App->model('DB::Abuse')->find_or_create( { email => $abuse_user->phone } ); - $mech->get_ok( '/admin/user_edit/' . $abuse_user->id ); + $mech->get_ok( '/admin/users/' . $abuse_user->id ); $mech->content_contains('User in abuse table'); my $abuse_found = FixMyStreet::App->model('DB::Abuse')->find( { email => $abuse_user->phone } ); ok $abuse_found, 'user in abuse table'; @@ -45,7 +45,7 @@ subtest 'remove user with phone account from abuse list from edit user page' => subtest 'no option to remove user already in abuse list' => sub { my $abuse = FixMyStreet::App->model('DB::Abuse')->find( { email => $user->email } ); $abuse->delete if $abuse; - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->content_lacks('User in abuse table'); }; @@ -66,11 +66,11 @@ subtest 'user search' => sub { $mech->content_contains( $user->name); my $u_id = $user->id; - $mech->content_like( qr{user_edit/$u_id">Edit</a>} ); + $mech->content_like( qr{users/$u_id">Edit</a>} ); $mech->get_ok('/admin/users?search=' . $user->email); - $mech->content_like( qr{user_edit/$u_id">Edit</a>} ); + $mech->content_like( qr{users/$u_id">Edit</a>} ); $user->from_body($haringey->id); $user->update; @@ -96,7 +96,7 @@ subtest 'user_edit does not show user from another council' => sub { FixMyStreet::override_config { ALLOWED_COBRANDS => [ 'oxfordshire' ], }, sub { - $mech->get('/admin/user_edit/' . $user->id); + $mech->get('/admin/users/' . $user->id); ok !$mech->res->is_success(), "want a bad response"; is $mech->res->code, 404, "got 404"; }; @@ -345,7 +345,7 @@ FixMyStreet::override_config { }, ) { subtest $test->{desc} => sub { - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); my $visible = $mech->visible_form_values; is_deeply $visible, $test->{fields}, 'expected user'; @@ -381,7 +381,7 @@ FixMyStreet::override_config { SMS_AUTHENTICATION => 1, }, sub { subtest "Test edit user add verified phone" => sub { - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok( { with_fields => { phone => '+61491570157', phone_verified => 1, @@ -393,9 +393,9 @@ FixMyStreet::override_config { my $existing_user = $mech->create_user_ok('existing@example.com', name => 'Existing User'); $mech->create_problems_for_body(2, 2514, 'Title', { user => $existing_user }); my $count = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id })->count; - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok( { with_fields => { email => 'existing@example.com' } }, 'submit email change' ); - is $mech->uri->path, '/admin/user_edit/' . $existing_user->id, 'redirected'; + is $mech->uri->path, '/admin/users/' . $existing_user->id, 'redirected'; my $p = FixMyStreet::DB->resultset('Problem')->search({ user_id => $existing_user->id })->count; is $p, $count + 2, 'reports merged'; }; @@ -406,7 +406,7 @@ $user = $mech->create_user_ok('test@example.com', name => 'Test User'); subtest "Send login email from admin" => sub { $mech->email_count_is(0); - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok( { button => 'send_login_email' @@ -435,7 +435,7 @@ subtest "Send login email from admin" => sub { subtest "Send login email from admin for unverified email" => sub { $user->update( { email_verified => 0 } ); $mech->email_count_is(0); - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok( { button => 'send_login_email' @@ -473,7 +473,7 @@ subtest "Anonymizing user from admin" => sub { $mech->create_problems_for_body(4, 2237, 'Title'); my $count_p = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id })->count; my $count_u = FixMyStreet::DB->resultset('Comment')->search({ user_id => $user->id })->count; - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok({ button => 'anon_everywhere' }); my $c = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id, anonymous => 1 })->count; is $c, $count_p; @@ -484,7 +484,7 @@ subtest "Anonymizing user from admin" => sub { subtest "Hiding user's reports from admin" => sub { my $count_p = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id })->count; my $count_u = FixMyStreet::DB->resultset('Comment')->search({ user_id => $user->id })->count; - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok({ button => 'hide_everywhere' }); my $c = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id, state => 'hidden' })->count; is $c, $count_p; @@ -497,7 +497,7 @@ subtest "Logging user out" => sub { $mech2->log_in_ok($user->email); $mech2->logged_in_ok; - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok({ button => 'logout_everywhere' }, 'Logging user out'); $mech2->not_logged_in_ok; }; @@ -506,7 +506,7 @@ subtest "Removing account from admin" => sub { $mech->create_problems_for_body(4, 2237, 'Title'); my $count_p = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id })->count; my $count_u = FixMyStreet::DB->resultset('Comment')->search({ user_id => $user->id })->count; - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->submit_form_ok({ button => 'remove_account' }, 'Removing account'); my $c = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id, anonymous => 1, name => '' })->count; is $c, $count_p, 'All reports anon/nameless'; @@ -519,7 +519,7 @@ subtest "Removing account from admin" => sub { }; subtest "can view list of user's alerts" => sub { - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->content_lacks("User's alerts", 'no list of alerts'); $mech->create_problems_for_body(1, 2514, 'Title', { user => $user }); @@ -532,13 +532,13 @@ subtest "can view list of user's alerts" => sub { }); - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); $mech->content_contains("User's alerts", 'has list of alerts'); $mech->content_contains($alert->id, 'lists alert'); }; subtest "can edit list of user's alerts" => sub { - $mech->get_ok( '/admin/user_edit/' . $user->id ); + $mech->get_ok( '/admin/users/' . $user->id ); my $alert = FixMyStreet::DB->resultset('Alert')->search({ user_id => $user->id, diff --git a/t/app/controller/alert_new.t b/t/app/controller/alert_new.t index 49efc4efa..f816c5317 100644 --- a/t/app/controller/alert_new.t +++ b/t/app/controller/alert_new.t @@ -326,11 +326,7 @@ subtest "Test two-tier council alerts" => sub { }; subtest "Test normal alert signups and that alerts are sent" => sub { - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); - my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User' ); - my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User' ); for my $alert ( @@ -375,64 +371,29 @@ subtest "Test normal alert signups and that alerts are sent" => sub { my $dt_parser = FixMyStreet::App->model('DB')->schema->storage->datetime_parser; my $report_time = '2011-03-01 12:00:00'; - my $report = FixMyStreet::App->model('DB::Problem')->find_or_create( { + my ($report) = $mech->create_problems_for_body(1, 1, 'Testing', { + dt => $dt, + user => $user1, postcode => 'EH1 1BB', - bodies_str => '1', areas => ',11808,135007,14419,134935,2651,20728,', category => 'Street lighting', - title => 'Testing', - detail => 'Testing Detail', - used_map => 1, - name => $user1->name, - anonymous => 0, state => 'fixed - user', - confirmed => $dt_parser->format_datetime($dt), lastupdate => $dt_parser->format_datetime($dt), whensent => $dt_parser->format_datetime($dt->clone->add( minutes => 5 )), - lang => 'en-gb', - service => '', - cobrand => 'default', - cobrand_data => '', - send_questionnaire => 1, latitude => '55.951963', longitude => '-3.189944', - user_id => $user1->id, - } ); + }); my $report_id = $report->id; ok $report, "created test report - $report_id"; - my $alert = FixMyStreet::App->model('DB::Alert')->create( { - parameter => $report_id, - alert_type => 'new_updates', - user => $user1, - } )->confirm; - ok $alert, 'created alert for reporter'; - - my $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report_id, - user_id => $user2->id, - name => 'Other User', - mark_fixed => 'false', - text => 'This is some update text', - state => 'confirmed', - confirmed => $dt->clone->add( hours => 7 ), - anonymous => 'f', - } ); - my $update_id = $update->id; - ok $update, "created test update - $update_id"; - - $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report_id, - user_id => $user2->id, - name => 'Anonymous User', - mark_fixed => 'true', - text => 'This is some more update text', - state => 'confirmed', - confirmed => $dt->clone->add( hours => 8 ), - anonymous => 't', - } ); - $update_id = $update->id; - ok $update, "created test update - $update_id"; + subtest 'check signing up for alerts via report page' => sub { + $mech->log_in_ok($user1->email); + $mech->get_ok("/report/$report_id"); + $mech->submit_form_ok({ button => 'alert', with_fields => { type => 'updates' } }); + }; + + $mech->create_comment_for_problem($report, $user2, 'Other User', 'This is some update text', 'f', 'confirmed', undef, { confirmed => $dt->clone->add( hours => 7 ) }); + $mech->create_comment_for_problem($report, $user2, 'Anonymous User', 'This is some more update text', 't', 'confirmed', 'fixed - user', { confirmed => $dt->clone->add( hours => 8 ) }); FixMyStreet::override_config { MAPIT_URL => 'http://mapit.uk/', @@ -477,9 +438,6 @@ subtest "Test normal alert signups and that alerts are sent" => sub { }; subtest "Test alerts are not sent for no-text updates" => sub { - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); - my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User' ); my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User' ); my $user3 = $mech->create_user_ok('staff@example.com', name => 'Staff User', from_body => $gloucester ); @@ -555,31 +513,8 @@ subtest "Test alerts are not sent for no-text updates" => sub { } )->confirm; ok $alert, 'created alert for other user'; - my $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report_id, - user_id => $user3->id, - name => 'Staff User', - mark_fixed => 'false', - text => '', - state => 'confirmed', - confirmed => $dt->clone->add( hours => 9 ), - anonymous => 'f', - } ); - my $update_id = $update->id; - ok $update, "created test update from staff user - $update_id"; - - my $update2 = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report2_id, - user_id => $user3->id, - name => 'Staff User', - mark_fixed => 'false', - text => 'This is a normal update', - state => 'confirmed', - confirmed => $dt->clone->add( hours => 9 ), - anonymous => 'f', - } ); - my $update2_id = $update2->id; - ok $update2, "created test update from staff user - $update2_id"; + $mech->create_comment_for_problem($report, $user3, 'Staff User', '', 'f', 'confirmed', undef, { confirmed => $dt->clone->add( hours => 9 ) }); + $mech->create_comment_for_problem($report2, $user3, 'Staff User', 'This is a normal update', 'f', 'confirmed', undef, { confirmed => $dt->clone->add( hours => 9 ) }); $mech->clear_emails_ok; FixMyStreet::override_config { @@ -596,9 +531,6 @@ subtest "Test alerts are not sent for no-text updates" => sub { }; subtest "Test no marked as confirmed added to alerts" => sub { - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); - my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User' ); my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User' ); my $user3 = $mech->create_user_ok('staff@example.com', name => 'Staff User', from_body => $gloucester ); @@ -640,19 +572,7 @@ subtest "Test no marked as confirmed added to alerts" => sub { } )->confirm; ok $alert, 'created alert for other user'; - my $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report_id, - user_id => $user3->id, - name => 'Staff User', - mark_fixed => 'false', - text => 'this is update', - state => 'confirmed', - problem_state => 'confirmed', - confirmed => $dt->clone->add( hours => 9 ), - anonymous => 'f', - } ); - my $update_id = $update->id; - ok $update, "created test update from staff user - $update_id"; + $mech->create_comment_for_problem($report, $user3, 'Staff User', 'this is update', 'f', 'confirmed', 'confirmed', { confirmed => $dt->clone->add( hours => 9 ) }); $mech->clear_emails_ok; FixMyStreet::override_config { @@ -694,9 +614,6 @@ for my $test ( }, ) { subtest $test->{desc} => sub { - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); - my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User' ); my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User' ); my $user3 = $mech->create_user_ok('staff@example.com', name => 'Staff User', from_body => $gloucester ); @@ -738,19 +655,7 @@ for my $test ( } )->confirm; ok $alert, 'created alert for other user'; - my $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report_id, - user_id => $user3->id, - name => 'Staff User', - mark_fixed => 'false', - text => $test->{update_text}, - problem_state => $test->{problem_state}, - state => 'confirmed', - confirmed => $dt->clone->add( hours => 9 ), - anonymous => 'f', - } ); - my $update_id = $update->id; - ok $update, "created test update from staff user - $update_id"; + $mech->create_comment_for_problem($report, $user3, 'Staff User', $test->{update_text}, 'f', 'confirmed', $test->{problem_state}, { confirmed => $dt->clone->add( hours => 9 ) }); $mech->clear_emails_ok; FixMyStreet::override_config { @@ -777,11 +682,7 @@ for my $test ( } subtest "Test signature template is used from cobrand" => sub { - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); - my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User' ); - my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User' ); my $dt = DateTime->now(time_zone => 'Europe/London')->add(days => 2); @@ -824,19 +725,7 @@ subtest "Test signature template is used from cobrand" => sub { my $ret = $alert->confirm; ok $ret, 'created alert for reporter'; - my $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report_id, - user_id => $user2->id, - name => 'Other User', - mark_fixed => 'false', - text => 'This is some update text', - state => 'confirmed', - confirmed => $dt->clone->add( hours => 7 ), - anonymous => 'f', - } ); - my $update_id = $update->id; - ok $update, "created test update - $update_id"; - + $mech->create_comment_for_problem($report, $user2, 'Other User', 'This is some update text', 'f', 'confirmed', undef, { confirmed => $dt->clone->add( hours => 7 ) }); $mech->clear_emails_ok; FixMyStreet::override_config { @@ -850,18 +739,7 @@ subtest "Test signature template is used from cobrand" => sub { like $email, qr/All the best/, 'default signature used'; unlike $email, qr/twitter.com/, 'nothing from fixmystreet signature'; - $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report_id, - user_id => $user2->id, - name => 'Anonymous User', - mark_fixed => 'true', - text => 'This is some more update text', - state => 'confirmed', - confirmed => $dt->clone->add( hours => 8 ), - anonymous => 't', - } ); - $update_id = $update->id; - ok $update, "created test update - $update_id"; + $mech->create_comment_for_problem($report, $user2, 'Anonymous User', 'This is some more update text', 't', 'confirmed', 'fixed - user', { confirmed => $dt->clone->add( hours => 8 ) }); $alert->cobrand('fixmystreet'); $alert->update; @@ -916,11 +794,7 @@ for my $test ( }, ) { subtest $test->{desc} => sub { - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); - my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User'); - my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User'); my $dt = DateTime->now->add( minutes => -30 ); @@ -984,13 +858,8 @@ for my $test ( } subtest 'check new updates alerts for non public reports only go to report owner' => sub { - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); - my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User'); - my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User'); - my $user3 = $mech->create_user_ok('updates@example.com', name => 'Update User'); my $dt = DateTime->now->add( minutes => -30 ); @@ -1023,16 +892,7 @@ subtest 'check new updates alerts for non public reports only go to report owner non_public => 1, } ); - my $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report->id, - user_id => $user3->id, - name => 'Anonymous User', - mark_fixed => 'false', - text => 'This is some more update text', - state => 'confirmed', - confirmed => $r_dt->clone->add( minutes => 8 ), - anonymous => 't', - } ); + $mech->create_comment_for_problem($report, $user3, 'Anonymous User', 'This is some more update text', 't', 'confirmed', undef, { confirmed => $r_dt->clone->add( minutes => 8 ) }); my $alert_user1 = FixMyStreet::App->model('DB::Alert')->create( { user => $user1, @@ -1072,18 +932,14 @@ subtest 'check new updates alerts for non public reports only go to report owner $mech->delete_user( $user3 ); }; -subtest 'check setting inlude dates in new updates cobrand option' => sub { +subtest 'check setting include dates in new updates cobrand option' => sub { my $include_date_in_alert_override= Sub::Override->new( "FixMyStreet::Cobrand::Default::include_time_in_update_alerts", sub { return 1; } ); - $mech->delete_user( 'reporter@example.com' ); - $mech->delete_user( 'alerts@example.com' ); my $user1 = $mech->create_user_ok('reporter@example.com', name => 'Reporter User'); - my $user2 = $mech->create_user_ok('alerts@example.com', name => 'Alert User'); - my $user3 = $mech->create_user_ok('updates@example.com', name => 'Update User'); my $dt = DateTime->now->add( minutes => -30 ); @@ -1115,16 +971,7 @@ subtest 'check setting inlude dates in new updates cobrand option' => sub { user_id => $user2->id, } ); - my $update = FixMyStreet::App->model('DB::Comment')->create( { - problem_id => $report->id, - user_id => $user3->id, - name => 'Anonymous User', - mark_fixed => 'false', - text => 'This is some more update text', - state => 'confirmed', - confirmed => $r_dt->clone->add( minutes => 8 ), - anonymous => 't', - } ); + my $update = $mech->create_comment_for_problem($report, $user3, 'Anonymous User', 'This is some more update text', 't', 'confirmed', undef, { confirmed => $r_dt->clone->add( minutes => 8 ) }); my $alert_user1 = FixMyStreet::App->model('DB::Alert')->create( { user => $user1, diff --git a/t/cobrand/bathnes.t b/t/cobrand/bathnes.t index 59e0d5246..4db2f058c 100644 --- a/t/cobrand/bathnes.t +++ b/t/cobrand/bathnes.t @@ -105,7 +105,7 @@ subtest "Custom CSV fields permission can be granted" => sub { is $counciluser->user_body_permissions->count, 0, 'counciluser has no permissions'; - $mech->get_ok("/admin/user_edit/" . $counciluser->id); + $mech->get_ok("/admin/users/" . $counciluser->id); $mech->content_contains('Extra columns in CSV export'); $mech->submit_form_ok( { with_fields => { diff --git a/t/cobrand/bromley.t b/t/cobrand/bromley.t index 05581bdfe..129531fcb 100644 --- a/t/cobrand/bromley.t +++ b/t/cobrand/bromley.t @@ -220,7 +220,7 @@ subtest 'check special subcategories in admin' => sub { ALLOWED_COBRANDS => 'bromley', MAPIT_URL => 'http://mapit.uk/', }, sub { - $mech->get_ok('/admin/user_edit/' . $user->id); + $mech->get_ok('/admin/users/' . $user->id); $mech->submit_form_ok({ with_fields => { 'contacts['.$contact->id.']' => 1, 'contacts[BLUE]' => 1 } }); }; $user->discard_changes; diff --git a/t/cobrand/zurich.t b/t/cobrand/zurich.t index 1faa0adf2..ee2724a07 100644 --- a/t/cobrand/zurich.t +++ b/t/cobrand/zurich.t @@ -1092,7 +1092,7 @@ FixMyStreet::override_config { }, sub { subtest 'users at the top level can be edited' => sub { $mech->log_in_ok( $superuser->email ); - $mech->get_ok('/admin/user_edit/' . $superuser->id ); + $mech->get_ok('/admin/users/' . $superuser->id ); }; }; diff --git a/templates/email/fixamingata/inactive-account.html b/templates/email/fixamingata/inactive-account.html new file mode 100644 index 000000000..20c309095 --- /dev/null +++ b/templates/email/fixamingata/inactive-account.html @@ -0,0 +1,26 @@ +[% + +email_summary = "Ditt inaktiva konto på " _ site_name; +email_columns = 1; + +PROCESS '_email_settings.html'; + +INCLUDE '_email_top.html'; + +%] + +<th style="[% td_style %][% only_column_style %]"> + <h1 style="[% h1_style %]">Ditt inaktiva konto</h1> + <p style="[% p_style %]"> +Ditt konto på [% site_name %] har varit inaktivt i [% email_from %] +[% nget('month', 'months', email_from) %], och vi tar automatiskt bort +konton som har varit inaktiva i [% anonymize_from %] +[% nget('month', 'months', anonymize_from) %]. Om du vill behålla ditt +konto, vänligen logga in för att hålla kontot aktivt: +</p> + <p style="margin: 20px auto; text-align: center"> + <a style="[% button_style %]" href="[% url %]">Besök [% site_name %]</a> + </p> + <p style="[% p_style %]">Tack för att du använder [% site_name %]!</p> + +[% INCLUDE '_email_bottom.html' %] diff --git a/templates/email/fixamingata/inactive-account.txt b/templates/email/fixamingata/inactive-account.txt new file mode 100644 index 000000000..b7b74c077 --- /dev/null +++ b/templates/email/fixamingata/inactive-account.txt @@ -0,0 +1,17 @@ +Subject: Ditt inaktiva konto på [% site_name %] + +Hej [% user.name %], + +Ditt konto på [% site_name %] har varit inaktivt i [% email_from %] +[% nget('month', 'months', email_from) %], och vi tar automatiskt bort +konton som har varit inaktiva i [% anonymize_from %] +[% nget('month', 'months', anonymize_from) %]. Om du vill behålla ditt +konto, vänligen logga in för att hålla kontot aktivt: + +[% url %] + +Tack för att du använder [% site_name %]! + +[% INCLUDE 'signature.txt' %] + +Det går inte att svara på detta mail. diff --git a/templates/web/base/admin/_translations.html b/templates/web/base/admin/bodies/_translations.html index d8f7d52fb..d8f7d52fb 100644 --- a/templates/web/base/admin/_translations.html +++ b/templates/web/base/admin/bodies/_translations.html diff --git a/templates/web/base/admin/body.html b/templates/web/base/admin/bodies/body.html index 37ab24496..afd2eff46 100644 --- a/templates/web/base/admin/body.html +++ b/templates/web/base/admin/bodies/body.html @@ -29,7 +29,7 @@ [% END %] <br> <a href="[% c.uri_for_email(body.url) %]" class="admin-offsite-link">[% loc('List all reported problems' ) %]</a> | - <a href="[% c.uri_for( 'body', body_id, { text => 1 } ) %]">[% loc('Text only version') %]</a> + <a href="[% c.uri_for_action( 'admin/bodies/edit', [ body_id ], { text => 1 } ) %]">[% loc('Text only version') %]</a> </p> @@ -65,7 +65,7 @@ </p> [% END %] -<form method="post" action="[% c.uri_for('body', body_id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> +<form method="post" action="[% c.uri_for_action('admin/bodies/edit', [ body_id ] ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> <table cellspacing="0" cellpadding="2" border="1" id="admin_contacts"> <tr> @@ -79,7 +79,7 @@ </tr> [% WHILE ( cat = contacts.next ) %] <tr [% IF cat.state == 'deleted' %]class="is-deleted"[% END %]> - <td class="contact-category"><a href="[% c.uri_for( 'body', body_id, cat.category ) %]">[% cat.category_display | html %]</a> + <td class="contact-category"><a href="[% c.uri_for_action( '/admin/bodies/edit', [ body_id ], cat.category ) %]">[% cat.category_display | html %]</a> <br>[% cat.email | html %]</td> <td> [% cat.state %] @@ -130,16 +130,16 @@ <div class="fms-admin-warning"> [% errors.values.join('<br>') %] </div> - [% INCLUDE 'admin/contact-form.html' translations=contact_translations %] + [% INCLUDE 'admin/bodies/contact-form.html' translations=contact_translations %] [% ELSE %] - [% INCLUDE 'admin/contact-form.html' translations={} %] + [% INCLUDE 'admin/bodies/contact-form.html' translations={} %] [% END %] </div> [% IF NOT errors and c.user.is_superuser %] <div class="admin-box"> <h2>[% loc('Edit body details') %]</h2> - [% INCLUDE 'admin/body-form.html' %] + [% INCLUDE 'admin/bodies/form.html' %] </div> [% END %][%# Only show all the above if no errors with category form %] diff --git a/templates/web/base/admin/category_edit.html b/templates/web/base/admin/bodies/category.html index 7ae4e59b4..2f789ed81 100644 --- a/templates/web/base/admin/category_edit.html +++ b/templates/web/base/admin/bodies/category.html @@ -19,7 +19,7 @@ [% END %] </p> -[% INCLUDE 'admin/contact-form.html' %] +[% INCLUDE 'admin/bodies/contact-form.html' %] <h2>[% loc('History') %]</h2> <table border="1"> diff --git a/templates/web/base/admin/contact-form.html b/templates/web/base/admin/bodies/contact-form.html index 7e2830e9c..c55c5c036 100644 --- a/templates/web/base/admin/contact-form.html +++ b/templates/web/base/admin/bodies/contact-form.html @@ -1,4 +1,4 @@ -<form method="post" action="[% c.uri_for('body', body_id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" id="category_edit"> +<form method="post" action="[% c.uri_for_action('admin/bodies/edit', [ body_id ] ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" id="category_edit"> [% IF contact.in_storage %] <p> @@ -20,7 +20,7 @@ </p> [% END %] - [% INCLUDE 'admin/_translations.html' %] + [% INCLUDE 'admin/bodies/_translations.html' %] <div class="admin-hint"> <p> @@ -117,7 +117,7 @@ as well.") %] </select> </p> - [% INCLUDE 'admin/open311-form-fields.html', object = contact%] + [% INCLUDE 'admin/bodies/open311-form-fields.html', object = contact%] [% END %] [% IF c.cobrand.enable_category_groups %] diff --git a/templates/web/base/admin/edit-league.html b/templates/web/base/admin/bodies/edit-league.html index 4f31eeb2e..4f31eeb2e 100644 --- a/templates/web/base/admin/edit-league.html +++ b/templates/web/base/admin/bodies/edit-league.html diff --git a/templates/web/base/admin/body-form.html b/templates/web/base/admin/bodies/form.html index 958ea5d78..0b162c274 100644 --- a/templates/web/base/admin/body-form.html +++ b/templates/web/base/admin/bodies/form.html @@ -1,4 +1,4 @@ - <form method="post" action="[% body ? c.uri_for('body', body.id) : c.uri_for('bodies') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> + <form method="post" action="[% body ? c.uri_for_action('admin/bodies/edit', [ body.id ]) : c.uri_for_action('admin/bodies/index') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> <div class="fms-admin-info"> [% loc( "Add a <strong>body</strong> for each administrative body, such as a council or department @@ -25,7 +25,7 @@ <input type="text" class="form-control" name="name" id="name" value="[% body.name | html %]" size="50"> </p> - [% INCLUDE 'admin/_translations.html' %] + [% INCLUDE 'admin/bodies/_translations.html' %] <div class="admin-hint"> <p> @@ -114,7 +114,7 @@ </select> </p> - [% INCLUDE 'admin/open311-form-fields.html', object = body, show_body_fields = 1 %] + [% INCLUDE 'admin/bodies/open311-form-fields.html', object = body, show_body_fields = 1 %] <div class="admin-hint"> <p> diff --git a/templates/web/base/admin/bodies.html b/templates/web/base/admin/bodies/index.html index 33ae5b25a..84e1cda3c 100644 --- a/templates/web/base/admin/bodies.html +++ b/templates/web/base/admin/bodies/index.html @@ -39,12 +39,12 @@ [% IF c.cobrand.moniker == 'zurich' %] [% FILTER repeat(4*body.indent_level) %] [% END %] [% IF admin_type == 'super' %] - <a href="[% c.uri_for( 'body', id ) %]">[% body.name | html %]</a> + <a href="[% c.uri_for_action( 'admin/bodies/edit', [ id ] ) %]">[% body.name | html %]</a> [% ELSE %] [% body.name | html %] [% END %] [% ELSE %] [%# not Zurich: all bodies should be links %] - <a href="[% c.uri_for( 'body', id ) %]">[% body.name | html%]</a> + <a href="[% c.uri_for_action( 'admin/bodies/edit', [ id ] ) %]">[% body.name | html %]</a> [%- IF body.parent %], [% body.parent.name | html %][% END -%] [% END %] </td> @@ -76,10 +76,10 @@ [% IF (c.cobrand.moniker == 'zurich' AND admin_type == 'super') OR c.user.is_superuser %] <div class="admin-box"> <h2>[% loc('Add body') %]</h2> - [% INCLUDE 'admin/body-form.html', body='' %] + [% INCLUDE 'admin/bodies/form.html', body='' %] </div> [% END %] -[% INCLUDE 'admin/edit-league.html' %] +[% INCLUDE 'admin/bodies/edit-league.html' %] [% INCLUDE 'admin/footer.html' %] diff --git a/templates/web/base/admin/open311-form-fields.html b/templates/web/base/admin/bodies/open311-form-fields.html index b716cf175..b716cf175 100644 --- a/templates/web/base/admin/open311-form-fields.html +++ b/templates/web/base/admin/bodies/open311-form-fields.html diff --git a/templates/web/base/admin/user-alerts.html b/templates/web/base/admin/users/alerts.html index 6058e595b..6058e595b 100644 --- a/templates/web/base/admin/user-alerts.html +++ b/templates/web/base/admin/users/alerts.html diff --git a/templates/web/base/admin/user_edit.html b/templates/web/base/admin/users/edit.html index cc456914d..8dd2b926f 100644 --- a/templates/web/base/admin/user_edit.html +++ b/templates/web/base/admin/users/edit.html @@ -8,8 +8,8 @@ [% status_message %] -[% INCLUDE 'admin/user-form.html' %] +[% INCLUDE 'admin/users/form.html' %] -[% INCLUDE 'admin/user-alerts.html' %] +[% INCLUDE 'admin/users/alerts.html' %] [% INCLUDE 'admin/footer.html' %] diff --git a/templates/web/base/admin/user-form.html b/templates/web/base/admin/users/form.html index 735e15e87..7bc291419 100644 --- a/templates/web/base/admin/user-form.html +++ b/templates/web/base/admin/users/form.html @@ -1,4 +1,4 @@ -<form method="post" id="user_edit" action="[% c.uri_for( 'user_edit', user.id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> +<form method="post" id="user_edit" action="[% c.uri_for_action( 'admin/users/edit', user.id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> <input type="hidden" name="token" value="[% csrf_token %]" > <input type="hidden" name="submit" value="1" > @@ -191,7 +191,7 @@ </ul> [% END %] [% END %] - [% TRY %][% INCLUDE 'admin/user-form-extra-fields.html' %][% CATCH file %][% END %] + [% TRY %][% INCLUDE 'admin/users/form-extra-fields.html' %][% CATCH file %][% END %] </ul> <p> <input type="submit" class="btn" name="Submit changes" value="[% loc('Submit changes') %]" > diff --git a/templates/web/base/admin/user_import.html b/templates/web/base/admin/users/import.html index f866ed955..0c0b903d7 100644 --- a/templates/web/base/admin/user_import.html +++ b/templates/web/base/admin/users/import.html @@ -3,7 +3,7 @@ [% status_message %] -<form method="post" id="user_edit" action="[% c.uri_for( 'user_import' ) %]" enctype="multipart/form-data" accept-charset="utf-8"> +<form method="post" id="user_edit" enctype="multipart/form-data" accept-charset="utf-8"> <input type="hidden" name="token" value="[% csrf_token %]" > <input type="hidden" name="submit" value="1" > @@ -27,7 +27,7 @@ [% FOREACH user IN new_users %] <tr> <td> - <a href="[% c.uri_for_action( 'admin/user_edit', user.id ) %]"> + <a href="[% c.uri_for_action( 'admin/users/edit', user.id ) %]"> [% user.name %] </a> </td> @@ -50,7 +50,7 @@ [% FOREACH user IN existing_users %] <tr> <td> - <a href="[% c.uri_for_action( 'admin/user_edit', user.id ) %]"> + <a href="[% c.uri_for_action( 'admin/users/edit', user.id ) %]"> [% user.name %] </a> </td> diff --git a/templates/web/base/admin/users.html b/templates/web/base/admin/users/index.html index 6dfcf4204..e939f008b 100644 --- a/templates/web/base/admin/users.html +++ b/templates/web/base/admin/users/index.html @@ -4,7 +4,7 @@ <div class="fms-admin-info"> [% loc("User search finds matches in users' names and email addresses.") %] </div> -<form method="get" action="[% c.uri_for('users') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> +<form method="get" action="[% c.uri_for_action('admin/users/index') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> <p><label for="search">[% loc('Search:') %]</label> <input class="form-control" type="text" name="search" size="30" id="search" value="[% searched | html %]"> </form> @@ -24,14 +24,14 @@ [%- FOREACH user IN users %] <tr> <td>[% PROCESS value_or_nbsp value=user.name %]</td> - <td><a href="[% c.uri_for( 'reports', search => user.email ) %]">[% PROCESS value_or_nbsp value=user.email %]</a></td> + <td><a href="[% c.uri_for_action( 'admin/reports', search => user.email ) %]">[% PROCESS value_or_nbsp value=user.email %]</a></td> <td>[% PROCESS value_or_nbsp value=user.from_body.name %] [% IF user.is_superuser %] * [% END %] </td> [% IF c.cobrand.moniker != 'zurich' %] <td>[% user.flagged == 2 ? loc('User in abuse table') : user.flagged ? loc('Yes') : ' ' %]</td> [% END %] - <td>[% IF user.id %]<a href="[% c.uri_for( 'user_edit', user.id ) %]">[% loc('Edit') %]</a>[% END %]</td> + <td>[% IF user.id %]<a href="[% c.uri_for_action( 'admin/users/edit', user.id ) %]">[% loc('Edit') %]</a>[% END %]</td> </tr> [%- END -%] </table> @@ -46,9 +46,9 @@ [% IF NOT searched %] <h2>[% loc('Add user') %]</h2> -[% INCLUDE 'admin/user-form.html', user = '' %] +[% INCLUDE 'admin/users/form.html', user = '' %] [% ELSE %] -<a href="[% c.uri_for( c.controller('Admin').action_for('user_add') ) %]">[% loc('Add user') %]</a> +<a href="[% c.uri_for_action('admin/users/add') %]">[% loc('Add user') %]</a> [% END %] [% INCLUDE 'admin/footer.html' %] diff --git a/templates/web/base/js/translation_strings.html b/templates/web/base/js/translation_strings.html index 1b0735919..af3073f91 100644 --- a/templates/web/base/js/translation_strings.html +++ b/templates/web/base/js/translation_strings.html @@ -65,7 +65,7 @@ fixmystreet.password_minimum_length = [% c.cobrand.password_minimum_length %]; show_pins: '[% loc('Show pins') | replace("'", "\\'") %]', hide_pins: '[% loc('Hide pins') | replace("'", "\\'") %]', - upload_max_files_exceeded: '[% loc ('Whoa there Testino! Three photos are enough.') | replace("'", "\\'") %]', + upload_max_files_exceeded: '[% loc ('Sorry! You’ve hit the limit of images that can be attached to one report.') | replace("'", "\\'") %]', upload_default_message: '[% loc ('Drag and drop photos here or <u>click to upload</u>') | replace("'", "\\'") %]', upload_cancel_confirmation: '[% loc ('Are you sure you want to cancel this upload?') | replace("'", "\\'") %]', upload_invalid_file_type: '[% loc ('Please upload an image only') | replace("'", "\\'") %]', diff --git a/templates/web/base/report/display_tools.html b/templates/web/base/report/display_tools.html index be788a50d..4ba8c8b2c 100644 --- a/templates/web/base/report/display_tools.html +++ b/templates/web/base/report/display_tools.html @@ -43,13 +43,15 @@ </a> [% loc('Receive email when updates are left on this problem.' ) %]</p> <fieldset> + [% IF c.user_exists %] + <input class="green-btn" type="submit" name="alert" value="[% loc('Subscribe') %]"> + [% ELSE %] <label for="alert_rznvy">[% loc('Your email') %]</label> <div class="form-txt-submit-box"> - [% IF NOT c.user_exists %] <input type="email" class="form-control" name="rznvy" id="alert_rznvy" value="[% email | html %]" size="30"> - [% END %] - <input class="green-btn" type="submit" value="[% loc('Subscribe') %]"> + <input class="green-btn" type="submit" name="alert" value="[% loc('Subscribe') %]"> </div> + [% END %] <input type="hidden" name="token" value="[% csrf_token %]"> <input type="hidden" name="id" value="[% problem.id %]"> <input type="hidden" name="type" value="updates"> diff --git a/templates/web/zurich/admin/body.html b/templates/web/zurich/admin/bodies/body.html index 11be6eef7..aab038ff8 100644 --- a/templates/web/zurich/admin/body.html +++ b/templates/web/zurich/admin/bodies/body.html @@ -20,7 +20,7 @@ </tr> [% WHILE ( cat = contacts.next ) %] <tr[% IF cat.state == 'deleted' %] class="is-deleted"[% END %]> - <td><a href="[% c.uri_for( 'body', body_id, cat.category ) %]">[% cat.category_display %]</a></td> + <td><a href="[% c.uri_for_action( 'admin/bodies/edit', [ body_id ], cat.category ) %]">[% cat.category_display %]</a></td> <td>[% cat.email | html %]</td> <td>[% cat.editor %]</td> <td>[% cat.note | html %]</td> @@ -39,13 +39,13 @@ </div> [% END %] - [% INCLUDE 'admin/contact-form.html' %] + [% INCLUDE 'admin/bodies/contact-form.html' %] [% END %] [% IF NOT errors %] <h2>[% loc('Edit body details') %]</h2> - [% INCLUDE 'admin/body-form.html' %] + [% INCLUDE 'admin/bodies/form.html' %] [% END %] [% INCLUDE 'admin/footer.html' %] diff --git a/templates/web/zurich/admin/contact-form.html b/templates/web/zurich/admin/bodies/contact-form.html index 3840d46a4..7b59124fb 100644 --- a/templates/web/zurich/admin/contact-form.html +++ b/templates/web/zurich/admin/bodies/contact-form.html @@ -1,4 +1,4 @@ -<form method="post" action="[% c.uri_for('body', body_id ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" id="category_edit"> +<form method="post" action="[% c.uri_for_action('admin/bodies/edit', [ body_id ] ) %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" id="category_edit"> [% IF contact.in_storage %] <h1>[% contact.category_display | html %]</h1> diff --git a/templates/web/zurich/admin/edit-league.html b/templates/web/zurich/admin/bodies/edit-league.html index e69de29bb..e69de29bb 100644 --- a/templates/web/zurich/admin/edit-league.html +++ b/templates/web/zurich/admin/bodies/edit-league.html diff --git a/templates/web/zurich/admin/body-form.html b/templates/web/zurich/admin/bodies/form.html index 44adcbc72..b625efc44 100644 --- a/templates/web/zurich/admin/body-form.html +++ b/templates/web/zurich/admin/bodies/form.html @@ -1,4 +1,4 @@ - <form method="post" action="[% body ? c.uri_for('body', body.id) : c.uri_for('bodies') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> + <form method="post" action="[% body ? c.uri_for_action('admin/bodies/edit', [ body.id ]) : c.uri_for_action('admin/bodies/index') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8"> <p> <label for="name">[% loc('Name') %]</label> diff --git a/web/cobrands/fixamingata/base.scss b/web/cobrands/fixamingata/base.scss index ceb90f878..e0f5f6dfc 100644 --- a/web/cobrands/fixamingata/base.scss +++ b/web/cobrands/fixamingata/base.scss @@ -16,9 +16,8 @@ } #site-logo { - width: 185px; - height: 38px; - background: transparent url('images/site-logo.png') 0 50% no-repeat; + height: 35px; + @include svg-background-image("/cobrands/fixamingata/images/site-logo"); } #report-cta { diff --git a/web/cobrands/fixamingata/images/homepage-logo.png b/web/cobrands/fixamingata/images/homepage-logo.png Binary files differdeleted file mode 100644 index fb51ef381..000000000 --- a/web/cobrands/fixamingata/images/homepage-logo.png +++ /dev/null diff --git a/web/cobrands/fixamingata/images/site-logo.png b/web/cobrands/fixamingata/images/site-logo.png Binary files differindex 314c81843..ee783bf63 100644 --- a/web/cobrands/fixamingata/images/site-logo.png +++ b/web/cobrands/fixamingata/images/site-logo.png diff --git a/web/cobrands/fixamingata/images/site-logo.svg b/web/cobrands/fixamingata/images/site-logo.svg new file mode 100644 index 000000000..afe553c45 --- /dev/null +++ b/web/cobrands/fixamingata/images/site-logo.svg @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="175" + height="35" + viewBox="0 0 175 35" + version="1.1" + id="svg58" + sodipodi:docname="site-logo.svg" + inkscape:version="0.92.3 (2405546, 2018-03-11)"> + <metadata + id="metadata64"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs62" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1017" + id="namedview60" + showgrid="false" + inkscape:zoom="2.8284271" + inkscape:cx="194.41893" + inkscape:cy="-36.107995" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + inkscape:current-layer="svg58" /> + <path + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#20bbdf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.25977862;marker:none;enable-background:accumulate" + d="M 15.34217,0 C 6.8681255,0 0,6.868125 0,15.342163 0,22.4644 4.8508582,28.452911 11.430245,30.182384 L 15.19759,35 18.891269,30.272411 C 25.653673,28.67061 30.687063,22.594254 30.687063,15.342163 30.687063,6.868125 23.816215,0 15.34217,0 Z" + id="path2982-5-04-3-5" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.33306313;marker:none;enable-background:accumulate" + d="m 15.34217,5.7996882 c -5.262759,0 -9.5424784,4.2797118 -9.5424784,9.5424748 0,5.262758 4.2797194,9.545212 9.5424784,9.545212 5.262762,0 9.542481,-4.282454 9.542481,-9.545212 0,-5.262763 -4.279719,-9.5424748 -9.542481,-9.5424748 z m 0,1.3094308 c 4.555093,0 8.233051,3.67795 8.233051,8.233044 0,4.555089 -3.677958,8.235781 -8.233051,8.235781 -4.555091,0 -8.2330474,-3.680692 -8.2330474,-8.235781 0,-2.053567 0.7471266,-3.928225 1.9859684,-5.368671 l 2.935311,2.935311 0.605611,0.608343 0.310989,0.310988 1.440375,1.437645 c 0.01735,0.01735 0.03331,0.03392 0.05183,0.0491 7.1e-4,0.0011 0.0021,0 0.0028,0 0.01795,0.01462 0.03566,0.02834 0.05455,0.04092 7.11e-4,0 0.0021,-10e-4 0.0028,0 0.01888,0.01252 0.03762,0.025 0.05729,0.03545 0.02101,0.0113 0.041,0.02105 0.06274,0.03001 0.02108,0.0087 0.04113,0.01533 0.06274,0.02183 0.0442,0.01355 0.08816,0.02282 0.133668,0.02727 0.0444,0.0044 0.08928,0.0044 0.133677,0 0.06658,-0.0064 0.13329,-0.02311 0.196416,-0.0491 0.08429,-0.03467 0.162509,-0.08613 0.231874,-0.155498 0.01735,-0.01735 0.03392,-0.03606 0.0491,-0.05456 0.06069,-0.07407 0.104939,-0.156174 0.130942,-0.242786 0.01301,-0.04331 0.02023,-0.08638 0.02456,-0.130944 0.0042,-0.04368 0.0041,-0.08999 0,-0.133675 -8.6e-5,-0.001 0,-0.0018 0,-0.0029 -0.0043,-0.04456 -0.01156,-0.08763 -0.02456,-0.130944 -0.0063,-0.02087 -0.01628,-0.04238 -0.02453,-0.06274 0,-0.001 0,-0.0021 0,-0.0029 -0.0347,-0.08442 -0.08613,-0.162506 -0.155498,-0.231883 L 13.940297,12.834792 13.629309,12.5238 13.020962,11.915458 10.096567,8.991058 c 1.423604,-1.175769 3.2509,-1.882297 5.245913,-1.882297 z m -0.160956,2.905299 c -0.159384,0 -0.317064,0.06064 -0.439199,0.182772 l -1.413094,1.410368 0.608334,0.608338 1.852305,-1.852297 -0.166413,-0.166409 c -0.122123,-0.122128 -0.282538,-0.182772 -0.441933,-0.182772 z m 0.919335,0.660169 -1.852304,1.852298 1.437652,1.437648 c 0.443109,0.443114 0.443109,1.166398 0,1.609511 -0.443112,0.443111 -1.166398,0.443111 -1.609509,0 l -1.437651,-1.437651 -1.830471,1.830481 4.239284,4.239284 c 0,0 3.086397,0.323578 4.329303,-0.919334 1.242912,-1.242906 0.962981,-4.372953 0.962981,-4.372953 z m -2.520654,2.463364 c 0.08178,0 0.16402,0.03037 0.226417,0.09275 0.124796,0.124797 0.124796,0.328047 0,0.452844 -0.124797,0.124798 -0.328048,0.124798 -0.452845,0 -0.124797,-0.124797 -0.124797,-0.328047 0,-0.452844 0.0624,-0.0624 0.144639,-0.09275 0.226428,-0.09275 z m -1.860486,0.07912 -1.388545,1.388538 c -0.244261,0.244261 -0.244261,0.639605 0,0.883866 l 0.16641,0.166413 1.830472,-1.830482 z m 3.118082,1.175755 c 0.08178,0 0.161294,0.0331 0.223693,0.09548 0.124798,0.124797 0.124798,0.325317 0,0.45011 -0.124797,0.124801 -0.325316,0.124801 -0.450114,0 -0.124797,-0.124793 -0.124797,-0.325313 0,-0.45011 0.0624,-0.0624 0.144641,-0.09548 0.226421,-0.09548 z" + id="rect3772-9-2-4-0" + inkscape:connector-curvature="0" /> + <g + transform="matrix(0.28296061,0,0,0.28296061,-7.6390861,-12.525227)" + style="font-style:normal;font-weight:normal;font-size:75.55453491px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + id="text4249-3-9"> + <path + inkscape:connector-curvature="0" + d="m 148.41624,90.072222 v 9.973198 h 4.68438 v 32.79067 h -4.68438 v 10.04875 h 28.10628 v -10.04875 h -7.70656 v -9.59543 h 14.35536 v -10.04875 h -14.35536 v -13.14649 h 11.40874 v 6.72435 h 13.37315 V 90.072222 h -45.18161" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6079" /> + <path + inkscape:connector-curvature="0" + d="m 198.78632,94.076612 c 0,3.626614 3.39996,7.631008 8.15989,7.631008 5.36437,0 8.311,-3.475512 8.311,-7.631008 0,-3.475505 -3.09774,-7.706562 -8.311,-7.706562 -5.28881,0 -8.15989,4.231057 -8.15989,7.706562 m 19.03974,48.808228 v -9.89764 h -3.62662 v -27.87963 h -17.52865 v 9.82209 h 3.17329 v 18.05754 h -3.17329 v 9.89764 h 21.15527" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6081" /> + <path + inkscape:connector-curvature="0" + d="m 248.05378,105.10757 v 9.44432 h 5.06215 l -5.66659,5.74215 -5.28882,-5.74215 h 3.55107 v -9.44432 h -24.47967 v 9.44432 h 3.39995 l 9.36876,10.27542 -7.70656,8.68877 h -5.06215 v 9.36876 h 19.34196 v -9.36876 h -4.91105 l 5.43993,-6.11992 5.74214,6.11992 h -3.55106 v 9.36876 h 24.1019 v -9.36876 h -3.55107 l -9.51987,-10.87985 8.00878,-8.08434 h 4.9866 v -9.44432 h -19.2664" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6083" /> + <path + inkscape:connector-curvature="0" + d="m 274.60648,117.04519 c 2.87107,-1.58664 6.79991,-3.47551 11.25763,-3.47551 4.07994,0 7.17768,0.83111 7.17768,4.23106 v 2.64441 c -0.37778,-0.45333 -3.02219,-2.64441 -7.4799,-2.64441 -8.53766,0 -15.71535,5.36438 -15.71535,13.90203 0,8.76432 7.70657,12.61761 14.88425,12.61761 2.71996,0 7.40434,-0.98221 10.19986,-4.83549 l 1.81331,3.39995 h 16.16867 v -9.67098 h -4.53327 v -14.73313 c 0,-12.16427 -11.40875,-15.56424 -20.47528,-15.56424 -6.95101,0 -13.44871,3.55107 -16.47089,5.36437 l 3.17329,8.76433 m 18.43531,14.9598 c -0.67999,1.13332 -2.56886,2.4933 -4.83549,2.4933 -2.26664,0 -4.00439,-1.43554 -4.00439,-3.55106 0,-2.34219 1.35998,-4.00439 4.38216,-4.00439 3.17329,0 4.23105,1.8133 4.45772,2.19108 v 2.87107" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6085" /> + <path + inkscape:connector-curvature="0" + d="m 368.64236,103.36982 v 4.45772 25.23521 h -4.23105 v 9.82209 h 23.87523 v -9.82209 h -4.1555 V 99.818757 h 4.1555 v -9.746535 h -28.78628 l -7.25323,28.257398 -7.63101,-28.257398 h -28.63517 v 9.746535 h 4.00439 v 33.319553 h -4.07994 v 9.74653 h 20.47528 v -9.74653 h -4.30661 v -29.76849 h 0.30222 c 0.22666,3.77772 1.20887,8.08434 1.58664,9.36876 l 8.46211,30.14626 h 15.7909 l 7.93322,-29.39071 c 0.52888,-1.73775 1.58665,-6.72436 2.19108,-10.12431 h 0.30222" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6087" /> + <path + inkscape:connector-curvature="0" + d="m 394.16563,94.076612 c 0,3.626614 3.39996,7.631008 8.15989,7.631008 5.36437,0 8.311,-3.475512 8.311,-7.631008 0,-3.475505 -3.09774,-7.706562 -8.311,-7.706562 -5.28881,0 -8.15989,4.231057 -8.15989,7.706562 m 19.03974,48.808228 v -9.89764 h -3.62661 V 105.10757 H 392.0501 v 9.82209 h 3.17329 v 18.05754 h -3.17329 v 9.89764 h 21.15527" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6089" /> + <path + inkscape:connector-curvature="0" + d="m 440.25979,142.88484 v -9.89764 h -5.43993 v -10.72875 c 0,-3.24884 0.90666,-7.02657 4.75994,-7.02657 4.45771,0 4.23105,4.45772 4.23105,7.02657 v 20.62639 h 19.03974 v -9.89764 h -3.55106 v -12.61761 c 0,-7.93322 -1.96443,-16.622 -13.67537,-16.622 -4.75993,0 -8.23545,2.49331 -10.8043,6.04437 l -1.73775,-4.68439 h -16.09312 v 9.82209 h 3.17329 v 18.05754 h -3.17329 v 9.89764 h 23.2708" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6091" /> + <path + inkscape:connector-curvature="0" + d="m 503.30889,142.88484 h 15.11091 v -18.88863 h 3.02218 v -8.08434 h -25.08411 v 8.08434 h 4.60883 v 3.17329 c -1.13332,1.96441 -2.94663,3.70217 -7.25324,3.70217 -6.57323,0 -10.35097,-6.19548 -10.35097,-13.90203 0,-7.55545 4.23106,-14.20426 10.35097,-14.20426 4.45772,0 8.38656,3.47552 8.61322,9.59543 h 13.52426 V 90.072222 h -11.25762 l -1.58665,5.439927 c -1.20887,-1.208872 -3.77774,-6.346581 -14.73313,-6.346581 -11.7865,0 -23.19525,11.333192 -23.19525,27.804072 0,16.31976 10.5021,26.59519 23.72413,26.59519 5.81769,0 10.50208,-2.64441 12.16428,-5.43992 l 2.34219,4.75993" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6093" /> + <path + inkscape:connector-curvature="0" + d="m 528.42249,117.04519 c 2.87107,-1.58664 6.79991,-3.47551 11.25763,-3.47551 4.07994,0 7.17768,0.83111 7.17768,4.23106 v 2.64441 c -0.37778,-0.45333 -3.02219,-2.64441 -7.4799,-2.64441 -8.53766,0 -15.71535,5.36438 -15.71535,13.90203 0,8.76432 7.70657,12.61761 14.88425,12.61761 2.71996,0 7.40434,-0.98221 10.19986,-4.83549 l 1.81331,3.39995 h 16.16867 v -9.67098 h -4.53327 v -14.73313 c 0,-12.16427 -11.40875,-15.56424 -20.47528,-15.56424 -6.95101,0 -13.44871,3.55107 -16.47089,5.36437 l 3.17329,8.76433 m 18.43531,14.9598 c -0.67999,1.13332 -2.56886,2.4933 -4.83549,2.4933 -2.26664,0 -4.00439,-1.43554 -4.00439,-3.55106 0,-2.34219 1.35998,-4.00439 4.38216,-4.00439 3.17329,0 4.23105,1.8133 4.45772,2.19108 v 2.87107" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6095" /> + <path + inkscape:connector-curvature="0" + d="m 568.96578,105.10757 v 9.82209 h 4.07994 v 13.44871 c 0.22666,9.67097 0.60445,15.86645 11.93762,15.86645 1.73775,0 6.87546,-0.52888 9.1421,-1.43553 v -9.67098 c -1.43554,0.60443 -3.77773,1.28442 -4.75994,1.28442 -2.94662,0 -2.41774,-3.47551 -2.41774,-6.04436 v -13.44871 h 7.17768 v -9.82209 h -7.17768 V 93.69884 l -13.90204,4.457717 v 6.951013 h -4.07994" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6097" /> + <path + inkscape:connector-curvature="0" + d="m 601.17325,117.04519 c 2.87107,-1.58664 6.79992,-3.47551 11.25763,-3.47551 4.07994,0 7.17768,0.83111 7.17768,4.23106 v 2.64441 c -0.37777,-0.45333 -3.02219,-2.64441 -7.4799,-2.64441 -8.53765,0 -15.71534,5.36438 -15.71534,13.90203 0,8.76432 7.70657,12.61761 14.88424,12.61761 2.71996,0 7.40435,-0.98221 10.19986,-4.83549 l 1.81331,3.39995 h 16.16867 v -9.67098 h -4.53327 v -14.73313 c 0,-12.16427 -11.40874,-15.56424 -20.47528,-15.56424 -6.95101,0 -13.44871,3.55107 -16.47089,5.36437 l 3.17329,8.76433 m 18.43531,14.9598 c -0.67999,1.13332 -2.56886,2.4933 -4.83549,2.4933 -2.26663,0 -4.00439,-1.43554 -4.00439,-3.55106 0,-2.34219 1.35998,-4.00439 4.38216,-4.00439 3.17329,0 4.23106,1.8133 4.45772,2.19108 v 2.87107" + style="font-variant:normal;font-stretch:normal;line-height:125%;font-family:ChunkFive;-inkscape-font-specification:ChunkFive;text-align:start;writing-mode:lr-tb;text-anchor:start" + id="path6099" /> + </g> +</svg> diff --git a/web/cobrands/fixamingata/layout.scss b/web/cobrands/fixamingata/layout.scss index 60e72859b..909212171 100644 --- a/web/cobrands/fixamingata/layout.scss +++ b/web/cobrands/fixamingata/layout.scss @@ -80,7 +80,8 @@ body.frontpage { #site-logo { width: 300px; height: 55px; - background: transparent url('images/homepage-logo.png') 0 50% no-repeat; + background-size: 275px 55px; + @include svg-background-image("/cobrands/fixamingata/images/site-logo"); } #main-nav { |