diff options
40 files changed, 821 insertions, 353 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 079740aed..848a70df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,10 @@ - Nicer Open311 errors. #2078 - Deleted body categories now hidden by default #1962 - Display contents of report's extra field #1809 + - Store user creation and last active times. + - Add scripts to anonymize inactive users and reports, + email inactive users, or to close reports to new updates. + - Admin ability to close reports to new updates. #43 - Development improvements: - Add HTML email previewer. - Add CORS header to Open311 output. #2022 diff --git a/bin/expire-sessions b/bin/expire-sessions index 8cfdd57e3..375ba4c6f 100755 --- a/bin/expire-sessions +++ b/bin/expire-sessions @@ -1,11 +1,11 @@ #!/usr/bin/env perl # expire-sessions: Run regularly to remove old sessions (plus -# can set up data for 'log user out' admin functionality). +# can set up data for 'log user out' admin functionality, and +# inactive user processing). -use strict; +use v5.14; use warnings; -require 5.8.0; BEGIN { use File::Basename qw(dirname); @@ -17,55 +17,46 @@ BEGIN { use FixMyStreet::DB; use Getopt::Long; use List::Util qw(uniq); -use MIME::Base64; -use Storable; GetOptions( + # Update sessions to make sure all present in User objects 'init' => \my $init, ); my $rs = FixMyStreet::DB->resultset("Session"); +my $now = time(); # Delete expired sessions (including from in User object) -while (my $session = $rs->search({ expires => { '<', time() } })->next) { - if (my $user = get_user($session)) { - my $id = get_id($session); - my $sessions = $user->get_extra_metadata('sessions'); - my @new_sessions = grep { $_ ne $id } @$sessions; - update_user_sessions($user, \@new_sessions) if @new_sessions != @$sessions; - } - $session->delete; -} - -if ($init) { - # Update sessions to make sure all present in User objects - print "Setting up sessions in user objects\n"; - while (my $session = $rs->next) { - my $user = get_user($session) or next; - my $id = get_id($session); +# And update last active time of current sessions +while (my $session = $rs->next) { + my $id = $session->id_code; + my $user = $session->user; + my $expires = $session->expires; + if (!$expires || $expires < $now) { + if ($user) { + my $sessions = $user->get_extra_metadata('sessions'); + my @new_sessions = grep { $_ ne $id } @$sessions; + update_user_sessions($user, \@new_sessions) if @new_sessions != @$sessions; + } + $session->delete; + } elsif ($user && $init) { my $sessions = $user->get_extra_metadata('sessions'); my @new_sessions = uniq @$sessions, $id; update_user_sessions($user, \@new_sessions) if @new_sessions != @$sessions; } + if ($user) { + update_user_last_active($user, $expires); + $user->update; + } } # --- -sub get_user { - my $session = shift; - return unless $session->session_data; - my $data = Storable::thaw(MIME::Base64::decode($session->session_data)); - return unless $data->{__user}; - my $user = FixMyStreet::DB->resultset("User")->find($data->{__user}{id}); - return $user; -} - -sub get_id { - my $session = shift; - my $id = $session->id; - $id =~ s/^session://; - $id =~ s/\s+$//; - return $id; +sub update_user_last_active { + my ($user, $expires) = @_; + return unless $expires; + my $t = DateTime->from_epoch(epoch => $expires)->subtract(weeks => 4); + $user->set_last_active($t) if !$user->last_active || $user->last_active < $t; } sub update_user_sessions { @@ -75,5 +66,4 @@ sub update_user_sessions { } else { $user->unset_extra_metadata('sessions'); } - $user->update; } diff --git a/bin/process-inactive-accounts b/bin/process-inactive-accounts new file mode 100755 index 000000000..3df200d3d --- /dev/null +++ b/bin/process-inactive-accounts @@ -0,0 +1,43 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../setenv.pl"; +} + +use Getopt::Long; +use FixMyStreet::Script::Inactive; +use Pod::Usage; + +my %h; +GetOptions(\%h, 'anonymize=i', 'email=i', 'verbose|v', 'help|h', 'dry-run|n'); +pod2usage(0) if $h{help}; +pod2usage(1) if !$h{anonymize}; +pod2usage("Anonymize time must be greater than email time") + if $h{email} && $h{email} >= $h{anonymize}; + +FixMyStreet::Script::Inactive->new(%h)->users; + +__END__ + +=head1 NAME + +process-inactive-accounts - deal with anonymizing old inactive accounts + +=head1 SYNOPSIS + +process-inactive-accounts --anonymize N [--email N] + + Options: + --anonymize Anonymize accounts inactive longer than this time (months) + --email Email accounts inactive longer than this time (months) + --dry-run Don't actually anonymize anything or send any emails + --verbose Output as to which users are being affected + --help This help message + +=cut diff --git a/bin/process-inactive-reports b/bin/process-inactive-reports new file mode 100755 index 000000000..d2c030c2c --- /dev/null +++ b/bin/process-inactive-reports @@ -0,0 +1,41 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../setenv.pl"; +} + +use Getopt::Long; +use FixMyStreet::Script::Inactive; +use Pod::Usage; + +my %h; +GetOptions(\%h, 'anonymize=i', 'close=i', 'verbose|v', 'help|h', 'dry-run|n'); +pod2usage(0) if $h{help}; +pod2usage(1) unless $h{anonymize} || $h{close}; + +FixMyStreet::Script::Inactive->new(%h)->reports; + +__END__ + +=head1 NAME + +process-inactive-reports - deal with anonymizing inactive non-open reports + +=head1 SYNOPSIS + +process-inactive-reports [--anonymize N] [--close N] + + Options: + --anonymize Anonymize non-open reports (and related) inactive longer than this time (months) + --close Close comments on non-open reports inactive longer than this time (months) + --dry-run Don't actually anonymize anything or send any emails + --verbose Output as to which reports are being affected + --help This help message + +=cut diff --git a/bin/update-schema b/bin/update-schema index fb88c84a1..2ae374e61 100755 --- a/bin/update-schema +++ b/bin/update-schema @@ -212,6 +212,7 @@ else { # (assuming schema change files are never half-applied, which should be the case) sub get_db_version { return 'EMPTY' if ! table_exists('problem'); + return '0062' if column_exists('users', 'created'); return '0061' if column_exists('body', 'extra'); return '0060' if column_exists('body', 'convert_latlong'); return '0059' if column_exists('response_templates', 'external_status_code'); diff --git a/db/downgrade_0062---0061.sql b/db/downgrade_0062---0061.sql new file mode 100644 index 000000000..fa958169a --- /dev/null +++ b/db/downgrade_0062---0061.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE users DROP created; +ALTER TABLE users DROP last_active; + +COMMIT; diff --git a/db/schema.sql b/db/schema.sql index fa0bd07bd..30f5d3a30 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -30,6 +30,8 @@ create table users ( from_body integer, flagged boolean not null default 'f', is_superuser boolean not null default 'f', + created timestamp not null default current_timestamp, + last_active timestamp not null default current_timestamp, title text, twitter_id bigint unique, facebook_id bigint unique, diff --git a/db/schema_0062-add-user-created-last-active.sql b/db/schema_0062-add-user-created-last-active.sql new file mode 100644 index 000000000..0e8875870 --- /dev/null +++ b/db/schema_0062-add-user-created-last-active.sql @@ -0,0 +1,7 @@ +BEGIN; + +ALTER TABLE users ADD created timestamp default current_timestamp not null; +ALTER TABLE users ADD last_active timestamp default current_timestamp not null; + +COMMIT; + diff --git a/perllib/Catalyst/Plugin/FixMyStreet/Session/StoreSessions.pm b/perllib/Catalyst/Plugin/FixMyStreet/Session/StoreSessions.pm index 5e7a3cede..30149fc2b 100644 --- a/perllib/Catalyst/Plugin/FixMyStreet/Session/StoreSessions.pm +++ b/perllib/Catalyst/Plugin/FixMyStreet/Session/StoreSessions.pm @@ -7,6 +7,7 @@ after set_authenticated => sub { my $sessions = $c->user->get_extra_metadata('sessions'); push @$sessions, $c->sessionid; $c->user->set_extra_metadata('sessions', $sessions); + $c->user->set_last_active; $c->user->update; }; @@ -16,6 +17,7 @@ before logout => sub { my $sessions = $user->get_extra_metadata('sessions'); $sessions = [ grep { $_ ne $c->sessionid } @$sessions ]; @$sessions ? $user->set_extra_metadata('sessions', $sessions) : $user->unset_extra_metadata('sessions'); + $user->set_last_active; $user->update; } }; diff --git a/perllib/CronFns.pm b/perllib/CronFns.pm index eb54d7627..76295d2c8 100755 --- a/perllib/CronFns.pm +++ b/perllib/CronFns.pm @@ -26,6 +26,7 @@ sub options { sub site { my $base_url = shift; my $site = 'fixmystreet'; + $site = 'fixamingata' if $base_url =~ /fixamingata/; $site = 'zurich' if $base_url =~ /zurich|zueri/; $site = 'smidsy' if $base_url =~ /smidsy|collideosco/; $site = 'kiitc' if $base_url =~ /kiitc|acv|keepitinthecommunity/; diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index 8f8d7cc47..7d04f5ff9 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -928,6 +928,12 @@ sub report_edit : Path('report_edit') : Args(1) { } $problem->set_inflated_columns(\%columns); + if ($c->get_param('closed_updates')) { + $problem->set_extra_metadata(closed_updates => 1); + } else { + $problem->unset_extra_metadata('closed_updates'); + } + $c->forward( '/admin/report_edit_category', [ $problem, $problem->state ne $old_state ] ); $c->forward('update_user', [ $problem ]); @@ -1911,20 +1917,7 @@ sub user_hide_everywhere : Private { sub user_remove_account : Private { my ( $self, $c, $user ) = @_; $c->forward('user_logout_everywhere', [ $user ]); - $user->problems->update({ anonymous => 1, name => '', send_questionnaire => 0 }); - $user->comments->update({ anonymous => 1, name => '' }); - $user->alerts->update({ whendisabled => \'current_timestamp' }); - $user->password('', 1); - $user->update({ - email => 'removed-' . $user->id . '@' . FixMyStreet->config('EMAIL_DOMAIN'), - email_verified => 0, - name => '', - phone => '', - phone_verified => 0, - title => undef, - twitter_id => undef, - facebook_id => undef, - }); + $user->anonymize_account; $c->stash->{status_message} = _('That user’s personal details have been removed.'); } diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm index 13eceadb0..4a5b8db5d 100644 --- a/perllib/FixMyStreet/App/Controller/Report/Update.pm +++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm @@ -240,6 +240,7 @@ This makes sure we only proceed to processing if we've had the form submitted sub check_form_submitted : Private { my ( $self, $c ) = @_; + return if $c->stash->{problem}->get_extra_metadata('closed_updates'); return $c->get_param('submit_update') || ''; } diff --git a/perllib/FixMyStreet/DB/Result/Session.pm b/perllib/FixMyStreet/DB/Result/Session.pm index 4713c99eb..a478c5444 100644 --- a/perllib/FixMyStreet/DB/Result/Session.pm +++ b/perllib/FixMyStreet/DB/Result/Session.pm @@ -24,5 +24,24 @@ __PACKAGE__->set_primary_key("id"); # Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:MVmCn4gLQWXTDIIaDHiVmA -# You can replace this text with custom code or comments, and it will be preserved on regeneration +use Storable; +use MIME::Base64; + +sub id_code { + my $self = shift; + my $id = $self->id; + $id =~ s/^session://; + $id =~ s/\s+$//; + return $id; +} + +sub user { + my $self = shift; + return unless $self->session_data; + my $data = Storable::thaw(MIME::Base64::decode($self->session_data)); + return unless $data->{__user}; + my $user = $self->result_source->schema->resultset("User")->find($data->{__user}{id}); + return $user; +} + 1; diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm index db68236bf..8b539f85d 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -20,10 +20,14 @@ __PACKAGE__->add_columns( }, "email", { data_type => "text", is_nullable => 1 }, + "email_verified", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "name", { data_type => "text", is_nullable => 1 }, "phone", { data_type => "text", is_nullable => 1 }, + "phone_verified", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "password", { data_type => "text", default_value => "", is_nullable => 0 }, "from_body", @@ -42,10 +46,20 @@ __PACKAGE__->add_columns( { data_type => "integer", is_nullable => 1 }, "extra", { data_type => "text", is_nullable => 1 }, - "email_verified", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, - "phone_verified", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "created", + { + data_type => "timestamp", + default_value => \"current_timestamp", + is_nullable => 0, + original => { default_value => \"now()" }, + }, + "last_active", + { + data_type => "timestamp", + default_value => \"current_timestamp", + is_nullable => 0, + original => { default_value => \"now()" }, + }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->add_unique_constraint("users_facebook_id_key", ["facebook_id"]); @@ -105,8 +119,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-09-19 18:02:17 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OKHKCSahWD3Ov6ulj+2f/w +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-05-23 18:54:36 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/V7+Ygv/t6VX8dDhNGN16w # These are not fully unique constraints (they only are when the *_verified # is true), but this is managed in ResultSet::User's find() wrapper. @@ -442,6 +456,25 @@ sub adopt { $other->delete; } +sub anonymize_account { + my $self = shift; + + $self->problems->update({ anonymous => 1, name => '', send_questionnaire => 0 }); + $self->comments->update({ anonymous => 1, name => '' }); + $self->alerts->update({ whendisabled => \'current_timestamp' }); + $self->password('', 1); + $self->update({ + email => 'removed-' . $self->id . '@' . FixMyStreet->config('EMAIL_DOMAIN'), + email_verified => 0, + name => '', + phone => '', + phone_verified => 0, + title => undef, + twitter_id => undef, + facebook_id => undef, + }); +} + # Planned reports / shortlist # Override the default auto-created function as we only want one live entry so @@ -511,4 +544,11 @@ has categories => ( }, ); +sub set_last_active { + my $self = shift; + my $time = shift; + $self->unset_extra_metadata('inactive_email_sent'); + $self->last_active($time or \'current_timestamp'); +} + 1; diff --git a/perllib/FixMyStreet/Script/Inactive.pm b/perllib/FixMyStreet/Script/Inactive.pm new file mode 100644 index 000000000..0468d2a52 --- /dev/null +++ b/perllib/FixMyStreet/Script/Inactive.pm @@ -0,0 +1,174 @@ +package FixMyStreet::Script::Inactive; + +use v5.14; +use warnings; + +use Moo; +use CronFns; +use FixMyStreet; +use FixMyStreet::Cobrand; +use FixMyStreet::DB; +use FixMyStreet::Email; + +has anonymize => ( is => 'ro' ); +has close => ( is => 'ro' ); +has email => ( is => 'ro' ); +has verbose => ( is => 'ro' ); +has dry_run => ( is => 'ro' ); + +sub BUILDARGS { + my ($cls, %args) = @_; + $args{dry_run} = delete $args{'dry-run'}; + return \%args; +} + +has cobrand => ( + is => 'lazy', + default => sub { + my $base_url = FixMyStreet->config('BASE_URL'); + my $site = CronFns::site($base_url); + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($site)->new; + $cobrand->set_lang_and_domain(undef, 1); + $cobrand; + }, +); + +has anonymous_user => ( + is => 'lazy', + default => sub { + FixMyStreet::DB->resultset("User")->find_or_create({ + email => 'removed-automatically@' . FixMyStreet->config('EMAIL_DOMAIN'), + }); + } +); + +sub users { + my $self = shift; + + say "DRY RUN" if $self->dry_run; + $self->anonymize_users; + $self->email_inactive_users if $self->email; +} + +sub reports { + my $self = shift; + + say "DRY RUN" if $self->dry_run; + $self->anonymize_reports if $self->anonymize; + $self->close_updates if $self->close; +} + +sub close_updates { + my $self = shift; + + my $problems = FixMyStreet::DB->resultset("Problem")->search({ + lastupdate => { '<', interval($self->close) }, + state => [ FixMyStreet::DB::Result::Problem->closed_states(), FixMyStreet::DB::Result::Problem->fixed_states() ], + extra => [ undef, { -not_like => '%closed_updates%' } ], + }); + + while (my $problem = $problems->next) { + say "Closing updates on problem #" . $problem->id if $self->verbose; + next if $self->dry_run; + $problem->set_extra_metadata( closed_updates => 1 ); + $problem->update; + } +} + +sub anonymize_reports { + my $self = shift; + + # Need to look though them all each time, in case any new updates/alerts + my $problems = FixMyStreet::DB->resultset("Problem")->search({ + lastupdate => { '<', interval($self->anonymize) }, + state => [ + FixMyStreet::DB::Result::Problem->closed_states(), + FixMyStreet::DB::Result::Problem->fixed_states(), + FixMyStreet::DB::Result::Problem->hidden_states(), + ], + }); + + while (my $problem = $problems->next) { + say "Anonymizing problem #" . $problem->id if $self->verbose; + next if $self->dry_run; + + # Remove personal data from the report + $problem->update({ + user => $self->anonymous_user, + name => '', + anonymous => 1, + send_questionnaire => 0, + }) if $problem->user != $self->anonymous_user; + + # Remove personal data from the report's updates + $problem->comments->search({ + user_id => { '!=' => $self->anonymous_user->id }, + })->update({ + user_id => $self->anonymous_user->id, + name => '', + anonymous => 1, + }); + + # Remove alerts - could just delete, but of interest how many there were, perhaps? + FixMyStreet::DB->resultset('Alert')->search({ + user_id => { '!=' => $self->anonymous_user->id }, + alert_type => 'new_updates', + parameter => $problem->id, + })->update({ + user_id => $self->anonymous_user->id, + whendisabled => \'current_timestamp', + }); + } +} + +sub anonymize_users { + my $self = shift; + + my $users = FixMyStreet::DB->resultset("User")->search({ + last_active => { '<', interval($self->anonymize) }, + }); + + while (my $user = $users->next) { + say "Anonymizing user #" . $user->id if $self->verbose; + next if $self->dry_run; + $user->anonymize_account; + } +} + +sub email_inactive_users { + my $self = shift; + + my $users = FixMyStreet::DB->resultset("User")->search({ + last_active => [ -and => { '<', interval($self->email) }, + { '>=', interval($self->anonymize) } ], + }); + while (my $user = $users->next) { + next if $user->get_extra_metadata('inactive_email_sent'); + + say "Emailing user #" . $user->id if $self->verbose; + next if $self->dry_run; + FixMyStreet::Email::send_cron( + $user->result_source->schema, + 'inactive-account.txt', + { + email_from => $self->email, + anonymize_from => $self->anonymize, + user => $user, + url => $self->cobrand->base_url_with_lang . '/my', + }, + { To => [ $user->email, $user->name ] }, + undef, 0, $self->cobrand, + ); + + $user->set_extra_metadata('inactive_email_sent', 1); + $user->update; + } +} + +sub interval { + my $interval = shift; + my $s = "current_timestamp - '$interval months'::interval"; + return \$s; +} + +1; diff --git a/perllib/Open311.pm b/perllib/Open311.pm index dc40076db..a91de0a7c 100644 --- a/perllib/Open311.pm +++ b/perllib/Open311.pm @@ -132,7 +132,7 @@ sub _populate_service_request_params { description => $description, service_code => $service_code, first_name => $firstname, - last_name => $lastname || '', + last_name => $lastname, }; $params->{phone} = $problem->user->phone if $problem->user->phone; @@ -413,9 +413,11 @@ sub _populate_service_request_update_params { sub split_name { my ( $self, $name ) = @_; + return ('', '') unless $name; + my ( $first, $last ) = ( $name =~ /(\w+)(?:\.?\s+(.+))?/ ); - return ( $first, $last ); + return ( $first || '', $last || ''); } sub _params_to_string { diff --git a/t/app/controller/admin/report_edit.t b/t/app/controller/admin/report_edit.t index 0a4e702bb..5e3e6c315 100644 --- a/t/app/controller/admin/report_edit.t +++ b/t/app/controller/admin/report_edit.t @@ -79,6 +79,7 @@ foreach my $test ( anonymous => 0, flagged => undef, non_public => undef, + closed_updates => undef, }, changes => { title => 'Edited Report', }, log_entries => [qw/edit/], @@ -95,6 +96,7 @@ foreach my $test ( anonymous => 0, flagged => undef, non_public => undef, + closed_updates => undef, }, changes => { detail => 'Edited Detail', }, log_entries => [qw/edit edit/], @@ -111,6 +113,7 @@ foreach my $test ( anonymous => 0, flagged => undef, non_public => undef, + closed_updates => undef, }, changes => { name => 'Edited User', }, log_entries => [qw/edit edit edit/], @@ -128,6 +131,7 @@ foreach my $test ( anonymous => 0, flagged => undef, non_public => undef, + closed_updates => undef, }, changes => { flagged => 'on', @@ -147,6 +151,7 @@ foreach my $test ( anonymous => 0, flagged => 'on', non_public => undef, + closed_updates => undef, }, changes => { username => $user2->email, }, log_entries => [qw/edit edit edit edit edit/], @@ -164,6 +169,7 @@ foreach my $test ( anonymous => 0, flagged => 'on', non_public => undef, + closed_updates => undef, }, expect_comment => 1, changes => { state => 'unconfirmed' }, @@ -181,6 +187,7 @@ foreach my $test ( anonymous => 0, flagged => 'on', non_public => undef, + closed_updates => undef, }, expect_comment => 1, changes => { state => 'confirmed' }, @@ -198,6 +205,7 @@ foreach my $test ( anonymous => 0, flagged => 'on', non_public => undef, + closed_updates => undef, }, expect_comment => 1, changes => { state => 'fixed' }, @@ -216,6 +224,7 @@ foreach my $test ( anonymous => 0, flagged => 'on', non_public => undef, + closed_updates => undef, }, expect_comment => 1, changes => { state => 'hidden' }, @@ -235,6 +244,7 @@ foreach my $test ( anonymous => 0, flagged => 'on', non_public => undef, + closed_updates => undef, }, expect_comment => 1, changes => { @@ -257,6 +267,7 @@ foreach my $test ( anonymous => 1, flagged => 'on', non_public => undef, + closed_updates => undef, }, changes => {}, log_entries => [ @@ -275,6 +286,7 @@ foreach my $test ( anonymous => 1, flagged => 'on', non_public => undef, + closed_updates => undef, }, changes => { non_public => 'on', @@ -285,6 +297,24 @@ foreach my $test ( resend => 0, }, { + description => 'close to updates', + fields => { + title => 'Edited Report', + detail => 'Edited Detail', + state => 'confirmed', + name => 'Edited User', + username => $user2->email, + anonymous => 1, + flagged => 'on', + non_public => 'on', + closed_updates => undef, + }, + changes => { closed_updates => 'on' }, + log_entries => [ + qw/edit edit resend edit state_change edit state_change edit state_change edit state_change edit state_change edit edit edit edit edit/ + ], + }, + { description => 'change state to investigating as body superuser', fields => { title => 'Edited Report', @@ -295,12 +325,13 @@ foreach my $test ( anonymous => 1, flagged => 'on', non_public => 'on', + closed_updates => undef, }, expect_comment => 1, user_body => $oxfordshire, changes => { state => 'investigating' }, log_entries => [ - qw/edit state_change edit resend edit state_change edit state_change edit state_change edit state_change edit state_change edit edit edit edit edit/ + qw/edit state_change edit edit resend edit state_change edit state_change edit state_change edit state_change edit state_change edit edit edit edit edit/ ], resend => 0, }, @@ -315,13 +346,14 @@ foreach my $test ( anonymous => 1, flagged => 'on', non_public => 'on', + closed_updates => undef, }, expect_comment => 1, expected_text => '*Category changed from ‘Other’ to ‘Potholes’*', user_body => $oxfordshire, changes => { state => 'in progress', category => 'Potholes' }, log_entries => [ - qw/edit state_change edit state_change edit resend edit state_change edit state_change edit state_change edit state_change edit state_change edit edit edit edit edit/ + qw/edit state_change edit state_change edit edit resend edit state_change edit state_change edit state_change edit state_change edit state_change edit edit edit edit edit/ ], resend => 0, }, @@ -371,6 +403,15 @@ foreach my $test ( $test->{changes}->{flagged} = 1 if $test->{changes}->{flagged}; $test->{changes}->{non_public} = 1 if $test->{changes}->{non_public}; + if ($test->{changes}->{closed_updates}) { + is $report->get_extra_metadata('closed_updates'), 1, "closed_updates updated"; + $mech->get_ok("/report/$report_id"); + $mech->content_lacks('Provide an update'); + $report->unset_extra_metadata('closed_updates'); + $report->update; + delete $test->{changes}->{closed_updates}; + } + is $report->$_, $test->{changes}->{$_}, "$_ updated" for grep { $_ ne 'username' } keys %{ $test->{changes} }; if ( $test->{user} ) { @@ -451,6 +492,7 @@ subtest 'change email to new user' => sub { anonymous => 1, flagged => 'on', non_public => 'on', + closed_updates => undef, external_id => '13', external_body => '', external_team => '', diff --git a/t/app/model/session.t b/t/app/model/session.t new file mode 100644 index 000000000..f76533727 --- /dev/null +++ b/t/app/model/session.t @@ -0,0 +1,14 @@ +use FixMyStreet::TestMech; + +my $mech = FixMyStreet::TestMech->new; + +$mech->log_in_ok('test@example.com'); + +my $session = FixMyStreet::DB->resultset("Session")->first; + +my $id = $session->id; +$id =~ s/\s+$//; +is $id, "session:" . $session->id_code; +is $session->user->email, 'test@example.com'; + +done_testing; diff --git a/t/script/inactive.t b/t/script/inactive.t new file mode 100644 index 000000000..4d78b385f --- /dev/null +++ b/t/script/inactive.t @@ -0,0 +1,71 @@ +use FixMyStreet::TestMech; + +use_ok 'FixMyStreet::Script::Inactive'; + +my $in = FixMyStreet::Script::Inactive->new( anonymize => 6, email => 3 ); +my $mech = FixMyStreet::TestMech->new; + +my $user = FixMyStreet::DB->resultset("User")->find_or_create({ email => 'test@example.com' }); +my $t = DateTime->new(year => 2016, month => 1, day => 1, hour => 12); +$user->last_active($t); +$user->update; + +my $user_inactive = FixMyStreet::DB->resultset("User")->find_or_create({ email => 'inactive@example.com' }); +$t = DateTime->now->subtract(months => 4); +$user_inactive->last_active($t); +$user_inactive->update; + +my @problems; +for (my $m = 1; $m <= 12; $m++) { + my $t = DateTime->new(year => 2017, month => $m, day => 1, hour => 12); + push @problems, $mech->create_problems_for_body(1, 2237, 'Title', { + dt => $t, + lastupdate => "$t", + state => $m % 2 ? 'fixed - user' : 'confirmed', + }); +} + +$mech->create_comment_for_problem($problems[0], $user, 'Name', 'Update', 0, 'confirmed', $problems[0]->state); +FixMyStreet::DB->resultset("Alert")->create({ alert_type => 'new_updates', parameter => $problems[2]->id, user => $user }); + +subtest 'Anonymization of inactive fixed/closed reports' => sub { + $in->reports; + + my $count = FixMyStreet::DB->resultset("Problem")->search({ user_id => $user->id })->count; + is $count, 6, 'Six non-anonymised'; + + my $comment = FixMyStreet::DB->resultset("Comment")->first; + my $alert = FixMyStreet::DB->resultset("Alert")->first; + is $comment->anonymous, 1, 'Comment anonymized'; + is $comment->user->email, 'removed-automatically@example.org', 'Comment user anonymized'; + is $alert->user->email, 'removed-automatically@example.org', 'Alert anonymized'; + isnt $alert->whendisabled, undef, 'Alert disabled'; + + $mech->create_comment_for_problem($problems[0], $user, 'Name 2', 'Update', 0, 'confirmed', $problems[0]->state); + $comment = FixMyStreet::DB->resultset("Comment")->search({ name => 'Name 2' })->first; + + $in->reports; + $comment->discard_changes; + is $comment->anonymous, 1, 'Comment anonymized'; + is $comment->user->email, 'removed-automatically@example.org', 'Comment user anonymized'; +}; + +subtest 'Closing updates on inactive fixed/closed reports' => sub { + my $in = FixMyStreet::Script::Inactive->new( close => 1 ); + $in->reports; + $problems[2]->discard_changes; + is $problems[2]->get_extra_metadata('closed_updates'), 1, 'Closed to updates'; + # TODO Visit page, check closed for updates +}; + +subtest 'Anonymization of inactive users' => sub { + $in->users; + + my $email = $mech->get_email; + like $email->as_string, qr/inactive\@example.com/, 'Inactive email sent'; + + $user->discard_changes; + is $user->email, 'removed-' . $user->id . '@example.org', 'User has been anonymized'; +}; + +done_testing; diff --git a/templates/email/default/inactive-account.html b/templates/email/default/inactive-account.html new file mode 100644 index 000000000..78b277877 --- /dev/null +++ b/templates/email/default/inactive-account.html @@ -0,0 +1,26 @@ +[% + +email_summary = "Your inactive account on " _ site_name; +email_columns = 1; + +PROCESS '_email_settings.html'; + +INCLUDE '_email_top.html'; + +%] + +<th style="[% td_style %][% only_column_style %]"> + <h1 style="[% h1_style %]">Your inactive account</h1> + <p style="[% p_style %]"> +Your account on [% site_name %] has been inactive for [% email_from %] +[% nget('month', 'months', email_from) %], and we automatically remove +accounts that have been inactive after [% anonymize_from %] +[% nget('month', 'months', anonymize_from) %]. If you wish to keep your +account, please log in to the site and that will keep it active: +</p> + <p style="margin: 20px auto; text-align: center"> + <a style="[% button_style %]" href="[% url %]">Visit [% site_name %]</a> + </p> + <p style="[% p_style %]">Thanks for using the site.</p> + +[% INCLUDE '_email_bottom.html' %] diff --git a/templates/email/default/inactive-account.txt b/templates/email/default/inactive-account.txt new file mode 100644 index 000000000..1b6b8b5bd --- /dev/null +++ b/templates/email/default/inactive-account.txt @@ -0,0 +1,18 @@ +Subject: Your inactive account on [% site_name %] + +Hello [% user.name %], + +Your account on [% site_name %] has been inactive for [% email_from %] +[% nget('month', 'months', email_from) %], and we automatically remove +accounts that have been inactive after [% anonymize_from %] +[% nget('month', 'months', anonymize_from) %]. If you wish to keep your +account, please log in to the site and that will keep it active: + +[% url %] + +Thanks for using the site. + +[% INCLUDE 'signature.txt' %] + +This email was sent automatically, from an unmonitored email account - so +please do not reply to it. diff --git a/templates/web/base/admin/report_edit.html b/templates/web/base/admin/report_edit.html index 138ba8c05..10eb0eea1 100644 --- a/templates/web/base/admin/report_edit.html +++ b/templates/web/base/admin/report_edit.html @@ -172,6 +172,9 @@ class="admin-offsite-link">[% problem.latitude %], [% problem.longitude %]</a> </li> [% END %] +<li><label class="inline-text" for="closed_updates">[% loc('Closed to updates') %]:</label> + <input type="checkbox" id="closed_updates" name="closed_updates"[% ' checked' IF problem.extra.closed_updates %]></li> + </ul> <input type="submit" class="btn" name="Submit changes" value="[% loc('Submit changes') %]"> </form> diff --git a/templates/web/base/report/display.html b/templates/web/base/report/display.html index f9c42b35d..8c461dced 100644 --- a/templates/web/base/report/display.html +++ b/templates/web/base/report/display.html @@ -53,7 +53,7 @@ [% TRY %][% INCLUDE 'report/sharing.html' %][% CATCH file %][% END %] [% INCLUDE 'report/updates.html' %] -[% IF two_column_sidebar %] +[% IF two_column_sidebar AND NOT problem.extra.closed_updates %] <button class="btn btn--provide-update js-provide-update hidden-nojs">[% loc('Provide an update') %]</button> <div class="hidden-js"> [% END %] @@ -62,7 +62,7 @@ [% ELSIF NOT shown_form %] [% INCLUDE 'report/update-form.html' %] [% END %] -[% IF two_column_sidebar %] +[% IF two_column_sidebar AND NOT problem.extra.closed_updates %] </div> [% END %] diff --git a/templates/web/base/report/display_tools.html b/templates/web/base/report/display_tools.html index 8ed86c228..e0d8e3f99 100644 --- a/templates/web/base/report/display_tools.html +++ b/templates/web/base/report/display_tools.html @@ -10,7 +10,7 @@ c.cobrand.moniker == 'fixmystreet' ? 'Unsuitable?' : loc('Report abuse') %]</a></li> [% END %] - [% IF c.cobrand.moniker != 'zurich' %] + [% IF NOT problem.extra.closed_updates AND c.cobrand.moniker != 'zurich' %] <li><a rel="nofollow" id="key-tool-report-updates" class="feed js-feed" href="[% c.uri_for( '/alert/subscribe', { id => problem.id } ) %]">[% loc('Get updates' ) %]</a></li> [% END %] [% IF c.cobrand.moniker == 'fixmystreet' %] diff --git a/templates/web/base/report/new/form_user_loggedin.html b/templates/web/base/report/new/form_user_loggedin.html index b82be3202..7f81764be 100644 --- a/templates/web/base/report/new/form_user_loggedin.html +++ b/templates/web/base/report/new/form_user_loggedin.html @@ -45,18 +45,7 @@ [% INCLUDE 'report/new/extra_name.html' %] [% PROCESS 'user/_anonymity.html' anonymous = report.anonymous %] - - <label for="form_name">[% loc('Name') %] - [% TRY %] - [% INCLUDE 'report/new/after_name.html' %] - [% CATCH file %] - [% END %] - </label> - [% IF field_errors.name %] - <p class='form-error'>[% field_errors.name %]</p> - [% END %] - <input type="text" class="form-control validName js-form-name" value="[% report.name | html %]" name="name" id="form_name"> - + [% INCLUDE 'report/new/form_user_name.html' %] [% INCLUDE 'report/_show_name_label.html' %] [% IF NOT c.user.phone_verified AND NOT c.cobrand.call_hook('disable_phone_number_entry') %] diff --git a/templates/web/base/report/new/form_user_loggedout_by_email.html b/templates/web/base/report/new/form_user_loggedout_by_email.html index 6ec0278ce..b633c4843 100644 --- a/templates/web/base/report/new/form_user_loggedout_by_email.html +++ b/templates/web/base/report/new/form_user_loggedout_by_email.html @@ -7,19 +7,7 @@ [% INCLUDE 'report/new/extra_name.html' %] [% PROCESS 'user/_anonymity.html' anonymous = report.anonymous %] - - <label for="form_name">[% loc('Name') %] - [% TRY %] - [% INCLUDE 'report/new/after_name.html' %] - [% CATCH file %] - [% END %] - </label> - [% IF field_errors.name %] - <p class='form-error'>[% field_errors.name %]</p> - [% END %] - - <input type="text" class="form-control form-focus-trigger validName js-form-name" value="[% report.name | html %]" name="name" id="form_name" placeholder="[% loc('Your name') %]"> - + [% INCLUDE 'report/new/form_user_name.html' extra_class='form-focus-trigger' %] [% INCLUDE 'report/_show_name_label.html' %] [% UNLESS c.cobrand.call_hook('disable_phone_number_entry') %] diff --git a/templates/web/base/report/new/form_user_name.html b/templates/web/base/report/new/form_user_name.html new file mode 100644 index 000000000..1f745eadb --- /dev/null +++ b/templates/web/base/report/new/form_user_name.html @@ -0,0 +1,10 @@ +<label for="form_name">[% loc('Name') %] +[% TRY %] + [% INCLUDE 'report/new/after_name.html' %] + [% CATCH file %] +[% END %] +</label> +[% IF field_errors.name %] + <p class='form-error'>[% field_errors.name %]</p> +[% END %] +<input type="text" class="form-control validName js-form-name [% extra_class %]" value="[% report.name | html %]" name="name" id="form_name"> diff --git a/templates/web/base/report/update-form.html b/templates/web/base/report/update-form.html index 1e67c2072..9276acde9 100644 --- a/templates/web/base/report/update-form.html +++ b/templates/web/base/report/update-form.html @@ -1,5 +1,5 @@ [% allow_creation = !c.cobrand.only_authed_can_create || (c.user && c.user.from_body) %] -[% RETURN IF NOT allow_creation %] +[% RETURN IF NOT allow_creation OR problem.extra.closed_updates %] <div id="update_form"> [% IF NOT login_success AND NOT oauth_need_email %] diff --git a/templates/web/bromley/report/new/form_user.html b/templates/web/bromley/report/new/form_user.html index cce985c95..20f522dcd 100644 --- a/templates/web/bromley/report/new/form_user.html +++ b/templates/web/bromley/report/new/form_user.html @@ -5,130 +5,18 @@ title, first name, and last name separately. %] -<h2 class="form-section-heading form-section-heading--private">Private details</h2> -<p class="form-section-description"> - [% tprintf( - loc('These will be sent to the council, but will never be shown online. <a href="%s">(See our privacy policy.)</a>') - '/faq#privacy' - ); %] +<h2 class="form-section-heading form-section-heading--private">[% loc('Private details') %]</h2> +<p class="form-section-description" id="js-councils_text_private"> + [% IF js %] + [% loc('These will be sent to the council, but will never be shown online.') %] + (<a href="/faq#privacy">[% loc('See our privacy policy') %]</a>.) + [% ELSE %] + [% PROCESS 'report/new/councils_text_private.html' %] + [% END %] </p> [% IF c.user_exists %] - <div class="form-box"> - [% INCLUDE 'report/new/extra_name.html' %] - - [% names = c.user.split_name %] - <label for="form_first_name">[% loc('First Name') %]</label> - [% IF field_errors.first_name %] - <p class='form-error'>[% field_errors.first_name %]</p> - [% END %] - <input class="form-control js-form-name" type="text" value="[% ( first_name || names.first ) | html %]" name="first_name" id="form_first_name" placeholder="[% loc('Your first name') %]"> - - <label for="form_last_name">[% loc('Last Name') %]</label> - [% IF field_errors.last_name %] - <p class='form-error'>[% field_errors.last_name %]</p> - [% END %] - <input class="form-control js-form-name" type="text" value="[% ( last_name || names.last ) | html %]" name="last_name" id="form_last_name" placeholder="[% loc('Your last name') %]"> - - - [%# if there is nothing in the name field then set check box as default on form %] - <div class="checkbox-group"> - <input type="checkbox" name="may_show_name" id="form_may_show_name" value="1"[% ' checked' IF !report.anonymous %]> - <label class="inline" for="form_may_show_name">[% loc('Show my name publicly') %]</label> - </div> - - <label for="form_phone">[% loc('Phone number (optional)') %]</label> - <input class="form-control" type="text" value="[% report.user.phone | html %]" name="phone" id="form_phone" placeholder="[% loc('Your phone number') %]"> - - <div class="general-notes"> - <p>[% loc('We never show your email address or phone number.') %]</p> - </div> - - <div class="form-txt-submit-box"> - <input class="green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Submit') %]"> - </div> - </div> + [% PROCESS "report/new/form_user_loggedin.html" %] [% ELSE %] - - <label for="form_username">[% loc('Your email') %]</label> - [% IF field_errors.username %] - <p class='form-error'>[% field_errors.username %]</p> - [% END %] - <input class="form-control" type="email" value="[% report.user.email | html %]" name="username" id="form_username" placeholder="[% loc('Please enter your email address') %]" required> - - <div id="form_sign_in"> - - <p>To submit your report you now need to confirm it either by email or by using a FixMyStreet password.</p> - - <div id="form_sign_in_no" class="form-box"> - <h5>Confirm my report by email</h5> - - [% INCLUDE 'report/new/extra_name.html' %] - - <label for="form_first_name">[% loc('First Name') %]</label> - [% IF field_errors.first_name %] - <p class='form-error'>[% field_errors.first_name %]</p> - [% END %] - <input type="text" class="form-control form-focus-trigger js-form-name" value="[% first_name | html %]" name="first_name" id="form_first_name" placeholder="[% loc('Your first name') %]"> - - <label for="form_last_name">[% loc('Last Name') %]</label> - [% IF field_errors.last_name %] - <p class='form-error'>[% field_errors.last_name %]</p> - [% END %] - <input type="text" class="form-control form-focus-trigger js-form-name" value="[% last_name | html %]" name="last_name" id="form_last_name" placeholder="[% loc('Your last name') %]"> - - [%# if there is nothing in the name field then set check box as default on form %] - <div class="checkbox-group"> - <input type="checkbox" name="may_show_name" id="form_may_show_name" value="1"[% ' checked' IF !report.anonymous %]> - <label class="inline" for="form_may_show_name">[% loc('Show my name publicly') %]</label> - </div> - - <label class="form-focus-hidden" for="form_phone">[% loc('Phone number (optional)') %]</label> - <input class="form-control form-focus-hidden" type="text" value="[% report.user.phone | html %]" name="phone" id="form_phone" placeholder="[% loc('Your phone number') %]"> - - <div class="general-notes form-focus-hidden"> - <p>[% loc('We never show your email address or phone number.') %]</p> - </div> - - <label class="form-focus-hidden" for="password_register">[% loc('Password (optional)') %]</label> - [% IF field_errors.password_register %] - <p class='form-error'>[% field_errors.password_register %]</p> - [% END %] - - <div class="general-notes form-focus-hidden"> - <p>[% loc('Providing a password is optional, but doing so will allow you to more easily report future problems, leave updates and manage your reports.') %]</p> - </div> - - <div class="form-txt-submit-box form-focus-hidden"> - <input class="form-control js-password-validate" type="password" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]"> - <input class="green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Submit') %]"> - </div> - - <div class="general-notes"> - <p>[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]</p> - </div> - - </div> - - <div id="form_sign_in_yes" class="form-box"> - - <h5>Confirm my report with my FixMyStreet password</h5> - - <label class="hidden-js n" for="password_sign_in">[% loc('Yes I have a password') %]</label> - <div class="form-txt-submit-box"> - [% IF field_errors.password %] - <p class='form-error'>[% field_errors.password %]</p> - [% END %] - <input class="form-control" type="password" name="password_sign_in" id="password_sign_in" placeholder="[% loc('Your password') %]" value=""> - <input class="green-btn js-submit_sign_in" type="submit" name="submit_sign_in" value="[% loc('Submit') %]"> - </div> - - <div class="checkbox-group"> - <input type="checkbox" id="remember_me" name="remember_me" value='1'[% ' checked' IF remember_me %]> - <label class="n inline" for="remember_me">[% loc('Keep me signed in on this computer') %]</label> - </div> - </div> - - </div> - + [% PROCESS "report/new/form_user_loggedout.html" %] [% END %] diff --git a/templates/web/bromley/report/new/form_user_loggedout.html b/templates/web/bromley/report/new/form_user_loggedout.html new file mode 100644 index 000000000..8d46e7b60 --- /dev/null +++ b/templates/web/bromley/report/new/form_user_loggedout.html @@ -0,0 +1,7 @@ +[% PROCESS 'report/new/form_user_loggedout_email.html' required = 1 %] + +<div id="form_sign_in"> + <p>To submit your report you now need to confirm it either by email or by using a FixMyStreet password.</p> + [% PROCESS 'report/new/form_user_loggedout_by_email.html' %] + [% PROCESS 'report/new/form_user_loggedout_password.html' %] +</div> diff --git a/templates/web/bromley/report/new/form_user_loggedout_by_email.html b/templates/web/bromley/report/new/form_user_loggedout_by_email.html new file mode 100644 index 000000000..aea672483 --- /dev/null +++ b/templates/web/bromley/report/new/form_user_loggedout_by_email.html @@ -0,0 +1,37 @@ +<div id="form_sign_in_no" class="form-box"> + <h5>Confirm my report by email</h5> + + [% INCLUDE 'report/new/extra_name.html' %] + [% PROCESS 'user/_anonymity.html' anonymous = report.anonymous %] + [% INCLUDE 'report/new/form_user_name.html' extra_class='form-focus-trigger' %] + [% INCLUDE 'report/_show_name_label.html' %] + + [% UNLESS c.cobrand.call_hook('disable_phone_number_entry') %] + <div id="js-hide-if-username-phone"> + <label class="form-focus-hidden" for="form_phone">[% loc('Phone number (optional)') %]</label> + <input class="form-control form-focus-hidden" type="text" value="[% report.user.phone_display | html %]" name="phone" id="form_phone"> + </div> + [% END %] + <div id="js-hide-if-username-email"> + <label class="form-focus-hidden" for="form_email">[% loc('Email address (optional)') %]</label> + <input class="form-control form-focus-hidden" type="text" value="[% report.user.email | html %]" name="email" id="form_email"> + </div> + + <label class="form-focus-hidden" for="password_register">[% loc('Password (optional)') %]</label> + [% IF field_errors.password_register %] + <p class='form-error'>[% field_errors.password_register %]</p> + [% END %] + <div class="general-notes form-focus-hidden"> + <p>[% loc('Providing a password is optional, but doing so will allow you to more easily report future problems, leave updates and manage your reports.') %]</p> + </div> + + <div class="form-txt-submit-box form-focus-hidden"> + <input class="form-control js-password-validate" type="password" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]"> + <input class="green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Submit') %]"> + </div> + + <div class="general-notes"> + <p>[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]</p> + </div> + +</div> diff --git a/templates/web/bromley/report/new/form_user_loggedout_password.html b/templates/web/bromley/report/new/form_user_loggedout_password.html new file mode 100644 index 000000000..a4255f647 --- /dev/null +++ b/templates/web/bromley/report/new/form_user_loggedout_password.html @@ -0,0 +1,18 @@ +<div id="form_sign_in_yes" class="form-box"> + + <h5>Confirm my report with my FixMyStreet password</h5> + + <label class="hidden-js n" for="password_sign_in">[% loc('Yes I have a password') %]</label> + [% IF field_errors.password %] + <p class='form-error'>[% field_errors.password %]</p> + [% END %] + <div class="form-txt-submit-box"> + <input class="form-control" type="password" name="password_sign_in" id="password_sign_in" placeholder="[% loc('Your password') %]" value=""> + <input class="green-btn js-submit_sign_in" type="submit" name="submit_sign_in" value="[% loc('Submit') %]"> + </div> + + <div class="checkbox-group"> + <input type="checkbox" id="remember_me" name="remember_me" value='1'[% ' checked' IF remember_me %]> + <label class="n inline" for="remember_me">[% loc('Keep me signed in on this computer') %]</label> + </div> +</div> diff --git a/templates/web/bromley/report/new/form_user_name.html b/templates/web/bromley/report/new/form_user_name.html new file mode 100644 index 000000000..8102c0ea7 --- /dev/null +++ b/templates/web/bromley/report/new/form_user_name.html @@ -0,0 +1,12 @@ +[% names = c.user.split_name %] +<label for="form_first_name">[% loc('First Name') %]</label> +[% IF field_errors.first_name %] + <p class='form-error'>[% field_errors.first_name %]</p> +[% END %] +<input class="form-control js-form-name [% extra_class %]" type="text" value="[% ( first_name || names.first ) | html %]" name="first_name" id="form_first_name" placeholder="[% loc('Your first name') %]"> + +<label for="form_last_name">[% loc('Last Name') %]</label> +[% IF field_errors.last_name %] + <p class='form-error'>[% field_errors.last_name %]</p> +[% END %] +<input class="form-control js-form-name [% extra_class %]" type="text" value="[% ( last_name || names.last ) | html %]" name="last_name" id="form_last_name" placeholder="[% loc('Your last name') %]"> diff --git a/templates/web/bromley/report/update-form.html b/templates/web/bromley/report/update-form.html index 9778a0db3..6c3ca9298 100644 --- a/templates/web/bromley/report/update-form.html +++ b/templates/web/bromley/report/update-form.html @@ -1,3 +1,6 @@ +[% allow_creation = !c.cobrand.only_authed_can_create || (c.user && c.user.from_body) %] +[% RETURN IF NOT allow_creation OR problem.extra.closed_updates %] + <div id="update_form"> [% UNLESS hide_header %] <h2[% IF two_column_sidebar %] class="hidden-js"[% END %]>[% loc('Provide an update') %]</h2> @@ -5,140 +8,33 @@ [% INCLUDE 'errors.html' %] - <form method="post" action="[% c.uri_for( '/report/update' ) %]" name="updateForm" class="validate"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]> + <form method="post" action="[% c.uri_for( '/report/update' ) %]" id="form_update_form" name="updateForm" class="validate"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]> <input type="hidden" name="token" value="[% csrf_token %]"> <fieldset> - <input type="hidden" name="submit_update" value="1"> - <input type="hidden" name="id" value="[% problem.id | html %]"> - - [% IF c.cobrand.allow_photo_upload %] - <input type="hidden" name="upload_fileid" value="[% upload_fileid %]"> - <label for="form_photo"> - <span data-singular="[% loc('Photo') %]" data-plural="[% loc('Photos') %]">[% loc('Photo') %]</span> - </label> - - [% IF field_errors.photo %] - <p class='form-error'>[% field_errors.photo %]</p> - [% END %] - - <div id="form_photos"> - [% IF upload_fileid %] - <p>[% loc('You have already attached photos to this update. Note that you can attach a maximum of 3 to this update (if you try to upload more, the oldest will be removed).') %]</p> - [% FOREACH id IN upload_fileid.split(',') %] - <img align="right" src="/photo/temp.[% id %]" alt=""> - [% END %] - [% END %] - <input type="file" name="photo1" id="form_photo"> - <label for="form_photo2">[% loc('Photo') %]</label> - <input type="file" name="photo2" id="form_photo2"> - <label for="form_photo3">[% loc('Photo') %]</label> - <input type="file" name="photo3" id="form_photo3"> - </div> - [% END %] - - <div class="general-notes"> - <p>Please note that new and separate occurrences of issues - should be logged as new reports, to avoid conflicting - information or updates and ensure the information is passed - promptly to our teams. Please log any new issue via - <a href="https://www.bromley.gov.uk/report">https://www.bromley.gov.uk/report</a> - using the map marker and description to detail where the issue - is located.</p> - </div> - - <label for="form_update">[% loc( 'Update' ) %]</label> - [% IF field_errors.update %] - <div class='form-error'>[% field_errors.update %]</div> - [% END %] - <textarea class="form-control" rows="7" cols="30" name="update" id="form_update" placeholder="[% loc('Please write your update here') %]" required>[% update.text | html %]</textarea> - - [% IF c.user && c.user.belongs_to_body( problem.bodies_str ) %] - <label for="state">[% loc( 'State' ) %]</label> - [% INCLUDE 'report/inspect/state_groups_select.html' %] - [% ELSE %] - [% IF problem.is_fixed AND c.user_exists AND c.user.id == problem.user_id %] - - <input type="checkbox" name="reopen" id="form_reopen" value="1"[% ' checked' IF update.mark_open %]> - <label class="inline" for="form_reopen">[% loc('This problem has not been fixed') %]</label> - - [% ELSIF !problem.is_fixed %] - - <div class="checkbox-group"> - <input type="checkbox" name="fixed" id="form_fixed" value="1"[% ' checked' IF update.mark_fixed %]> - <label class="inline" for="form_fixed">[% loc('This problem has been fixed') %]</label> - </div> - - [% END %] - [% END %] - - [% IF c.user_exists %] - + [% IF NOT login_success AND NOT oauth_need_email %] + [% INCLUDE 'report/update/form_update.html' %] + [% END %] + [% IF c.user_exists %] [% INCLUDE 'report/update/form_name.html' %] - - <input class="final-submit green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"> - - - [% ELSE %] - - <label for="form_username">[% loc('Email' ) %] - <span class="muted">([% loc('We never show your email') %])</span> - </label> - - [% IF field_errors.username %] - <p class='form-error'>[% field_errors.username %]</p> - [% END %] - <input class="form-control" type="email" name="username" id="form_username" value="[% update.user.email | html %]" placeholder="[% loc('Your email address' ) %]" required> - + <div class="clearfix"><input class="final-submit green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"></div> + [% ELSIF oauth_need_email %] + [% INCLUDE 'report/update/form_user_loggedout_email.html' required = 1 %] <div id="form_sign_in"> - <p>To submit your update you now need to confirm it either by email or by using a FixMyStreet password.</p> - - <div id="form_sign_in_no" class="form-box"> - <h5>Confirm my report by email</h5> - - [% INCLUDE 'report/update/form_name.html' %] - - <label for="password_register">[% loc('Password (optional)') %]</label> - [% IF field_errors.password_register %] - <p class='form-error'>[% field_errors.password_register %]</p> - [% END %] - - <div class="general-notes"> - <p>[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]</p> - </div> - - <div class="form-txt-submit-box"> - <input type="password" class="form-control js-password-validate" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]"> - <input class="green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"> - </div> - - <div class="general-notes"> - <p>[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]</p> - </div> - - </div> - <div id="form_sign_in_yes" class="form-box"> - <h5>Confirm my report with my FixMyStreet password</h5> - - <label class="hidden-js n" for="password_sign_in">[% loc('Yes I have a password') %]</label> - [% IF field_errors.password %] - <p class='form-error'>[% field_errors.password %]</p> - [% END %] - <div class="form-txt-submit-box"> - <input type="password" class="form-control" name="password_sign_in" id="password_sign_in" value="" placeholder="[% loc('Your password') %]"> - <input class="green-btn js-submit_sign_in" type="submit" name="submit_sign_in" value="[% loc('Post') %]"> - </div> - - <div class="checkbox-group"> - <input type="checkbox" id="remember_me" name="remember_me" value='1'[% ' checked' IF remember_me %]> - <label class="inline n" for="remember_me">[% loc('Keep me signed in on this computer') %]</label> - </div> - </div> + <h3>[% loc("Now to submit your update…") %]</h3> + <h2>[% tprintf(loc("Do you have a %s password?", "%s is the site name"), site_name) %]</h2> + [% INCLUDE 'report/update/form_user_loggedout_by_email.html' %] + [% INCLUDE 'report/update/form_user_loggedout_password.html' %] + <input type="hidden" name="oauth_need_email" value="1"> </div> + [% ELSE %] + [% INCLUDE 'report/update/form_user_loggedout.html' %] + [% END %] - [% END %] - - <p>Your information will only be used in accordance with our <a href="/faq#privacy">privacy policy</a>.</p> + <p>Your information will only be used in accordance with our <a href="/faq#privacy">privacy policy</a>.</p> + [% IF login_success OR oauth_need_email %] + [% INCLUDE 'report/update/form_update.html' %] + [% END %] </fieldset> </form> </div> diff --git a/templates/web/bromley/report/update/form_update.html b/templates/web/bromley/report/update/form_update.html new file mode 100644 index 000000000..06d7c455e --- /dev/null +++ b/templates/web/bromley/report/update/form_update.html @@ -0,0 +1,54 @@ +<input type="hidden" name="submit_update" value="1"> +<input type="hidden" name="id" value="[% problem.id | html %]"> + +[% IF c.cobrand.allow_photo_upload %] + <input type="hidden" name="upload_fileid" value="[% upload_fileid %]"> + <label for="form_photo"> + <span data-singular="[% loc('Photo') %]" data-plural="[% loc('Photos') %]">[% loc('Photo') %]</span> + </label> + + [% IF field_errors.photo %] + <p class='form-error'>[% field_errors.photo %]</p> + [% END %] + + <div id="form_photos"> + [% IF upload_fileid %] + <p>[% loc('You have already attached photos to this update. Note that you can attach a maximum of 3 to this update (if you try to upload more, the oldest will be removed).') %]</p> + [% FOREACH id IN upload_fileid.split(',') %] + <img align="right" src="/photo/temp.[% id %]" alt=""> + [% END %] + [% END %] + <input type="file" name="photo1" id="form_photo"> + <label for="form_photo2">[% loc('Photo') %]</label> + <input type="file" name="photo2" id="form_photo2"> + <label for="form_photo3">[% loc('Photo') %]</label> + <input type="file" name="photo3" id="form_photo3"> + </div> +[% END %] + +<div class="general-notes"> + <p>Please note that new and separate occurrences of issues + should be logged as new reports, to avoid conflicting + information or updates and ensure the information is passed + promptly to our teams. Please log any new issue via + <a href="https://www.bromley.gov.uk/report">https://www.bromley.gov.uk/report</a> + using the map marker and description to detail where the issue + is located.</p> +</div> + +<label for="form_update">[% loc( 'Update' ) %]</label> +[% IF c.user && c.user.belongs_to_body( problem.bodies_str ) %] + [% INCLUDE 'admin/response_templates_select.html' for='form_update' %] +[% END %] +[% IF field_errors.update %] + <div class='form-error'>[% field_errors.update %]</div> +[% END %] +<textarea rows="7" cols="30" name="update" class="form-control" id="form_update" placeholder="[% loc('Please write your update here') %]" required>[% update.text | html %]</textarea> + +[% IF c.user && c.user.belongs_to_body( problem.bodies_str ) %] + <label for="state">[% loc( 'State' ) %]</label> + [% INCLUDE 'report/inspect/state_groups_select.html' %] +[% ELSE %] + [% INCLUDE report/update/form_state_checkbox.html %] +[% END %] + diff --git a/templates/web/bromley/report/update/form_user_loggedout.html b/templates/web/bromley/report/update/form_user_loggedout.html new file mode 100644 index 000000000..f19238934 --- /dev/null +++ b/templates/web/bromley/report/update/form_user_loggedout.html @@ -0,0 +1,7 @@ +[% INCLUDE 'report/update/form_user_loggedout_email.html' required=1 %] + +<div id="form_sign_in"> + <p>To submit your update you now need to confirm it either by email or by using a FixMyStreet password.</p> + [% INCLUDE 'report/update/form_user_loggedout_by_email.html' %] + [% INCLUDE 'report/update/form_user_loggedout_password.html' %] +</div> diff --git a/templates/web/bromley/report/update/form_user_loggedout_by_email.html b/templates/web/bromley/report/update/form_user_loggedout_by_email.html new file mode 100644 index 000000000..393d8e4cc --- /dev/null +++ b/templates/web/bromley/report/update/form_user_loggedout_by_email.html @@ -0,0 +1,23 @@ +<div id="form_sign_in_no" class="form-box"> + <h5>Confirm my report by email</h5> + + [% INCLUDE 'report/update/form_name.html' %] + + <label for="password_register">[% loc('Password (optional)') %]</label> + [% IF field_errors.password_register %] + <p class='form-error'>[% field_errors.password_register %]</p> + [% END %] + <div class="general-notes"> + <p>[% loc('Providing a password is optional, but doing so will allow you to more easily report future problems, leave updates and manage your reports.') %]</p> + </div> + + <div class="form-txt-submit-box"> + <input class="form-control js-password-validate" type="password" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]"> + <input class="green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"> + </div> + + <div class="general-notes"> + <p>[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]</p> + </div> + +</div> diff --git a/templates/web/bromley/report/update/form_user_loggedout_email.html b/templates/web/bromley/report/update/form_user_loggedout_email.html new file mode 100644 index 000000000..228ca7509 --- /dev/null +++ b/templates/web/bromley/report/update/form_user_loggedout_email.html @@ -0,0 +1,8 @@ +<label for="form_username">[% loc('Email' ) %] + <span class="muted">([% loc('We never show your email') %])</span> +</label> + +[% IF field_errors.username %] + <p class='form-error'>[% field_errors.username %]</p> +[% END %] +<input class="form-control" type="email" name="username" id="form_username" value="[% update.user.email | html %]" placeholder="[% loc('Your email address' ) %]" required> diff --git a/templates/web/bromley/report/update/form_user_loggedout_password.html b/templates/web/bromley/report/update/form_user_loggedout_password.html new file mode 100644 index 000000000..3b7adb84e --- /dev/null +++ b/templates/web/bromley/report/update/form_user_loggedout_password.html @@ -0,0 +1,18 @@ +<div id="form_sign_in_yes" class="form-box"> + + <h5>Confirm my report with my FixMyStreet password</h5> + + <label class="hidden-js n" for="password_sign_in">[% loc('Yes I have a password') %]</label> + [% IF field_errors.password %] + <p class='form-error'>[% field_errors.password %]</p> + [% END %] + <div class="form-txt-submit-box"> + <input class="form-control" type="password" name="password_sign_in" id="password_sign_in" placeholder="[% loc('Your password') %]" value=""> + <input class="green-btn js-submit_sign_in" type="submit" name="submit_sign_in" value="[% loc('Post') %]"> + </div> + + <div class="checkbox-group"> + <input type="checkbox" id="remember_me" name="remember_me" value='1'[% ' checked' IF remember_me %]> + <label class="n inline" for="remember_me">[% loc('Keep me signed in on this computer') %]</label> + </div> +</div> diff --git a/templates/web/fixamingata/about/privacy.html b/templates/web/fixamingata/about/privacy.html index 805ca5f00..ced2c3b27 100755 --- a/templates/web/fixamingata/about/privacy.html +++ b/templates/web/fixamingata/about/privacy.html @@ -15,35 +15,48 @@ <h1>Offentlighet, kakor och tredjepartstjänster</h1> <p><strong>Vår användning av din information och vad du behöver veta.</strong></p> <p>Offentlighetsprincipen är en viktig del i det demokratiska samhället. Genom tryckfrihetsförordningen och offentlighetslagen har du rätt att ta del av allmänna offentliga handlingar hos statliga och kommunala myndigheter. Det kan ge dig insyn i hur förtroendevalda (politiker) och tjänstemän handlägger ärenden, vilka handlingar som kommit in till kommunens olika förvaltningar och vilka som skickats ut.</p> -<p>Lagen som reglerar hur personuppgifter samlas in, bearbetas och sprids heter Personuppgiftslagen (PuL). Lagen skyddar den enskildes integritet vid behandling av personuppgifter. Fr.o.m. 2018-05-25 införs ett nytt regelverk i EU för behandling av personuppgifter och ska börja tillämpas av medlemsstaterna i maj 2018.</p> -<p><a href="http://www.datainspektionen.se/dataskyddsreformen/dataskyddsforordningen/">Dataskyddsförordning</a></p> -<p>FixaMinGata använder kakor för att hjälpa oss att göra tjänsten bättre. Nedan beskrivs hur din data används.</p> +<p>Fr.o.m. 2018-05-25 tillämpas ett nytt regelverk inom EU för behandling av personuppgifter – i Sverige kallad Dataskyddsfördordningen.</p> +<p><strong>Laglig grund för behandling av personuppgifter</strong><br>FixaMinGata.se behandlar all data, inklusive personuppgifter, baserat på Dataskyddsförordningens Artikel 6.1f - Behandlingen är nödvändig för ändamål som rör den personuppgiftsansvariges eller en tredje parts berättigade intressen.</p> +<p> </p> +<p><a href="http://www.datainspektionen.se/dataskyddsreformen/dataskyddsforordningen/">Dataskyddsförordning</a>en</p> +<p>Förordningen innehåller regler om hur personuppgifter får behandlas. Förordningen gäller i alla EU:s medlemsländer och ersätter nationella regler, som till exempel Personuppgiftslagen i Sverige.</p> +<p>Dataskyddsförordningens syfte är dels att skydda enskildas grundläggande rättigheter och friheter, särskilt deras rätt till skydd av personuppgifter och dels att skapa en enhetlig och likvärdig nivå för skyddet av personuppgifter inom EU, så att det fria flödet av uppgifter inom unionen inte hindras.</p> +<p>En tillsynsmyndighet i varje EU-land ska övervaka att de som behandlar personuppgifter följer dataskyddsförordningen. Tillsynsmyndigheten ska vara fullständigt oberoende i utförandet av sina uppgifter och utövandet av sina befogenheter. I Sverige föreslås <u>Datainspektionen</u> få det uppdraget.</p> +<p>FixaMinGata använder s k kakor för att hjälpa oss att göra tjänsten bättre. Nedan beskrivs hur de data du rapporterar in via FixaMinGata – inklusive eventuella uppgifter om dig själv – används.</p> <h2>Offentlighet och personuppgifter</h2> +<p>För att Du som rapportör ska kunna rapportera ärenden behandlas och lagras personuppgifter i samband med skapande av konto. Inrapporterade ärenden kommer avidentifieras tre månader efter den senaste aktiviteten i ärendet. Det innebär att eventuella personuppgifter i ärendet kommer att raderas, medan uppgifterna om ärendet kan vara av allmänt intresse och kommer att finnas kvar som underlag för forskning, statistik o dyl. Konton som inte använts på 12 månader kommer att raderas och när Du återigen ska rapportera behöver ett nytt konto skapas.</p> <dl> - <dt>Hur hanteras mina personuppgifter?</dt> - <dd>Din e-postadress och namn lagras i vår databas. Du har rätt att begära att få ta del av de uppgifter som finns registrerade om dig genom att kontakta vårt personuppgiftsombud.</dd> - <dt>Hur hanteras min information hos FixaMinGata?</dt> - <dd>Vi kommer att visa en rapports ärende och information publikt, dock inte visa din e-postadress eller ditt namn, om du inte ger oss tillstånd.</dd> - <dt>Hur hanteras min information hos kommunen?</dt> - <dd>All information som du anger kring ett problem kommer att skickas till berörd kommun. Alla handlingar som inkommer till en kommun betraktas som offentliga och därmed kommer också alla rapporter från FixaMinGata att vara offentliga via den berörda kommunen.</dd> + <dt>Hur behandlas mina personuppgifter?</dt> + <dd>Din e-postadress och namn lagras i vår databas. Du har rätt att begära att få ta del av de uppgifter som finns registrerade om dig, begära rättning av uppgifter eller att uppgifter ska raderas genom att kontakta vårt personuppgiftsombud.<p>Konton som inte använts på 12 månader kommer att raderas.</p></dd> + <dt>Hur behandlas min information hos FixaMinGata?</dt> + <dd>Vi kommer att visa en rapports ärende och information publikt, dock inte visa din e-postadress eller ditt namn, om du inte samtycker till detta. Lösta ärenden kommer att avidentifieras tre månader efter den senaste aktiviteten i ärendet (se ovan).</dd> + <dt>Hur behandlas min information hos kommunen?</dt> + <dd>All information som du anger kring ett problem kommer att skickas till berörd kommun eller den part som behandlar en kommuns ärenden. Alla handlingar som inkommer till en kommun betraktas som offentliga och därmed kommer också alla rapporter från FixaMinGata att vara offentliga via den berörda kommunen.</dd> </dl> <h2>Sekretess</h2> <dl> - <dt>Vem ser min epostadress?</dt> - <dd>Om du rapporterar ett problem kommer vi att skicka - din information och upplysning om problemet - till den kommun som är ansvarig för det område där du lokaliserat problemet. Förutom kommunen, som ser din e-postadress, kommer personer som är ansvariga för tjänsten FixaMinGata att kunna se din e-postadress. Ingen kommer att använda din e-postadress för något annat än för att kunna administrera FixaMinGata. Vi kommer inte att ge ut eller sälja din e-postadress till någon annan om vi inte blir tvingade till det av domstol. Ditt namn, som du anger det, kommer inte att publiceras om du inte ger oss tillstånd. Observera dock att om du skriver ditt namn någon annanstans, exempelvis i rapportens information, kommer det att vara synligt.</dd> + <dt>Vem ser min e-postadress?</dt> + <dd>Om du rapporterar ett problem kommer vi att skicka din information och upplysning om problemet till den kommun, eller den part som behandlar en kommuns ärende, som är ansvarig för det område där du lokaliserat problemet. Förutom kommun/partner, som ser din e-postadress, kommer personer som är ansvariga för tjänsten FixaMinGata att kunna se din e-postadress.<p>Din e-postadress kommer <strong><em>bara</em></strong> användas för att kunna administrera FixaMinGata. Vi kommer inte att ge ut eller sälja din e-postadress till någon annan om vi inte blir tvingade till det av domstol. Ditt namn, som du anger det, kommer inte att publiceras om du inte ger samtycke. Observera dock att om du skriver ditt namn någon annanstans, exempelvis i rapportens information, kommer det att vara synligt.</p></dd> <dt>Kommer ni att skicka spam?</dt> <dd>Aldrig. Vi kommer att skicka ett e-postmeddelande till dig om någon lämnar en uppdatering på ett problem som du rapporterat, och fyra veckor efter din rapportering skickas ett frågeformulär. Vi kommer endast att skicka e-postmeddelande som relaterar till ditt problem.</dd> + <dt>Borttag av information, bilder e dyl.</dt> + <dd>Om du laddar upp bilder och annan information kring ett problem, bör du vara försiktig så att inte personer, bilar eller andra uppgifter som kan identifiera en annan person följer med.<p>Begäran om radering av personuppgift sker genom att kontakta vårt personuppgiftsombud.</p></dd> </dl> <h2>Kakor</h2> -<p>Kakor används för att ge en besökare tillgång till olika funktioner. För att göra tjänsten lättare och mer användbara skickas små datafiler till din dator eller mobiltelefon. Dessa kallas för kakor och de flesta webbplatser använder dem. Informationen används för att, till exempel, komma ihåg tidigare inloggning, och för att mäta hur tjänsten används som underlag för förbättringar.</p> -<p><a href="http://www.pts.se/sv/Bransch/Regler/Lagar/Lag-om-elektronisk-kommunikation/Cookies-kakor/">Om kakor och hur du kan hantera dem, Post- och telestyrelsen</a></p> +<p>S k kakor används för att ge en användare tillgång till olika funktioner. För att göra tjänsten lättare och mer användbara skickas små datafiler till din dator eller mobiltelefon. Dessa kallas för kakor och de flesta webbplatser använder dem. Informationen används för att, till exempel, komma ihåg tidigare inloggning, och för att mäta hur tjänsten används som underlag för förbättringar.</p> +<p><a href="http://www.pts.se/sv/Bransch/Regler/Lagar/Lag-om-elektronisk-kommunikation/Cookies-kakor/">Läs mer om kakor och hur du kan hantera dem på Post- och telestyrelsens webbplats.</a></p> <p>Nedan visas en lista med kakor som tjänsten använder.</p> <table cellpadding=5> <tr align="left"><th scope="col">Namn</th><th scope="col">Innehåll</th><th scope="col">Giltighet</th></tr> <tr><td>fixmystreet_app_session</td><td nowrap>A slumpvis sträng</td><td>Raderas när webbläsaren stängs, eller efter fyra veckor om “Håll mig inloggad” är valt</td></tr> </table> +<dl> + <dt>Vad är <em>INTE</em> FixaMinGata till för?</dt> + <dd>FixaMinGata är inte lämpligt för andra typer av problem än de som anges ovan. Du måste kontakta din kommun eller fastighetsägare direkt för problem i stil med:<ul><li>Brådskande och akuta problem</li><li>Oljud i din närmiljö</li><li>Brand och rök</li><li>Förslag på nya vägar, farthinder, etc.</li><li>Klaga på dina grannar</li><li>Klaga på din kommun</li><li>Droger, djurplågeri, stöld, eller andra kriminella aktiviteter.</li><li>Information kring brott/överträdelser eller misstanke om detsamma</li></ul></dd> +</dl> + [% INCLUDE 'footer.html' pagefooter = 'yes' %] |