diff options
56 files changed, 1101 insertions, 349 deletions
diff --git a/.gitignore b/.gitignore index fc1d063c6..2c2171260 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ gh_fixmycommunity *[Ff]ix[Mm]indelo* *[Dd]ans[Mm]on[Qq]wat* *[Cc]uido[Mm]i[Cc]iudad* + +# Commercial +/fixmystreet-commercial +*[Gg]round[Cc]ontrol* diff --git a/CHANGELOG.md b/CHANGELOG.md index ba7225e4b..991ad63ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,16 @@ * Unreleased - New features: - Body and category names can now be translated in the admin. #1244 + - Report states can be edited and translated in the admin. #1826 - Body users can now create reports as an anonymous user. #1796 - Extra fields can be added to report form site-wide. #1743 - Body users can filter reports by all states. #1790 + - `LOGIN_REQUIRED` config key to limit site access to logged-in users. + - `SIGNUPS_DISABLED` config key to prevent new user registrations. - Front end improvements: - Always show pagination figures even if only one page. #1787 - Report pages list every update to a report. #1806 + - Cobrands can implement `hide_areas_on_reports` to hide outline on map. - Admin improvements: - Highlight current shortlisted user in list tooltip. #1788 - Extra fields on contacts can be edited. #1743 diff --git a/bin/fixmystreet.com/fixture b/bin/fixmystreet.com/fixture index 6df675f7c..6cf5ad199 100755 --- a/bin/fixmystreet.com/fixture +++ b/bin/fixmystreet.com/fixture @@ -54,7 +54,7 @@ BEGIN END $func$; }) or die $!; - $db->dbh->do( scalar FixMyStreet->path_to('db/alert_types.sql')->slurp ) or die $!; + $db->dbh->do( scalar FixMyStreet->path_to('db/fixture.sql')->slurp ) or die $!; $db->dbh->do( scalar FixMyStreet->path_to('db/generate_secret.sql')->slurp ) or die $!; say "Emptied database"; } diff --git a/bin/make_css b/bin/make_css index 6a70aba01..b99459cda 100755 --- a/bin/make_css +++ b/bin/make_css @@ -19,6 +19,7 @@ use MIME::Base64; use MIME::Types; use Path::Tiny; use Pod::Usage; +use Try::Tiny; # Store ARGV in case we need to restart later. my @ARGVorig = @ARGV; @@ -125,7 +126,12 @@ sub compile { sub write_if_different { my ($fn, $data) = @_; $fn = path($fn); - my $current = $fn->slurp_utf8; + my $current = try { + $fn->slurp_utf8; + } catch { + return if $_->{err} eq 'No such file or directory'; + die $_; + }; if (!$current || $current ne $data) { $fn->spew_utf8($data); return 1; diff --git a/bin/update-schema b/bin/update-schema index 32c00ff5e..a27bbbba1 100755 --- a/bin/update-schema +++ b/bin/update-schema @@ -40,6 +40,7 @@ BEGIN { } use FixMyStreet; +use FixMyStreet::Cobrand; use FixMyStreet::DB; use mySociety::MaPit; use Getopt::Long; @@ -112,7 +113,7 @@ if ($upgrade && $current_version eq 'EMPTY') { if ($commit) { run_statements(get_statements("$bin_dir/../db/schema.sql")); run_statements(get_statements("$bin_dir/../db/generate_secret.sql")); - run_statements(get_statements("$bin_dir/../db/alert_types.sql")); + run_statements(get_statements("$bin_dir/../db/fixture.sql")); } } elsif ($upgrade) { if ($version) { @@ -139,12 +140,28 @@ if ($upgrade && $current_version eq 'EMPTY') { my $area_ids = $db->dbh->selectcol_arrayref('SELECT area_id FROM body_areas'); if ( @$area_ids ) { my $areas = mySociety::MaPit::call('areas', $area_ids); + $db->txn_begin; foreach (values %$areas) { $db->dbh->do('UPDATE body SET name=? WHERE id=?', {}, $_->{name}, $_->{id}); } $db->txn_commit; } } + + if ( $commit && $current_version lt '0054' ) { + $nothing = 0; + print "States created, importing names\n"; + my @avail = FixMyStreet::Cobrand->available_cobrand_classes; + # Pick first available cobrand and language for database name import + my $cobrand = $avail[0] ? FixMyStreet::Cobrand::class($avail[0]) : 'FixMyStreet::Cobrand::Default'; + my $lang = $cobrand->new->set_lang_and_domain(undef, 1, FixMyStreet->path_to('locale')->stringify); + my $names = $db->dbh->selectcol_arrayref('SELECT name FROM state'); + $db->txn_begin; + foreach (@$names) { + $db->dbh->do('UPDATE state SET name=? WHERE name=?', {}, _($_), $_); + } + $db->txn_commit; + } } if ($downgrade) { @@ -195,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 '0054' if table_exists('state'); return '0053' if table_exists('report_extra_fields'); return '0052' if table_exists('translation'); return '0051' if column_exists('contacts', 'state'); diff --git a/conf/general.yml-example b/conf/general.yml-example index 0e437fac3..345a6426d 100644 --- a/conf/general.yml-example +++ b/conf/general.yml-example @@ -204,3 +204,10 @@ TESTING_COUNCILS: '' # if you're using Message Manager, include the URL here (see https://github.com/mysociety/message-manager/) MESSAGE_MANAGER_URL: '' + +# If you want to hide all pages from non-logged-in users, set this to 1. +LOGIN_REQUIRED: 0 + +# If you want to stop new users from registering, set this to 1. +# NB: This also disables all Facebook/Twitter logins. +SIGNUPS_DISABLED: 0 diff --git a/db/downgrade_0054---0053.sql b/db/downgrade_0054---0053.sql new file mode 100644 index 000000000..6fcc41c33 --- /dev/null +++ b/db/downgrade_0054---0053.sql @@ -0,0 +1,39 @@ +BEGIN; + +DROP TABLE state; + +ALTER TABLE problem ADD CONSTRAINT problem_state_check CHECK ( + state = 'unconfirmed' + or state = 'hidden' + or state = 'partial' + or state = 'confirmed' + or state = 'investigating' + or state = 'planned' + or state = 'in progress' + or state = 'action scheduled' + or state = 'fixed' + or state = 'fixed - council' + or state = 'fixed - user' + or state = 'closed' + or state = 'unable to fix' + or state = 'not responsible' + or state = 'duplicate' + or state = 'internal referral' +); +ALTER TABLE comment ADD CONSTRAINT comment_problem_state_check CHECK ( + problem_state = 'confirmed' + or problem_state = 'investigating' + or problem_state = 'planned' + or problem_state = 'in progress' + or problem_state = 'action scheduled' + or problem_state = 'fixed' + or problem_state = 'fixed - council' + or problem_state = 'fixed - user' + or problem_state = 'closed' + or problem_state = 'unable to fix' + or problem_state = 'not responsible' + or problem_state = 'duplicate' + or problem_state = 'internal referral' +); + +COMMIT; diff --git a/db/alert_types.sql b/db/fixture.sql index 471fd905f..840906223 100644 --- a/db/alert_types.sql +++ b/db/fixture.sql @@ -1,3 +1,13 @@ +INSERT INTO state (label, type, name) VALUES ('investigating', 'open', 'Investigating'); +INSERT INTO state (label, type, name) VALUES ('in progress', 'open', 'In progress'); +INSERT INTO state (label, type, name) VALUES ('planned', 'open', 'Planned'); +INSERT INTO state (label, type, name) VALUES ('action scheduled', 'open', 'Action scheduled'); +INSERT INTO state (label, type, name) VALUES ('unable to fix', 'closed', 'No further action'); +INSERT INTO state (label, type, name) VALUES ('not responsible', 'closed', 'Not responsible'); +INSERT INTO state (label, type, name) VALUES ('duplicate', 'closed', 'Duplicate'); +INSERT INTO state (label, type, name) VALUES ('internal referral', 'closed', 'Internal referral'); +INSERT INTO state (label, type, name) VALUES ('fixed', 'fixed', 'Fixed'); + -- New updates on a particular problem report insert into alert_type (ref, head_sql_query, head_table, @@ -18,11 +28,8 @@ insert into alert_type values ('new_problems', '', '', 'New problems on FixMyStreet', '/', 'The latest problems reported by users', 'problem', - 'problem.non_public = ''f'' and problem.state in - (''confirmed'', ''investigating'', ''planned'', ''in progress'', - ''fixed'', ''fixed - council'', ''fixed - user'', ''closed'' - ''action scheduled'', ''not responsible'', ''duplicate'', ''unable to fix'', - ''internal referral'' )', + 'problem.non_public = ''f'' and problem.state NOT IN + (''unconfirmed'', ''hidden'', ''partial'')', 'created desc', '{{title}}, {{confirmed}}', '/report/{{id}}', '{{detail}}', 'alert-problem'); @@ -46,11 +53,8 @@ insert into alert_type values ('local_problems', '', '', 'New local problems on FixMyStreet', '/', 'The latest local problems reported by users', 'problem_find_nearby(?, ?, ?) as nearby,problem', - 'nearby.problem_id = problem.id and problem.non_public = ''f'' and problem.state in - (''confirmed'', ''investigating'', ''planned'', ''in progress'', - ''fixed'', ''fixed - council'', ''fixed - user'', ''closed'', - ''action scheduled'', ''not responsible'', ''duplicate'', ''unable to fix'', - ''internal referral'')', + 'nearby.problem_id = problem.id and problem.non_public = ''f'' and problem.state NOT IN + (''unconfirmed'', ''hidden'', ''partial'')', 'created desc', '{{title}}, {{confirmed}}', '/report/{{id}}', '{{detail}}', 'alert-problem-nearby'); @@ -74,11 +78,8 @@ insert into alert_type values ('postcode_local_problems', '', '', 'New problems near {{POSTCODE}} on FixMyStreet', '/', 'The latest local problems reported by users', 'problem_find_nearby(?, ?, ?) as nearby,problem', - 'nearby.problem_id = problem.id and problem.non_public = ''f'' and problem.state in - (''confirmed'', ''investigating'', ''planned'', ''in progress'', - ''fixed'', ''fixed - council'', ''fixed - user'', ''closed'', - ''action scheduled'', ''not responsible'', ''duplicate'', ''unable to fix'', - ''internal referral'')', + 'nearby.problem_id = problem.id and problem.non_public = ''f'' and problem.state NOT IN + (''unconfirmed'', ''hidden'', ''partial'')', 'created desc', '{{title}}, {{confirmed}}', '/report/{{id}}', '{{detail}}', 'alert-problem-nearby'); @@ -102,11 +103,8 @@ insert into alert_type values ('council_problems', '', '', 'New problems to {{COUNCIL}} on FixMyStreet', '/reports', 'The latest problems for {{COUNCIL}} reported by users', 'problem', - 'problem.non_public = ''f'' and problem.state in - (''confirmed'', ''investigating'', ''planned'', ''in progress'', - ''fixed'', ''fixed - council'', ''fixed - user'', ''closed'', - ''action scheduled'', ''not responsible'', ''duplicate'', ''unable to fix'', - ''internal referral'' ) AND + 'problem.non_public = ''f'' and problem.state NOT IN + (''unconfirmed'', ''hidden'', ''partial'') AND regexp_split_to_array(bodies_str, '','') && ARRAY[?]', 'created desc', '{{title}}, {{confirmed}}', '/report/{{id}}', '{{detail}}', 'alert-problem-council' @@ -122,11 +120,8 @@ values ('ward_problems', '', '', 'New problems for {{COUNCIL}} within {{WARD}} ward on FixMyStreet', '/reports', 'The latest problems for {{COUNCIL}} within {{WARD}} ward reported by users', 'problem', - 'problem.non_public = ''f'' and problem.state in - (''confirmed'', ''investigating'', ''planned'', ''in progress'', - ''fixed'', ''fixed - council'', ''fixed - user'', ''closed'', - ''action scheduled'', ''not responsible'', ''duplicate'', ''unable to fix'', - ''internal referral'' ) AND + 'problem.non_public = ''f'' and problem.state NOT IN + (''unconfirmed'', ''hidden'', ''partial'') AND (regexp_split_to_array(bodies_str, '','') && ARRAY[?] or bodies_str is null) and areas like ''%,''||?||'',%''', 'created desc', @@ -142,11 +137,8 @@ insert into alert_type values ('area_problems', '', '', 'New problems within {{NAME}}''s boundary on FixMyStreet', '/reports', 'The latest problems within {{NAME}}''s boundary reported by users', 'problem', - 'problem.non_public = ''f'' and problem.state in - (''confirmed'', ''investigating'', ''planned'', ''in progress'', - ''fixed'', ''fixed - council'', ''fixed - user'', ''closed'', - ''action scheduled'', ''not responsible'', ''duplicate'', ''unable to fix'', - ''internal referral'' ) AND + 'problem.non_public = ''f'' and problem.state NOT IN + (''unconfirmed'', ''hidden'', ''partial'') AND areas like ''%,''||?||'',%''', 'created desc', '{{title}}, {{confirmed}}', '/report/{{id}}', '{{detail}}', 'alert-problem-area' diff --git a/db/schema.sql b/db/schema.sql index ed930a13e..fedab2b9d 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -177,24 +177,7 @@ create table problem ( -- Metadata created timestamp not null default current_timestamp, confirmed timestamp, - state text not null check ( - state = 'unconfirmed' - or state = 'confirmed' - or state = 'investigating' - or state = 'planned' - or state = 'in progress' - or state = 'action scheduled' - or state = 'closed' - or state = 'fixed' - or state = 'fixed - council' - or state = 'fixed - user' - or state = 'hidden' - or state = 'partial' - or state = 'unable to fix' - or state = 'not responsible' - or state = 'duplicate' - or state = 'internal referral' - ), + state text not null, lang text not null default 'en-gb', service text not null default '', cobrand text not null default '' check (cobrand ~* '^[a-z0-9_]*$'), @@ -327,21 +310,7 @@ create table comment ( cobrand_data text not null default '' check (cobrand_data ~* '^[a-z0-9_]*$'), -- Extra data used in cobranded versions of the site mark_fixed boolean not null, mark_open boolean not null default 'f', - problem_state text check ( - problem_state = 'confirmed' - or problem_state = 'investigating' - or problem_state = 'planned' - or problem_state = 'in progress' - or problem_state = 'action scheduled' - or problem_state = 'closed' - or problem_state = 'fixed' - or problem_state = 'fixed - council' - or problem_state = 'fixed - user' - or problem_state = 'unable to fix' - or problem_state = 'not responsible' - or problem_state = 'duplicate' - or problem_state = 'internal referral' - ), + problem_state text, -- other fields? one to indicate whether this was written by the council -- and should be highlighted in the display? external_id text, @@ -546,3 +515,10 @@ CREATE TABLE report_extra_fields ( language text, extra text ); + +CREATE TABLE state ( + id serial not null primary key, + label text not null unique, + type text not null check (type = 'open' OR type = 'closed' OR type = 'fixed'), + name text not null unique +); diff --git a/db/schema_0054-add-state-table.sql b/db/schema_0054-add-state-table.sql new file mode 100644 index 000000000..c4be36015 --- /dev/null +++ b/db/schema_0054-add-state-table.sql @@ -0,0 +1,51 @@ +BEGIN; + +CREATE TABLE state ( + id serial not null primary key, + label text not null unique, + type text not null check (type = 'open' OR type = 'closed' OR type = 'fixed'), + name text not null unique +); + +INSERT INTO state (label, type, name) VALUES ('investigating', 'open', 'Investigating'); +INSERT INTO state (label, type, name) VALUES ('in progress', 'open', 'In progress'); +INSERT INTO state (label, type, name) VALUES ('planned', 'open', 'Planned'); +INSERT INTO state (label, type, name) VALUES ('action scheduled', 'open', 'Action scheduled'); +INSERT INTO state (label, type, name) VALUES ('unable to fix', 'closed', 'No further action'); +INSERT INTO state (label, type, name) VALUES ('not responsible', 'closed', 'Not responsible'); +INSERT INTO state (label, type, name) VALUES ('duplicate', 'closed', 'Duplicate'); +INSERT INTO state (label, type, name) VALUES ('internal referral', 'closed', 'Internal referral'); +INSERT INTO state (label, type, name) VALUES ('fixed', 'fixed', 'Fixed'); + +ALTER TABLE problem DROP CONSTRAINT problem_state_check; +ALTER TABLE comment DROP CONSTRAINT comment_problem_state_check; + +UPDATE alert_type SET item_where = 'nearby.problem_id = problem.id + and problem.non_public = ''f'' + and problem.state NOT IN (''hidden'', ''unconfirmed'', ''partial'')' + WHERE ref = 'postcode_local_problems'; +UPDATE alert_type set item_where = 'problem.non_public = ''f'' + and problem.state NOT IN (''hidden'', ''unconfirmed'', ''partial'')' + WHERE ref = 'new_problems'; +UPDATE alert_type set item_where = 'problem.non_public = ''f'' + and problem.state in (''fixed'', ''fixed - user'', ''fixed - council'')' + WHERE ref = 'new_fixed_problems'; +UPDATE alert_type set item_where = 'nearby.problem_id = problem.id + and problem.non_public = ''f'' + and problem.state NOT IN (''hidden'', ''unconfirmed'', ''partial'')' + WHERE ref = 'local_problems'; +UPDATE alert_type set item_where = 'problem.non_public = ''f'' + AND problem.state NOT IN (''hidden'', ''unconfirmed'', ''partial'') + AND regexp_split_to_array(bodies_str, '','') && ARRAY[?]' + WHERE ref = 'council_problems'; +UPDATE alert_type set item_where = 'problem.non_public = ''f'' + AND problem.state NOT IN (''hidden'', ''unconfirmed'', ''partial'') + AND (regexp_split_to_array(bodies_str, '','') && ARRAY[?] or bodies_str is null) and + areas like ''%,''||?||'',%''' + WHERE ref = 'ward_problems'; +UPDATE alert_type set item_where = 'problem.non_public = ''f'' + AND problem.state NOT IN (''hidden'', ''unconfirmed'', ''partial'') + AND areas like ''%,''||?||'',%''' + WHERE ref = 'area_problems'; + +COMMIT; diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm index 25d3d744e..a0477ca40 100644 --- a/perllib/FixMyStreet/App.pm +++ b/perllib/FixMyStreet/App.pm @@ -173,6 +173,7 @@ sub setup_request { $c->setup_dev_overrides(); my $cobrand = $c->cobrand; + FixMyStreet::DB->schema->cobrand($cobrand); $cobrand->call_hook('add_response_headers'); @@ -218,6 +219,7 @@ sub setup_request { mySociety::MaPit::configure( "http://$host/fakemapit/" ); } + $c->stash->{has_fixed_state} = FixMyStreet::DB::Result::Problem::fixed_states->{fixed}; $c->cobrand->call_hook('setup_states'); if (FixMyStreet->test_mode) { diff --git a/perllib/FixMyStreet/App/Controller/Admin/States.pm b/perllib/FixMyStreet/App/Controller/Admin/States.pm new file mode 100644 index 000000000..e4c07c9ca --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/States.pm @@ -0,0 +1,102 @@ +package FixMyStreet::App::Controller::Admin::States; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +sub begin : Private { + my ( $self, $c ) = @_; + + $c->forward('/admin/begin'); +} + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + + $c->forward('/auth/get_csrf_token'); + $c->forward('/admin/fetch_languages'); + my $rs = $c->model('DB::State'); + + if ($c->req->method eq 'POST') { + $c->forward('/auth/check_csrf_token'); + + $c->forward('process_new') + && $c->forward('delete') + && $c->forward('update'); + + $rs->clear; + } + + $c->stash->{open_states} = $rs->open; + $c->stash->{closed_states} = $rs->closed; + $c->stash->{fixed_states} = $rs->fixed; +} + +sub process_new : Private { + my ($self, $c) = @_; + if ($c->get_param('new_fixed')) { + $c->model('DB::State')->create({ + label => 'fixed', + type => 'fixed', + name => _('Fixed'), + }); + return 0; + } + return 1 unless $c->get_param('new'); + my %params = map { $_ => $c->get_param($_) } qw/label type name/; + $c->model('DB::State')->create(\%params); + return 0; +} + +sub delete : Private { + my ($self, $c) = @_; + + my @params = keys %{ $c->req->params }; + my ($to_delete) = map { /^delete:(.*)/ } grep { /^delete:/ } @params; + if ($to_delete) { + $c->model('DB::State')->search({ label => $to_delete })->delete; + return 0; + } + return 1; +} + +sub update : Private { + my ($self, $c) = @_; + + my $rs = $c->model('DB::State'); + my %db_states = map { $_->label => $_ } @{$rs->states}; + my @params = keys %{ $c->req->params }; + my @states = map { /^type:(.*)/ } grep { /^type:/ } @params; + + foreach my $state (@states) { + # If there is only one language, we still store confirmed/closed + # as translations, as that seems a sensible place to store them. + if ($state eq 'confirmed' or $state eq 'closed') { + if (my $name = $c->get_param("name:$state")) { + my ($lang) = keys %{$c->stash->{languages}}; + $db_states{$state}->add_translation_for('name', $lang, $name); + } + } else { + $db_states{$state}->update({ + type => $c->get_param("type:$state"), + name => $c->get_param("name:$state"), + }); + } + + foreach my $lang (keys(%{$c->stash->{languages}})) { + my $id = $c->get_param("translation_id:$state:$lang"); + my $text = $c->get_param("translation:$state:$lang"); + if ($text) { + $db_states{$state}->add_translation_for('name', $lang, $text); + } elsif ($id) { + $c->model('DB::Translation')->find({ id => $id })->delete; + } + } + } + + return 1; +} + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 83fb0554c..825066026 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -128,6 +128,18 @@ sub email_sign_in : Private { return; } + # If user registration is disabled then bail out at this point + # if there's not already a user with this email address. + # NB this uses the same template as a successful sign in to stop + # enumeration of valid email addresses. + if ( FixMyStreet->config('SIGNUPS_DISABLED') + && !$c->model('DB::User')->search({ email => $good_email })->count + && !$c->stash->{current_user} # don't break the change email flow + ) { + $c->stash->{template} = 'auth/token.html'; + return; + } + my $user_params = {}; $user_params->{password} = $c->get_param('password_register') if $c->get_param('password_register'); @@ -199,6 +211,10 @@ sub token : Path('/M') : Args(1) { my $user = $c->model('DB::User')->find_or_new({ email => $data->{email} }); + # Bail out if this is a new user and SIGNUPS_DISABLED is set + $c->detach( '/page_error_403_access_denied', [] ) + if FixMyStreet->config('SIGNUPS_DISABLED') && !$user->in_storage && !$data->{old_email}; + if ($data->{old_email}) { # Were logged in as old_email, want to switch to email ($user) if ($user->in_storage) { @@ -244,6 +260,8 @@ sub fb : Private { sub facebook_sign_in : Private { my ( $self, $c ) = @_; + $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED'); + my $fb = $c->forward('/auth/fb'); my $url = $fb->get_authorization_url(scope => ['email']); @@ -302,6 +320,8 @@ sub tw : Private { sub twitter_sign_in : Private { my ( $self, $c ) = @_; + $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED'); + my $twitter = $c->forward('/auth/tw'); my $url = $twitter->get_authentication_url(callback => $c->uri_for('/auth/Twitter')); diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm index fbe5a2dc9..f3989e760 100644 --- a/perllib/FixMyStreet/App/Controller/Dashboard.pm +++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm @@ -24,6 +24,8 @@ sub example : Local : Args(0) { my ( $self, $c ) = @_; $c->stash->{template} = 'dashboard/index.html'; + $c->stash->{filter_states} = $c->cobrand->state_groups_inspect; + $c->stash->{children} = {}; for my $i (1..3) { $c->stash->{children}{$i} = { id => $i, name => "Ward $i" }; @@ -93,6 +95,7 @@ sub index : Path : Args(0) { $c->stash->{body} = $body; # Set up the data for the dropdowns + $c->stash->{filter_states} = $c->cobrand->state_groups_inspect; # Just take the first area ID we find my $area_id = $body->body_areas->first->area_id; @@ -145,12 +148,10 @@ sub index : Path : Args(0) { # List of reports underneath summary table $c->stash->{q_state} = $c->get_param('state') || ''; - if ( $c->stash->{q_state} eq 'fixed' ) { + if ( $c->stash->{q_state} eq 'fixed - council' ) { $prob_where->{'me.state'} = [ FixMyStreet::DB::Result::Problem->fixed_states() ]; } elsif ( $c->stash->{q_state} ) { $prob_where->{'me.state'} = $c->stash->{q_state}; - $prob_where->{'me.state'} = { IN => [ 'planned', 'action scheduled' ] } - if $prob_where->{'me.state'} eq 'action scheduled'; } my $params = { %$prob_where, diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm index b597cb7a8..8f8205719 100644 --- a/perllib/FixMyStreet/App/Controller/Reports.pm +++ b/perllib/FixMyStreet/App/Controller/Reports.pm @@ -558,12 +558,11 @@ sub stash_report_filter_status : Private { if ($c->user and ($c->user->is_superuser or ( $c->stash->{body} and $c->user->belongs_to_body($c->stash->{body}->id) ))) { + $c->stash->{filter_states} = $c->cobrand->state_groups_inspect; foreach my $state (FixMyStreet::DB::Result::Problem->visible_states()) { if ($status{$state}) { - %filter_problem_states = (%filter_problem_states, ($state => 1)); - my $pretty_state = $state; - $pretty_state =~ tr/ /_/; - $filter_status{$pretty_state} = 1; + $filter_problem_states{$state} = 1; + $filter_status{$state} = 1; } } } diff --git a/perllib/FixMyStreet/App/Controller/Root.pm b/perllib/FixMyStreet/App/Controller/Root.pm index 64d7fa6ae..7f70623ae 100644 --- a/perllib/FixMyStreet/App/Controller/Root.pm +++ b/perllib/FixMyStreet/App/Controller/Root.pm @@ -16,6 +16,18 @@ FixMyStreet::App::Controller::Root - Root Controller for FixMyStreet::App =head1 METHODS +=head2 begin + +Any pre-flight checking for all requests + +=cut +sub begin : Private { + my ( $self, $c ) = @_; + + $c->forward( 'check_login_required' ); +} + + =head2 auto Set up general things for this instance @@ -130,6 +142,27 @@ sub page_error : Private { $c->response->status($code); } +sub check_login_required : Private { + my ($self, $c) = @_; + + return if $c->user_exists || !FixMyStreet->config('LOGIN_REQUIRED'); + + # Whitelisted URL patterns are allowed without login + my $whitelist = qr{ + ^auth(/|$) + | ^js/translation_strings\.(.*?)\.js + | ^[PACQM]/ # various tokens that log the user in + }x; + return if $c->request->path =~ $whitelist; + + # Blacklisted URLs immediately 404 + # This is primarily to work around a Safari bug where the appcache + # URL is requested in an infinite loop if it returns a 302 redirect. + $c->detach('/page_error_404_not_found', []) if $c->request->path =~ /^offline/; + + $c->detach( '/auth/redirect' ); +} + =head2 end Attempt to render a view, if needed. diff --git a/perllib/FixMyStreet/App/Model/DB.pm b/perllib/FixMyStreet/App/Model/DB.pm index db8e72c27..c116abffc 100644 --- a/perllib/FixMyStreet/App/Model/DB.pm +++ b/perllib/FixMyStreet/App/Model/DB.pm @@ -21,6 +21,7 @@ __PACKAGE__->config( sub build_per_context_instance { my ( $self, $c ) = @_; + # $self->schema->cobrand($c->cobrand); $self->schema->cache({}); return $self; } diff --git a/perllib/FixMyStreet/App/View/Web.pm b/perllib/FixMyStreet/App/View/Web.pm index 93c459e26..93aa0e2fb 100644 --- a/perllib/FixMyStreet/App/View/Web.pm +++ b/perllib/FixMyStreet/App/View/Web.pm @@ -170,12 +170,8 @@ sub decode { sub prettify_state { my ($self, $c, $text, $single_fixed) = @_; - # New template to prevent interaction with current one - my $tt = FixMyStreet::Template->new({ INCLUDE_PATH => $self->{include_path} }); - my $var; - $tt->process('report/state-list.html', { state => $text }, \$var); - $var =~ s/ - .*// if $single_fixed; - return $var; + + return FixMyStreet::DB->resultset("State")->display($text, $single_fixed); } 1; diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 5dcdc9a4b..250919d09 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -179,7 +179,7 @@ sub restriction { return $self->moniker ? { cobrand => $self->moniker } : {}; } -=head2 base_url_with_lang +=head2 base_url_with_lang =cut @@ -358,7 +358,7 @@ sub front_stats_data { Returns any disambiguating information available. Defaults to none. -=cut +=cut sub disambiguate_location { FixMyStreet->config('GEOCODING_DISAMBIGUATION') or {}; } @@ -642,6 +642,7 @@ sub admin_pages { # There are some pages that only super users can see if ( $user->is_superuser ) { $pages->{flagged} = [ _('Flagged'), 7 ]; + $pages->{states} = [ _('States'), 8 ]; $pages->{config} = [ _('Configuration'), 9]; }; # And some that need special permissions @@ -820,7 +821,7 @@ sub is_two_tier { 0; } =item council_rss_alert_options -Generate a set of options for council rss alerts. +Generate a set of options for council rss alerts. =cut @@ -1066,6 +1067,28 @@ sub show_unconfirmed_reports { 0; } +sub state_groups_admin { + my $rs = FixMyStreet::DB->resultset("State"); + my @fixed = FixMyStreet::DB::Result::Problem->fixed_states; + [ + [ $rs->display('confirmed'), [ FixMyStreet::DB::Result::Problem->open_states ] ], + @fixed ? [ $rs->display('fixed'), [ FixMyStreet::DB::Result::Problem->fixed_states ] ] : (), + [ $rs->display('closed'), [ FixMyStreet::DB::Result::Problem->closed_states ] ], + [ $rs->display('hidden'), [ FixMyStreet::DB::Result::Problem->hidden_states ] ] + ] +} + +sub state_groups_inspect { + my $rs = FixMyStreet::DB->resultset("State"); + my @fixed = FixMyStreet::DB::Result::Problem->fixed_states; + [ + [ $rs->display('confirmed'), [ grep { $_ ne 'planned' } FixMyStreet::DB::Result::Problem->open_states ] ], + @fixed ? [ $rs->display('fixed'), [ 'fixed - council' ] ] : (), + [ $rs->display('closed'), [ grep { $_ ne 'closed' } FixMyStreet::DB::Result::Problem->closed_states ] ], + [ $rs->display('hidden'), [ 'hidden' ] ] + ] +} + =head2 never_confirm_updates If true then we never send an email to confirm an update diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm index a061ff46c..b3d6b28c3 100644 --- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm +++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm @@ -122,12 +122,19 @@ sub path_to_pin_icons { sub pin_hover_title { my ($self, $problem, $title) = @_; - my $state = $self->{c}->render_fragment( - 'report/state-list.html', - { state => $problem->state }); + my $state = FixMyStreet::DB->resultset("State")->display($problem->state, 1); return "$state: $title"; } +sub state_groups_inspect { + [ + [ _('New'), [ 'confirmed', 'investigating' ] ], + [ _('Scheduled'), [ 'action scheduled' ] ], + [ _('Fixed'), [ 'fixed - council' ] ], + [ _('Closed'), [ 'not responsible', 'duplicate', 'unable to fix' ] ], + ] +} + sub open311_config { my ($self, $row, $h, $params) = @_; diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm index d688eb8b9..562f29693 100644 --- a/perllib/FixMyStreet/DB/Result/Comment.pm +++ b/perllib/FixMyStreet/DB/Result/Comment.pm @@ -227,8 +227,6 @@ sub meta_line { my $meta = ''; - $c->stash->{last_state} ||= ''; - if ($self->anonymous or !$self->name) { $meta = sprintf( _( 'Posted anonymously at %s' ), Utils::prettify_dt( $self->confirmed ) ) } elsif ($self->user->from_body) { @@ -257,60 +255,36 @@ sub meta_line { $meta = sprintf( _( 'Posted by %s at %s' ), FixMyStreet::Template::html_filter($self->name), Utils::prettify_dt( $self->confirmed ) ) } + if ($self->get_extra_metadata('defect_raised')) { + $meta .= ', ' . _( 'and a defect raised' ); + } + + return $meta; +}; + +sub problem_state_display { + my ( $self, $c ) = @_; + my $update_state = ''; + my $cobrand = $c->cobrand->moniker; if ($self->mark_fixed) { - $update_state = _( 'marked as fixed' ); + return FixMyStreet::DB->resultset("State")->display('fixed', 1); } elsif ($self->mark_open) { - $update_state = _( 'reopened' ); + return FixMyStreet::DB->resultset("State")->display('confirmed', 1); } elsif ($self->problem_state) { my $state = $self->problem_state; - - if ($state eq 'confirmed') { - if ($c->stash->{last_state}) { - $update_state = _( 'reopened' ) - } - } elsif ($state eq 'investigating') { - $update_state = _( 'marked as investigating' ) - } elsif ($state eq 'planned') { - $update_state = _( 'marked as planned' ) - } elsif ($state eq 'in progress') { - $update_state = _( 'marked as in progress' ) - } elsif ($state eq 'action scheduled') { - $update_state = _( 'marked as action scheduled' ) - } elsif ($state eq 'closed') { - $update_state = _( 'marked as closed' ) - } elsif ($state =~ /^fixed/) { - $update_state = _( 'marked as fixed' ) - } elsif ($state eq 'unable to fix') { - $update_state = _( 'marked as no further action' ) - } elsif ($state eq 'not responsible') { - $update_state = _( "marked as not the council's responsibility" ) - } elsif ($state eq 'duplicate') { - $update_state = _( 'closed as a duplicate report' ) - } elsif ($state eq 'internal referral') { - $update_state = _( 'marked as an internal referral' ) - } - - if ($c->cobrand->moniker eq 'bromley' || $self->problem->to_body_named('Bromley')) { - if ($state eq 'not responsible') { - $update_state = 'marked as third party responsibility' + if ($state eq 'not responsible') { + $update_state = _( "not the council's responsibility" ); + if ($cobrand eq 'bromley' || $self->problem->to_body_named('Bromley')) { + $update_state = 'third party responsibility'; } + } else { + $update_state = FixMyStreet::DB->resultset("State")->display($state, 1); } - } - if ($update_state ne $c->stash->{last_state} and $update_state) { - $meta .= ", $update_state"; - } - - if ($self->get_extra_metadata('defect_raised')) { - $meta .= ', ' . _( 'and a defect raised' ); - } - - $c->stash->{last_state} = $update_state; - - return $meta; -}; + return $update_state; +} 1; diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index a74a04828..77190679b 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -220,15 +220,8 @@ HASHREF. =cut sub open_states { - my $states = { - 'confirmed' => 1, - 'investigating' => 1, - 'in progress' => 1, - 'planned' => 1, - 'action scheduled' => 1, - }; - - return wantarray ? keys %{$states} : $states; + my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->open}; + return wantarray ? @states : { map { $_ => 1 } @states }; } =head2 @@ -242,13 +235,9 @@ HASHREF. =cut sub fixed_states { - my $states = { - 'fixed' => 1, - 'fixed - user' => 1, - 'fixed - council' => 1, - }; - - return wantarray ? keys %{ $states } : $states; + my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->fixed}; + push @states, 'fixed - user', 'fixed - council' if @states; + return wantarray ? @states : { map { $_ => 1 } @states }; } =head2 @@ -262,18 +251,10 @@ HASHREF. =cut sub closed_states { - my $states = { - 'closed' => 1, - 'unable to fix' => 1, - 'not responsible' => 1, - 'duplicate' => 1, - 'internal referral' => 1, - }; - - return wantarray ? keys %{$states} : $states; + my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->closed}; + return wantarray ? @states : { map { $_ => 1 } @states }; } - =head2 @states = FixMyStreet::DB::Problem::all_states(); @@ -289,21 +270,10 @@ sub all_states { 'hidden' => 1, 'partial' => 1, 'unconfirmed' => 1, - 'confirmed' => 1, - 'investigating' => 1, - 'in progress' => 1, - 'planned' => 1, - 'action scheduled' => 1, - 'fixed' => 1, 'fixed - council' => 1, 'fixed - user' => 1, - 'unable to fix' => 1, - 'not responsible' => 1, - 'duplicate' => 1, - 'closed' => 1, - 'internal referral' => 1, }; - + map { $states->{$_->label} = 1 } @{FixMyStreet::DB->resultset("State")->states}; return wantarray ? keys %{$states} : $states; } @@ -920,15 +890,21 @@ sub photos { my $id = $self->id; my @photos = map { my $cachebust = substr($_, 0, 8); + # Some Varnish configurations (e.g. on mySociety infra) strip cookies from + # images, which means image requests will be redirected to the login page + # if LOGIN_REQUIRED is set. To stop this happening, Varnish should be + # configured to not strip cookies if the cookie_passthrough param is + # present, which this line ensures will be if LOGIN_REQUIRED is set. + my $extra = (FixMyStreet->config('LOGIN_REQUIRED')) ? "&cookie_passthrough=1" : ""; my ($hash, $format) = split /\./, $_; { id => $hash, - url_temp => "/photo/temp.$hash.$format", - url_temp_full => "/photo/fulltemp.$hash.$format", - url => "/photo/$id.$i.$format?$cachebust", - url_full => "/photo/$id.$i.full.$format?$cachebust", - url_tn => "/photo/$id.$i.tn.$format?$cachebust", - url_fp => "/photo/$id.$i.fp.$format?$cachebust", + url_temp => "/photo/temp.$hash.$format$extra", + url_temp_full => "/photo/fulltemp.$hash.$format$extra", + url => "/photo/$id.$i.$format?$cachebust$extra", + url_full => "/photo/$id.$i.full.$format?$cachebust$extra", + url_tn => "/photo/$id.$i.tn.$format?$cachebust$extra", + url_fp => "/photo/$id.$i.fp.$format?$cachebust$extra", idx => $i++, } } $photoset->all_ids; diff --git a/perllib/FixMyStreet/DB/Result/State.pm b/perllib/FixMyStreet/DB/Result/State.pm new file mode 100644 index 000000000..b8a35d42b --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/State.pm @@ -0,0 +1,48 @@ +use utf8; +package FixMyStreet::DB::Result::State; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; +__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn"); +__PACKAGE__->table("state"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "state_id_seq", + }, + "label", + { data_type => "text", is_nullable => 0 }, + "type", + { data_type => "text", is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 0 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->add_unique_constraint("state_label_key", ["label"]); +__PACKAGE__->add_unique_constraint("state_name_key", ["name"]); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-08-22 15:17:43 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dvtAOpeYqEF9T3otHHgLqw + +use Moo; +use namespace::clean; + +with 'FixMyStreet::Roles::Translatable'; + +sub msgstr { + my $self = shift; + my $lang = $self->result_source->schema->lang; + return $self->name unless $lang && $self->translated->{name}{$lang}; + return $self->translated->{name}{$lang}{msgstr}; +} + +1; diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm index e44b2530f..19adf5d49 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -295,6 +295,16 @@ sub permissions { sub has_permission_to { my ($self, $permission_type, $body_ids) = @_; + # Nobody, including superusers, can have a permission which isn't available + # in the current cobrand. + my $cobrand = $self->result_source->schema->cobrand; + my $cobrand_perms = $cobrand->available_permissions; + my %available = map { %$_ } values %$cobrand_perms; + # The 'trusted' permission is never set in the cobrand's + # available_permissions (see note there in Default.pm) so include it here. + $available{trusted} = 1; + return 0 unless $available{$permission_type}; + return 1 if $self->is_superuser; return 0 if !$body_ids || (ref $body_ids && !@$body_ids); $body_ids = [ $body_ids ] unless ref $body_ids; diff --git a/perllib/FixMyStreet/DB/ResultSet/State.pm b/perllib/FixMyStreet/DB/ResultSet/State.pm new file mode 100644 index 000000000..8b6a8963e --- /dev/null +++ b/perllib/FixMyStreet/DB/ResultSet/State.pm @@ -0,0 +1,78 @@ +package FixMyStreet::DB::ResultSet::State; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings; +use Memcached; + +sub _hardcoded_states { + my $rs = shift; + my $open = $rs->new({ id => -1, label => 'confirmed', type => 'open', name => _("Open") }); + my $closed = $rs->new({ id => -2, label => 'closed', type => 'closed', name => _("Closed") }); + return ($open, $closed); +} + +# As states will change rarely, and then only through the admin, +# we cache these in the package on first use, and clear on update. + +sub clear { + Memcached::set('states', ''); +} + +sub states { + my $rs = shift; + + my $states = Memcached::get('states'); + if ($states && !FixMyStreet->test_mode) { + # Need to reattach schema + $states->[0]->result_source->schema( $rs->result_source->schema ) if $states->[0]; + return $states; + } + + # Pick up and cache any translations + my $q = $rs->result_source->schema->resultset("Translation")->search({ + tbl => 'state', + col => 'name', + }); + my %trans; + $trans{$_->object_id}{$_->lang} = { id => $_->id, msgstr => $_->msgstr } foreach $q->all; + + my @states = ($rs->_hardcoded_states, $rs->search(undef, { order_by => 'label' })->all); + $_->translated->{name} = $trans{$_->id} || {} foreach @states; + $states = \@states; + Memcached::set('states', $states); + return $states; +} + +# Some functions to provide filters on the above data + +sub open { [ $_[0]->_filter(sub { $_->type eq 'open' }) ] } +sub closed { [ $_[0]->_filter(sub { $_->type eq 'closed' }) ] } +sub fixed { [ $_[0]->_filter(sub { $_->type eq 'fixed' }) ] } + +# We sometimes have only a state label to display, no associated object. +# This function can be used to return that label's display name. + +sub display { + my ($rs, $label, $single_fixed) = @_; + my $unchanging = { + unconfirmed => _("Unconfirmed"), + hidden => _("Hidden"), + partial => _("Partial"), + 'fixed - council' => _("Fixed - Council"), + 'fixed - user' => _("Fixed - User"), + }; + $label = 'fixed' if $single_fixed && $label =~ /^fixed - (council|user)$/; + return $unchanging->{$label} if $unchanging->{$label}; + my ($state) = $rs->_filter(sub { $_->label eq $label }); + return $label unless $state; + return $state->msgstr; +} + +sub _filter { + my ($rs, $fn) = @_; + my $states = $rs->states; + grep &$fn, @$states; +} + +1; diff --git a/perllib/FixMyStreet/DB/Schema.pm b/perllib/FixMyStreet/DB/Schema.pm index 10bbd434f..be39069d8 100644 --- a/perllib/FixMyStreet/DB/Schema.pm +++ b/perllib/FixMyStreet/DB/Schema.pm @@ -25,6 +25,8 @@ __PACKAGE__->connection(FixMyStreet->dbic_connect_info); has lang => ( is => 'rw' ); +has cobrand => ( is => 'rw' ); + has cache => ( is => 'rw', lazy => 1, default => sub { {} } ); 1; diff --git a/perllib/FixMyStreet/Roles/Extra.pm b/perllib/FixMyStreet/Roles/Extra.pm index dc2e5c241..445f6d91c 100644 --- a/perllib/FixMyStreet/Roles/Extra.pm +++ b/perllib/FixMyStreet/Roles/Extra.pm @@ -175,4 +175,20 @@ sub get_extra { return $extra; } +=head2 get_extra_field_value + +Return the value of a field stored in `_fields` in extra, or undefined if +it's not present. + +=cut + +sub get_extra_field_value { + my ($self, $name) = @_; + + my @fields = @{ $self->get_extra_fields() }; + + my ($field) = grep { $_->{name} eq $name } @fields; + return $field->{value}; +} + 1; diff --git a/perllib/FixMyStreet/Script/Questionnaires.pm b/perllib/FixMyStreet/Script/Questionnaires.pm index 3f22eb150..ec6139d2d 100644 --- a/perllib/FixMyStreet/Script/Questionnaires.pm +++ b/perllib/FixMyStreet/Script/Questionnaires.pm @@ -16,6 +16,9 @@ sub send { sub send_questionnaires_period { my ( $period, $params ) = @_; + # Don't send if we don't have a fixed state + return unless FixMyStreet::DB::Result::Problem::fixed_states->{fixed}; + my $rs = FixMyStreet::DB->resultset('Questionnaire'); # Select all problems that need a questionnaire email sending diff --git a/perllib/FixMyStreet/TestAppProve.pm b/perllib/FixMyStreet/TestAppProve.pm index f6e09fbe9..7a387547d 100644 --- a/perllib/FixMyStreet/TestAppProve.pm +++ b/perllib/FixMyStreet/TestAppProve.pm @@ -75,7 +75,7 @@ sub run { $SIG{__WARN__} = sub { print STDERR @_ if $_[0] !~ m/NOTICE: CREATE TABLE/; }; $dbh->do( path('db/schema.sql')->slurp ) or die $!; - $dbh->do( path('db/alert_types.sql')->slurp ) or die $!; + $dbh->do( path('db/fixture.sql')->slurp ) or die $!; $dbh->do( path('db/generate_secret.sql')->slurp ) or die $!; $SIG{__WARN__} = $tmpwarn; diff --git a/t/app/controller/admin_states.t b/t/app/controller/admin_states.t new file mode 100644 index 000000000..60ffe5b88 --- /dev/null +++ b/t/app/controller/admin_states.t @@ -0,0 +1,24 @@ +use FixMyStreet::TestMech; + +my $mech = FixMyStreet::TestMech->new; + +my $user = $mech->create_user_ok('superuser@example.com', name => 'Super User', is_superuser => 1); + +$mech->log_in_ok( $user->email ); + +subtest 'basic states admin' => sub { + $mech->get_ok('/admin/states'); + $mech->submit_form_ok({ button => 'new', with_fields => { label => 'third party', type => 'closed', name => 'Third party referral' } }); + $mech->content_contains('Third party referral'); + $mech->content_contains('Fixed'); + $mech->submit_form_ok({ button => 'delete:fixed' }); + $mech->content_lacks('Fixed'); + $mech->submit_form_ok({ form_number => 2, button => 'new_fixed' }); + $mech->content_contains('Fixed'); + $mech->submit_form_ok({ with_fields => { 'name:third party' => 'Third party incident' } }); + $mech->content_contains('Third party incident'); +}; + +# TODO Language tests + +done_testing; diff --git a/t/app/controller/auth.t b/t/app/controller/auth.t index 388216a1f..cb7d16969 100644 --- a/t/app/controller/auth.t +++ b/t/app/controller/auth.t @@ -5,6 +5,7 @@ my $mech = FixMyStreet::TestMech->new; my $test_email = 'test@example.com'; my $test_email2 = 'test@example.net'; +my $test_email3 = 'newuser@example.org'; my $test_password = 'foobar'; END { @@ -279,6 +280,94 @@ subtest "sign in but have email form autofilled" => sub { is $mech->uri->path, '/my', "redirected to correct page"; }; +$mech->log_out_ok; -# more test: -# TODO: test that email are always lowercased +subtest "sign in with uppercase email" => sub { + $mech->get_ok('/auth'); + my $uc_test_email = uc $test_email; + $mech->submit_form_ok( + { + form_name => 'general_auth', + fields => { + email => $uc_test_email, + password_sign_in => $test_password, + }, + button => 'sign_in', + }, + "sign in with '$uc_test_email' and auto-completed name" + ); + is $mech->uri->path, '/my', "redirected to correct page"; + + $mech->content_contains($test_email); + $mech->content_lacks($uc_test_email); + + my $count = FixMyStreet::App->model('DB::User')->search( { email => $uc_test_email } )->count; + is $count, 0, "uppercase user wasn't created"; +}; + + +FixMyStreet::override_config { + SIGNUPS_DISABLED => 1, +}, sub { + subtest 'signing in with an unknown email address disallowed' => sub { + $mech->log_out_ok; + # create a new account + $mech->clear_emails_ok; + $mech->get_ok('/auth'); + $mech->submit_form_ok( + { + form_name => 'general_auth', + fields => { email => $test_email3, }, + button => 'email_sign_in', + }, + "create a new account" + ); + + ok $mech->email_count_is(0); + + my $count = FixMyStreet::App->model('DB::User')->search( { email => $test_email3 } )->count; + is $count, 0, "no user exists"; + }; + + subtest 'signing in as known email address with new password is allowed' => sub { + my $new_password = "myshinynewpassword"; + + $mech->clear_emails_ok; + $mech->get_ok('/auth'); + $mech->submit_form_ok( + { + form_name => 'general_auth', + fields => { + email => "$test_email", + password_register => $new_password, + r => 'faq', # Just as a test + }, + button => 'email_sign_in', + }, + "email_sign_in with '$test_email'" + ); + + $mech->not_logged_in_ok; + + ok $mech->email_count_is(1); + my $link = $mech->get_link_from_email; + $mech->get_ok($link); + is $mech->uri->path, '/faq', "redirected to the Help page"; + + $mech->log_out_ok; + + $mech->get_ok('/auth'); + $mech->submit_form_ok( + { + form_name => 'general_auth', + fields => { + email => $test_email, + password_sign_in => $new_password, + }, + button => 'sign_in', + }, + "sign in with '$test_email' and new password" + ); + is $mech->uri->path, '/my', "redirected to correct page"; + }; +}; diff --git a/t/app/controller/dashboard.t b/t/app/controller/dashboard.t index 9d424c1ae..b87b58b38 100644 --- a/t/app/controller/dashboard.t +++ b/t/app/controller/dashboard.t @@ -545,22 +545,11 @@ FixMyStreet::override_config { }, { desc => 'limit by state works', - state => 'fixed', + state => 'fixed - council', report_counts => [2,0,0], report_counts_after => [1,0,0], }, { - desc => 'planned counted as action scheduled', - p1 => { - state => 'planned', - conf_dt => DateTime->now(), - category => 'Potholes', - }, - state => 'action scheduled', - report_counts => [3,0,0], - report_counts_after => [1,0,0], - }, - { desc => 'All fixed states count as fixed', p1 => { state => 'fixed - council', @@ -573,7 +562,7 @@ FixMyStreet::override_config { category => 'Potholes', }, state => 'fixed', - report_counts => [5,0,0], + report_counts => [4,0,0], report_counts_after => [3,0,0], }, ) { @@ -612,7 +601,7 @@ FixMyStreet::override_config { while ( my $row = $csv->getline( $data_handle ) ) { push @rows, $row; } - is scalar @rows, 7, '1 (header) + 6 (reports) = 7 lines'; + is scalar @rows, 6, '1 (header) + 5 (reports) = 6 lines'; }; }; restore_time; diff --git a/t/app/controller/report_display.t b/t/app/controller/report_display.t index 093ea9cf8..4d73a5204 100644 --- a/t/app/controller/report_display.t +++ b/t/app/controller/report_display.t @@ -313,7 +313,7 @@ for my $test ( date => DateTime->now, state => 'investigating', banner_id => 'progress', - banner_text => 'progress', + banner_text => 'investigating', fixed => 0 }, { @@ -321,7 +321,7 @@ for my $test ( date => DateTime->now, state => 'action scheduled', banner_id => 'progress', - banner_text => 'progress', + banner_text => 'action scheduled', fixed => 0 }, { @@ -329,7 +329,7 @@ for my $test ( date => DateTime->now, state => 'planned', banner_id => 'progress', - banner_text => 'progress', + banner_text => 'planned', fixed => 0 }, { diff --git a/t/app/controller/report_inspect.t b/t/app/controller/report_inspect.t index fcd7c724d..68f9063cf 100644 --- a/t/app/controller/report_inspect.t +++ b/t/app/controller/report_inspect.t @@ -57,7 +57,7 @@ FixMyStreet::override_config { }; subtest "test basic inspect submission" => sub { - $mech->submit_form_ok({ button => 'save', with_fields => { traffic_information => 'Yes', state => 'Action Scheduled', include_update => undef } }); + $mech->submit_form_ok({ button => 'save', with_fields => { traffic_information => 'Yes', state => 'Action scheduled', include_update => undef } }); $report->discard_changes; my $alert = FixMyStreet::App->model('DB::Alert')->find( { user => $user, alert_type => 'new_updates', confirmed => 1, } @@ -90,9 +90,8 @@ FixMyStreet::override_config { is $report->user->get_extra_metadata('reputation'), $reputation, "User reputation wasn't changed"; $mech->get_ok("/report/$report_id"); my $meta = $mech->extract_update_metas; - like $meta->[0], qr/Updated by .*action scheduled/, 'First update mentions action scheduled'; - like $meta->[1], qr/Posted by .*defect raised/, 'Update mentions defect raised'; - unlike $meta->[2], qr/Posted by .*action scheduled/, 'Update does not mention action scheduled'; + like $meta->[0], qr/State changed to: Action scheduled/, 'First update mentions action scheduled'; + like $meta->[2], qr/Posted by .*defect raised/, 'Update mentions defect raised'; $user->unset_extra_metadata('categories'); $user->update; @@ -300,7 +299,7 @@ FixMyStreet::override_config { subtest "Oxfordshire-specific traffic management options are shown" => sub { $report->update({ state => 'confirmed' }); $mech->get_ok("/report/$report_id"); - $mech->submit_form_ok({ button => 'save', with_fields => { traffic_information => 'Signs and Cones', state => 'Action Scheduled', include_update => undef } }); + $mech->submit_form_ok({ button => 'save', with_fields => { traffic_information => 'Signs and Cones', state => 'Action scheduled', include_update => undef } }); $report->discard_changes; is $report->state, 'action scheduled', 'report state changed'; is $report->get_extra_metadata('traffic_information'), 'Signs and Cones', 'report data changed'; diff --git a/t/app/controller/report_updates.t b/t/app/controller/report_updates.t index 4cb035bac..0526b2fd7 100644 --- a/t/app/controller/report_updates.t +++ b/t/app/controller/report_updates.t @@ -93,8 +93,7 @@ for my $test ( anonymous => 't', mark_fixed => 'true', mark_open => 'false', - meta => -'Posted anonymously at 15:47, Sat 16 April 2011, marked as fixed', + meta => [ 'State changed to: Fixed', 'Posted anonymously at 15:47, Sat 16 April 2011' ] }, { description => 'named user, anon is true, reopened', @@ -102,7 +101,7 @@ for my $test ( anonymous => 't', mark_fixed => 'false', mark_open => 'true', - meta => 'Posted anonymously at 15:47, Sat 16 April 2011, reopened', + meta => [ 'State changed to: Open', 'Posted anonymously at 15:47, Sat 16 April 2011' ] } ) { @@ -118,8 +117,9 @@ for my $test ( $mech->content_contains('This is some update text'); my $meta = $mech->extract_update_metas; - is scalar @$meta, 1, 'number of updates'; - is $meta->[0], $test->{meta}; + my $test_meta = ref $test->{meta} ? $test->{meta} : [ $test->{meta} ]; + is scalar @$meta, scalar @$test_meta, 'number of updates'; + is_deeply $meta, $test_meta; }; } @@ -131,7 +131,7 @@ subtest "updates displayed on report with empty bodies_str" => sub { $mech->get_ok("/report/$report_id"); my $meta = $mech->extract_update_metas; - is scalar @$meta, 1, 'update displayed'; + is scalar @$meta, 2, 'update displayed'; $report->update({ bodies_str => $old_bodies_str }); }; @@ -185,10 +185,11 @@ subtest "several updates shown in correct order" => sub { $mech->get_ok("/report/$report_id"); my $meta = $mech->extract_update_metas; - is scalar @$meta, 3, 'number of updates'; + is scalar @$meta, 4, 'number of updates'; is $meta->[0], 'Posted by Other User at 12:23, Thu 10 March 2011', 'first update'; is $meta->[1], 'Posted by Main User at 12:23, Thu 10 March 2011', 'second update'; - is $meta->[2], 'Posted anonymously at 08:12, Tue 15 March 2011, marked as fixed', 'third update'; + is $meta->[2], 'State changed to: Fixed', 'third update, part 1'; + is $meta->[3], 'Posted anonymously at 08:12, Tue 15 March 2011', 'third update, part 2'; }; for my $test ( @@ -613,7 +614,6 @@ for my $test ( state => 'internal referral', }, state => 'internal referral', - meta => "an internal referral", }, { desc => 'from authority user marks report as not responsible', @@ -635,7 +635,6 @@ for my $test ( state => 'duplicate', }, state => 'duplicate', - meta => 'a duplicate report', }, { desc => 'from authority user marks report as internal referral', @@ -646,7 +645,6 @@ for my $test ( state => 'internal referral', }, state => 'internal referral', - meta => 'an internal referral', }, { desc => 'from authority user marks report sent to two councils as fixed', @@ -711,19 +709,13 @@ for my $test ( my $update_meta = $mech->extract_update_metas; my $meta_state = $test->{meta} || $test->{fields}->{state}; - if ( $test->{reopened} ) { - like $update_meta->[0], qr/reopened/, 'update meta says reopened'; - } elsif ( $test->{state} eq 'duplicate' ) { - like $update_meta->[0], qr/closed as $meta_state/, 'update meta includes state change'; - } else { - like $update_meta->[0], qr/marked as $meta_state/, 'update meta includes state change'; - } + like $update_meta->[0], qr/$meta_state/i, 'update meta includes state change'; if ($test->{view_username}) { - like $update_meta->[0], qr{Westminster City Council \(Test User\)}, 'update meta includes council and user name'; + like $update_meta->[1], qr{Westminster City Council \(Test User\)}, 'update meta includes council and user name'; $user->user_body_permissions->delete_all; } else { - like $update_meta->[0], qr{Westminster City Council}, 'update meta includes council name'; + like $update_meta->[1], qr{Westminster City Council}, 'update meta includes council name'; $mech->content_contains( '<strong>Westminster City Council</strong>', 'council name in bold'); } @@ -751,24 +743,22 @@ subtest 'check meta correct for comments marked confirmed but not marked open' = $mech->get_ok( "/report/" . $report->id ); my $update_meta = $mech->extract_update_metas; - unlike $update_meta->[0], qr/reopened/, + unlike $update_meta->[0], qr/Open/, 'update meta does not say reopened'; $comment->update( { mark_open => 1, problem_state => undef } ); $mech->get_ok( "/report/" . $report->id ); $update_meta = $mech->extract_update_metas; - unlike $update_meta->[0], qr/marked as open/, - 'update meta does not says marked as open'; - like $update_meta->[0], qr/reopened/, 'update meta does say reopened'; + like $update_meta->[0], qr/Open/, 'update meta does say open'; $comment->update( { mark_open => 0, problem_state => undef } ); $mech->get_ok( "/report/" . $report->id ); $update_meta = $mech->extract_update_metas; - unlike $update_meta->[0], qr/marked as open/, + unlike $update_meta->[0], qr/Open/, 'update meta does not says marked as open'; - unlike $update_meta->[0], qr/reopened/, 'update meta does not say reopened'; + unlike $update_meta->[0], qr/Open/, 'update meta does not say reopened'; }; subtest "check first comment with no status change has no status in meta" => sub { @@ -782,7 +772,7 @@ subtest "check first comment with no status change has no status in meta" => sub $mech->get_ok("/report/$report_id"); my $update_meta = $mech->extract_update_metas; - unlike $update_meta->[0], qr/marked as|reopened/, 'update meta does not include state change'; + unlike $update_meta->[0], qr/State changed to/, 'update meta does not include state change'; }; subtest "check comment with no status change has not status in meta" => sub { @@ -820,7 +810,7 @@ subtest "check comment with no status change has not status in meta" => sub { is $report->state, 'fixed - council', 'correct report state'; is $update->problem_state, 'fixed - council', 'correct update state'; my $update_meta = $mech->extract_update_metas; - unlike $update_meta->[1], qr/marked as/, 'update meta does not include state change'; + unlike $update_meta->[1], qr/State changed to/, 'update meta does not include state change'; $user->from_body( $body->id ); $user->update; @@ -854,9 +844,9 @@ subtest "check comment with no status change has not status in meta" => sub { is $report->state, 'investigating', 'correct report state'; is $update->problem_state, 'investigating', 'correct update state'; $update_meta = $mech->extract_update_metas; - like $update_meta->[0], qr/marked as fixed/, 'first update meta says fixed'; - unlike $update_meta->[1], qr/marked as/, 'second update meta does not include state change'; - like $update_meta->[2], qr/marked as investigating/, 'third update meta says investigating'; + like $update_meta->[0], qr/fixed/i, 'first update meta says fixed'; + unlike $update_meta->[2], qr/State changed to/, 'second update meta does not include state change'; + like $update_meta->[3], qr/investigating/i, 'third update meta says investigating'; my $dt = DateTime->now( time_zone => "local" )->add( seconds => 1 ); $comment = FixMyStreet::App->model('DB::Comment')->find_or_create( @@ -883,10 +873,10 @@ subtest "check comment with no status change has not status in meta" => sub { is $report->state, 'investigating', 'correct report state'; is $update->problem_state, undef, 'no update state'; $update_meta = $mech->extract_update_metas; - like $update_meta->[0], qr/marked as fixed/, 'first update meta says fixed'; - unlike $update_meta->[1], qr/marked as/, 'second update meta does not include state change'; - like $update_meta->[2], qr/marked as investigating/, 'third update meta says investigating'; - unlike $update_meta->[3], qr/marked as/, 'fourth update meta has no state change'; + like $update_meta->[0], qr/fixed/i, 'first update meta says fixed'; + unlike $update_meta->[2], qr/State changed to/, 'second update meta does not include state change'; + like $update_meta->[3], qr/investigating/i, 'third update meta says investigating'; + unlike $update_meta->[5], qr/State changed to/, 'fourth update meta has no state change'; }; subtest 'check meta correct for second comment marking as reopened' => sub { @@ -907,7 +897,7 @@ subtest 'check meta correct for second comment marking as reopened' => sub { $mech->get_ok( "/report/" . $report->id ); my $update_meta = $mech->extract_update_metas; - like $update_meta->[0], qr/fixed/, 'update meta says fixed'; + like $update_meta->[0], qr/fixed/i, 'update meta says fixed'; $comment = FixMyStreet::App->model('DB::Comment')->create( { @@ -925,7 +915,7 @@ subtest 'check meta correct for second comment marking as reopened' => sub { $mech->get_ok( "/report/" . $report->id ); $update_meta = $mech->extract_update_metas; - like $update_meta->[1], qr/reopened/, 'update meta says reopened'; + like $update_meta->[2], qr/Open/, 'update meta says reopened'; }; subtest "check first comment with status change but no text is displayed" => sub { @@ -953,9 +943,9 @@ subtest "check first comment with status change but no text is displayed" => sub $mech->get_ok("/report/$report_id"); my $update_meta = $mech->extract_update_metas; - like $update_meta->[0], qr/Updated by/, 'updated by meta if no text'; - unlike $update_meta->[0], qr/Test User/, 'commenter name not included'; - like $update_meta->[0], qr/investigating/, 'update meta includes state change'; + like $update_meta->[1], qr/Updated by/, 'updated by meta if no text'; + unlike $update_meta->[1], qr/Test User/, 'commenter name not included'; + like $update_meta->[0], qr/investigating/i, 'update meta includes state change'; ok $user->user_body_permissions->create({ body => $body, @@ -964,9 +954,9 @@ subtest "check first comment with status change but no text is displayed" => sub $mech->get_ok("/report/$report_id"); $update_meta = $mech->extract_update_metas; - like $update_meta->[0], qr/Updated by/, 'updated by meta if no text'; - like $update_meta->[0], qr/Test User/, 'commenter name included if user has view contribute permission'; - like $update_meta->[0], qr/investigating/, 'update meta includes state change'; + like $update_meta->[1], qr/Updated by/, 'updated by meta if no text'; + like $update_meta->[1], qr/Test User/, 'commenter name included if user has view contribute permission'; + like $update_meta->[0], qr/investigating/i, 'update meta includes state change'; }; @@ -1712,10 +1702,10 @@ for my $test ( desc => 'update unable to fix without marking as fixed leaves state unchanged', initial_state => 'unable to fix', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 0, + reopen => 0, }, end_state => 'unable to fix', }, @@ -1723,10 +1713,10 @@ for my $test ( desc => 'update internal referral without marking as fixed leaves state unchanged', initial_state => 'internal referral', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 0, + reopen => 0, }, end_state => 'internal referral', }, @@ -1734,10 +1724,10 @@ for my $test ( desc => 'update not responsible without marking as fixed leaves state unchanged', initial_state => 'not responsible', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 0, + reopen => 0, }, end_state => 'not responsible', }, @@ -1745,10 +1735,10 @@ for my $test ( desc => 'update duplicate without marking as fixed leaves state unchanged', initial_state => 'duplicate', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 0, + reopen => 0, }, end_state => 'duplicate', }, @@ -1808,48 +1798,48 @@ for my $test ( end_state => 'confirmed', }, { - desc => 'can mark unable to fix as fixed, cannot mark not closed', + desc => 'cannot mark unable to fix as fixed, can reopen', initial_state => 'unable to fix', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 1, + reopen => 1, }, - end_state => 'fixed - user', + end_state => 'confirmed', }, { - desc => 'can mark internal referral as fixed, cannot mark not closed', + desc => 'cannot mark internal referral as fixed, can reopen', initial_state => 'internal referral', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 1, + reopen => 1, }, - end_state => 'fixed - user', + end_state => 'confirmed', }, { - desc => 'can mark not responsible as fixed, cannot mark not closed', + desc => 'cannot mark not responsible as fixed, can reopen', initial_state => 'not responsible', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 1, + reopen => 1, }, - end_state => 'fixed - user', + end_state => 'confirmed', }, { - desc => 'can mark duplicate as fixed, cannot mark not closed', + desc => 'cannot mark duplicate as fixed, can reopen', initial_state => 'duplicate', expected_form_fields => { - fixed => undef, + reopen => undef, }, submitted_form_fields => { - fixed => 1, + reopen => 1, }, - end_state => 'fixed - user', + end_state => 'confirmed', }, ) { subtest $test->{desc} => sub { diff --git a/t/app/controller/root.t b/t/app/controller/root.t new file mode 100644 index 000000000..413341d89 --- /dev/null +++ b/t/app/controller/root.t @@ -0,0 +1,76 @@ +use FixMyStreet::TestMech; + +ok( my $mech = FixMyStreet::TestMech->new, 'Created mech object' ); + +my @urls = ( + "/", + "/reports", + "/about/faq", + "/around?longitude=-1.351488&latitude=51.847235" +); + + +FixMyStreet::override_config { + LOGIN_REQUIRED => 0, + MAPIT_URL => 'http://mapit.uk/' +}, sub { + subtest 'LOGIN_REQUIRED = 0 behaves correctly' => sub { + foreach my $url (@urls) { + $mech->get_ok($url); + is $mech->res->code, 200, "got 200 for page"; + is $mech->res->previous, undef, 'No redirect'; + } + }; +}; + + +FixMyStreet::override_config { + LOGIN_REQUIRED => 1, + MAPIT_URL => 'http://mapit.uk/' +}, sub { + subtest 'LOGIN_REQUIRED = 1 redirects to /auth if not logged in' => sub { + foreach my $url (@urls) { + $mech->get_ok($url); + is $mech->res->code, 200, "got 200 for final destination"; + is $mech->res->previous->code, 302, "got 302 for redirect"; + is $mech->uri->path, '/auth'; + } + }; + + subtest 'LOGIN_REQUIRED = 1 does not redirect if logged in' => sub { + $mech->log_in_ok('user@example.org'); + foreach my $url (@urls) { + $mech->get_ok($url); + is $mech->res->code, 200, "got 200 for final destination"; + is $mech->res->previous, undef, 'No redirect'; + } + $mech->log_out_ok; + }; + + subtest 'LOGIN_REQUIRED = 1 allows whitelisted URLs' => sub { + my @whitelist = ( + '/auth', + '/js/translation_strings.en-gb.js' + ); + + foreach my $url (@whitelist) { + $mech->get_ok($url); + is $mech->res->code, 200, "got 200 for final destination"; + is $mech->res->previous, undef, 'No redirect'; + } + }; + + subtest 'LOGIN_REQUIRED = 1 404s blacklisted URLs' => sub { + my @blacklist = ( + '/offline/appcache', + ); + + foreach my $url (@blacklist) { + $mech->get($url); + ok !$mech->res->is_success(), "want a bad response"; + is $mech->res->code, 404, "got 404"; + } + }; +}; + +done_testing(); diff --git a/t/app/model/extra.t b/t/app/model/extra.t index 17f34c6c1..a5e3e3574 100644 --- a/t/app/model/extra.t +++ b/t/app/model/extra.t @@ -98,4 +98,46 @@ subtest 'Default hash layout' => sub { }; }; +subtest 'Get named field values' => sub { + my $user = $db->resultset('User')->create({ + email => 'test-moderation@example.com', + name => 'Test User' + }); + my $report = $db->resultset('Problem')->create( + { + postcode => 'BR1 3SB', + bodies_str => "", + areas => "", + category => 'Other', + title => 'Good bad good', + detail => 'Good bad bad bad good bad', + used_map => 't', + name => 'Test User 2', + anonymous => 'f', + state => 'confirmed', + lang => 'en-gb', + service => '', + cobrand => 'default', + latitude => '51.4129', + longitude => '0.007831', + user_id => $user->id, + }); + + $report->push_extra_fields( + { + name => "field1", + description => "This is a test field", + value => "value 1", + }, + { + name => "field 2", + description => "Another test", + value => "this is a test value", + } + ); + + is $report->get_extra_field_value("field1"), "value 1", "field1 has correct value"; + is $report->get_extra_field_value("field 2"), "this is a test value", "field 2 has correct value"; +}; + done_testing(); diff --git a/t/app/model/state.t b/t/app/model/state.t new file mode 100644 index 000000000..1653e36e2 --- /dev/null +++ b/t/app/model/state.t @@ -0,0 +1,62 @@ +use FixMyStreet::Test; +use Test::More; + +my $rs = FixMyStreet::DB->resultset('State'); +my $trans_rs = FixMyStreet::DB->resultset('Translation'); + +for ( + { label => 'in progress', lang => 'de' }, + { label => 'investigating', lang => 'fr' }, + { label => 'duplicate', lang => 'de' }, +) { + my $lang = $_->{lang}; + my $obj = $rs->find({ label => $_->{label} }); + $trans_rs->create({ tbl => 'state', col => 'name', object_id => $obj->id, + lang => $lang, msgstr => "$lang $_->{label}" }); +} + +my $states = $rs->states; +my %states = map { $_->label => $_ } @$states; + +subtest 'Open/closed database data is as expected' => sub { + my $open = $rs->open; + is @$open, 5; + my $closed = $rs->closed; + is @$closed, 5; +}; + +is $rs->display('investigating'), 'Investigating'; +is $rs->display('bad'), 'bad'; +is $rs->display('confirmed'), 'Open'; +is $rs->display('closed'), 'Closed'; +is $rs->display('fixed - council'), 'Fixed - Council'; +is $rs->display('fixed - user'), 'Fixed - User'; +is $rs->display('fixed'), 'Fixed'; + +subtest 'default name is untranslated' => sub { + is $states{'in progress'}->name, 'In progress'; + is $states{'in progress'}->msgstr, 'In progress'; + is $states{'action scheduled'}->name, 'Action scheduled'; + is $states{'action scheduled'}->msgstr, 'Action scheduled'; +}; + +subtest 'msgstr gets translated if available when the language changes' => sub { + FixMyStreet::DB->schema->lang('de'); + is $states{'in progress'}->name, 'In progress'; + is $states{'in progress'}->msgstr, 'de in progress'; + is $states{'investigating'}->name, 'Investigating'; + is $states{'investigating'}->msgstr, 'Investigating'; + is $states{'unable to fix'}->name, 'No further action'; + is $states{'unable to fix'}->msgstr, 'No further action'; +}; + +$rs->clear; + +is_deeply [ sort FixMyStreet::DB::Result::Problem->open_states ], + ['action scheduled', 'confirmed', 'in progress', 'investigating', 'planned'], 'open states okay'; +is_deeply [ sort FixMyStreet::DB::Result::Problem->closed_states ], + ['closed', 'duplicate', 'internal referral', 'not responsible', 'unable to fix'], 'closed states okay'; +is_deeply [ sort FixMyStreet::DB::Result::Problem->fixed_states ], + ['fixed', 'fixed - council', 'fixed - user'], 'fixed states okay'; + +done_testing(); diff --git a/t/app/sendreport/inspection_required.t b/t/app/sendreport/inspection_required.t index 73bdd14f7..c8cb30592 100644 --- a/t/app/sendreport/inspection_required.t +++ b/t/app/sendreport/inspection_required.t @@ -5,6 +5,9 @@ use FixMyStreet::SendReport::Email; ok( my $mech = FixMyStreet::TestMech->new, 'Created mech object' ); +use_ok 'FixMyStreet::Cobrand'; +FixMyStreet::DB->schema->cobrand(FixMyStreet::Cobrand::FixMyStreet->new()); + my $user = $mech->create_user_ok( 'user@example.com' ); my $body = $mech->create_body_ok( 2237, 'Oxfordshire County Council'); diff --git a/t/cobrand/bromley.t b/t/cobrand/bromley.t index 46c2472cd..f3053c29a 100644 --- a/t/cobrand/bromley.t +++ b/t/cobrand/bromley.t @@ -41,9 +41,9 @@ for my $update ('in progress', 'unable to fix') { # Test Bromley special casing of 'unable to fix' $mech->get_ok( '/report/' . $report->id ); $mech->content_contains( 'marks it as in progress' ); -$mech->content_contains( 'marked as in progress' ); +$mech->content_contains( 'State changed to: In progress' ); $mech->content_contains( 'marks it as unable to fix' ); -$mech->content_contains( 'marked as no further action' ); +$mech->content_contains( 'State changed to: No further action' ); subtest 'testing special Open311 behaviour', sub { $report->set_extra_fields(); diff --git a/templates/web/base/admin/problem_row.html b/templates/web/base/admin/problem_row.html index 79461367e..446e94d66 100644 --- a/templates/web/base/admin/problem_row.html +++ b/templates/web/base/admin/problem_row.html @@ -34,8 +34,8 @@ [% loc('Created') %]: [% PROCESS format_time time=problem.created %] <br>[% loc('When sent') %]: [% PROCESS format_time time=problem.whensent %] [%- IF problem.is_visible %]<br>[% loc('Confirmed:' ) %] [% PROCESS format_time time=problem.confirmed %][% END -%] - [%- IF problem.is_fixed %]<br>[% loc('Fixed:') %] [% PROCESS format_time time=problem.lastupdate %][% END -%] - [%- IF problem.is_closed %]<br>[% loc('Closed:') %] [% PROCESS format_time time=problem.lastupdate %][% END -%] + [%- IF problem.is_fixed %]<br>[% prettify_state('fixed') %]: [% PROCESS format_time time=problem.lastupdate %][% END -%] + [%- IF problem.is_closed %]<br>[% prettify_state('closed') %]: [% PROCESS format_time time=problem.lastupdate %][% END -%] [%- IF problem.is_open %]<br>[% loc('Last update:') %] [% PROCESS format_time time=problem.lastupdate %][% END -%] </small></td> <td><a href="[% c.uri_for( 'report_edit', problem.id ) %]">[% loc('Edit') %]</a></td> diff --git a/templates/web/base/admin/report_blocks.html b/templates/web/base/admin/report_blocks.html index 933521b94..f5896b88f 100644 --- a/templates/web/base/admin/report_blocks.html +++ b/templates/web/base/admin/report_blocks.html @@ -1,13 +1,7 @@ [% SET report_blocks_included = 1; - -SET state_groups = [ - [ loc('Open'), [ 'confirmed', 'investigating', 'planned', 'in progress', 'action scheduled' ] ], - [ loc('Fixed'), [ 'fixed', 'fixed - user', 'fixed - council' ] ], - [ loc('Closed'), [ 'unable to fix', 'not responsible', 'duplicate', 'closed', 'internal referral' ] ], - [ loc('Hidden'), [ 'hidden', 'partial', 'unconfirmed' ] ] -]; +SET state_groups = c.cobrand.state_groups_admin; %] diff --git a/templates/web/base/admin/states/index.html b/templates/web/base/admin/states/index.html new file mode 100644 index 000000000..bd87f5013 --- /dev/null +++ b/templates/web/base/admin/states/index.html @@ -0,0 +1,118 @@ +[% INCLUDE 'admin/header.html' title=loc('States') ~%] + +[% SET rows = languages.size + 1 IF languages.size > 1 ~%] + +<form method="post" accept-charset="utf-8"> + +<table> + <tr> + <th>Label</th> + <th>Type</th> + <th colspan="2">Name</th> + <th> </th> + </tr> + [% FOREACH state IN open_states.merge(closed_states).merge(fixed_states) %] + <tr> + <td rowspan="[% rows %]"> + [% IF state.label == 'confirmed' %] + open + [% ELSE %] + [% state.label %] + [% END %] + </td> + <td rowspan="[% rows %]"> + [% IF state.label == 'confirmed' %] + [% loc('Open') %]<input type="hidden" name="type:confirmed" value="open"> + [% ELSIF state.label == 'closed' %] + [% loc('Closed') %]<input type="hidden" name="type:closed" value="closed"> + [% ELSIF state.label == 'fixed' %] + [% loc('Fixed') %]<input type="hidden" name="type:fixed" value="fixed"> + [% ELSE %] + <select name="type:[% state.label %]"> + <option value="open"[% ' selected' IF state.type == 'open' %]>[% loc('Open') %]</option> + <option value="closed"[% ' selected' IF state.type == 'closed' %]>[% loc('Closed') %]</option> + </select> + [% END %] + </td> + <td colspan="2"> + [% IF state.label != 'confirmed' AND state.label != 'closed' %] + <input type="text" name="name:[% state.label %]" value="[% state.name %]"> + [% ELSIF languages.size == 1 %] + <input type="text" name="name:[% state.label %]" value="[% state.msgstr %]"> + [% ELSE %] + [% state.name %] + [% END %] + </td> + <td style="text-align:center;vertical-align:middle" rowspan="[% rows %]"> + [% IF state.label != 'confirmed' AND state.label != 'closed' %] + <input class="btn btn--small" type="submit" name="delete:[% state.label %]" value="Delete"> + [% END %] + </td> + </tr> + [% IF languages.size > 1 %] + [% FOREACH language IN languages.keys.sort %] + <tr> + <td style="vertical-align:middle; text-align:right;"> + <label style="margin:0" for="translation:[% state.label %]:[% language %]"> + [% languages.$language.name %] ([% language %]) + </label> + </td> + <td> + <input type="hidden" name="translation_id:[% state.label %]:[% language %]" + value="[% state.translated.name.$language.id %]"> + <input type="text" name="translation:[% state.label %]:[% language %]" + id="translation:[% state.label %]:[% language %]" value="[% state.translated.name.$language.msgstr %]"> + </td> + </tr> + [% END %] + [% END %] + </td> + </tr> + [% END %] +</table> + + <p> + <input type="hidden" name="token" value="[% csrf_token %]"> + <input type="submit" class="btn" value="[% loc('Update') %]"> + </p> + +</form> + +[% IF fixed_states.size == 0 %] +<form method="post" accept-charset="utf-8"> + <p> + <input type="hidden" name="token" value="[% csrf_token %]"> + <input type="submit" class="btn" name="new_fixed" value="[% loc('Add fixed state') %]"> + </p> +</form> + +[% END %] + +<h2>[% loc('New state') %]</h2> + +<form method="post" accept-charset="utf-8"> + <p> + <label for="label">[% loc('Label') %] <small>[% loc('(a-z and space only)') %]</small></label> + <input type="text" class="form-control" name="label" id="label" value="" size="20" pattern="[a-z ]+"> + </p> + + <p> + <label for="type">[% loc('Label') %]</label> + <select name="type" id="type"> + <option value="open">[% loc('Open') %]</option> + <option value="closed">[% loc('Closed') %]</option> + </select> + </p> + + <p> + <label for="name">[% loc('Name') %]</label> + <input type="text" class="form-control" name="name" id="name" value="" size="20"> + </p> + + <p> + <input type="hidden" name="token" value="[% csrf_token %]"> + <input type="submit" class="btn" name="new" value="[% loc('Create') %]"> + </p> +</form> + +[% INCLUDE 'admin/footer.html' %] diff --git a/templates/web/base/auth/token.html b/templates/web/base/auth/token.html index a4dedcec3..9a79a5e67 100644 --- a/templates/web/base/auth/token.html +++ b/templates/web/base/auth/token.html @@ -14,8 +14,14 @@ <div class="confirmation-header confirmation-header--inbox"> - <h1>[% loc("Nearly done! Now check your email…") %]</h1> - <p>[% loc("Click the link in our confirmation email to sign in.") %]</p> + [% IF c.config.SIGNUPS_DISABLED %] + <h1>[% loc("Nearly done!") %]</h1> + <p>[% loc("If there's a user associated with the address you entered, we've sent a confirmation email.") %]</p> + <p>[% loc("Click the link in that email to sign in.") %]</p> + [% ELSE %] + <h1>[% loc("Nearly done! Now check your email…") %]</h1> + <p>[% loc("Click the link in our confirmation email to sign in.") %]</p> + [% END %] <p> [% loc("Can’t find our email? Check your spam folder – that’s the solution 99% of the time.") %] diff --git a/templates/web/base/dashboard/index.html b/templates/web/base/dashboard/index.html index 6033ef36b..e47798573 100644 --- a/templates/web/base/dashboard/index.html +++ b/templates/web/base/dashboard/index.html @@ -136,11 +136,12 @@ </select> <p>[% loc('Report state:') %] <select class="form-control" name="state"> <option value=''>[% loc('All') %]</option> - [% FOREACH state IN [ ['confirmed', loc('Open')], ['investigating', - loc('Investigating')], ['action scheduled', loc('Planned')], ['in progress', - loc('In Progress')], ['closed', loc('Closed')], ['fixed', loc('Fixed')] ] %] - <option [% 'selected ' IF state.0 == q_state %] value="[% state.0 %]">[% state.1 %]</option> - [% END %] + [% FOR group IN filter_states %] + [% FOR state IN group.1 %] + [% NEXT IF state == 'hidden' %] + <option [% 'selected ' IF state == q_state %] value="[% state %]">[% prettify_state(state, 1) %]</option> + [% END %] + [% END %] </select> <input type="submit" class="btn" value="[% loc('Look up') %]"> <a class="export_as_csv" href="[% c.req.uri_with({ export => 1 }) %]">[% loc('Export as CSV') %]</a> diff --git a/templates/web/base/front/stats.html b/templates/web/base/front/stats.html index eb671137b..41358c869 100644 --- a/templates/web/base/front/stats.html +++ b/templates/web/base/front/stats.html @@ -35,7 +35,9 @@ <div id="front_stats"> <div>[% tprintf( new_text, decode(new_n) ) %]</div> + [% IF has_fixed_state %] <div>[% tprintf( fixed_text, decode(fixed_n) ) %]</div> + [% END %] [% IF c.cobrand.moniker != 'zurich' %] <div>[% tprintf( updates_text, decode(updates_n) ) %]</div> [% END %] diff --git a/templates/web/base/maps/openlayers.html b/templates/web/base/maps/openlayers.html index e8d6c2e06..a9758f738 100644 --- a/templates/web/base/maps/openlayers.html +++ b/templates/web/base/maps/openlayers.html @@ -6,7 +6,9 @@ <input type="hidden" name="zoom" value="[% map.zoom %]"> <div id="js-map-data" +[%- UNLESS c.cobrand.call_hook('hide_areas_on_reports') %] data-area="[% map.area.join(',') %]" +[%- END %] data-all_pins='[% all_pins %]' data-latitude=[% map.latitude %] data-longitude=[% map.longitude %] @@ -23,7 +25,7 @@ data-map_type="[% map.map_type %]" [% IF include_key -%] data-key='[% c.config.BING_MAPS_API_KEY %]' -[%- END %] +[%- END -%] > </div> <div id="map_box" aria-hidden="true"> diff --git a/templates/web/base/report/_inspect.html b/templates/web/base/report/_inspect.html index fb58a0cfa..59806dc66 100644 --- a/templates/web/base/report/_inspect.html +++ b/templates/web/base/report/_inspect.html @@ -28,8 +28,8 @@ <span id="problem_northing">[% local_coords.1 %]</span> [% ELSE %] <strong>[% loc('Latitude/Longitude:') %]</strong> - <span id="problem_latitude">[% problem.latitude %]</span> - <span id="problem_longitude">[% problem.longitude %]</span>, + <span id="problem_latitude">[% problem.latitude %]</span>, + <span id="problem_longitude">[% problem.longitude %]</span> [% END %] <input type="hidden" name="longitude" value="[% problem.longitude %]"> <input type="hidden" name="latitude" value="[% problem.latitude %]"> diff --git a/templates/web/base/report/_item.html b/templates/web/base/report/_item.html index 3f5c5464b..9449ca55d 100644 --- a/templates/web/base/report/_item.html +++ b/templates/web/base/report/_item.html @@ -40,9 +40,9 @@ [% END %] <small> [% IF NOT no_fixed AND problem.is_fixed %] - <span class="item-list__item__state">[% loc('Fixed') %]</span> + <span class="item-list__item__state">[% prettify_state('fixed') %]</span> [% ELSIF NOT no_fixed AND problem.is_closed %] - <span class="item-list__item__state">[% loc('Closed') %]</span> + <span class="item-list__item__state">[% prettify_state('closed') %]</span> [% ELSIF (c.user.has_permission_to('report_edit_priority', problem.bodies_str_ids) OR c.user.has_permission_to('report_inspect', problem.bodies_str_ids)) AND problem.response_priority %] <span class="item-list__item__state">[% problem.response_priority.name %]</span> [% END %] diff --git a/templates/web/base/report/banner.html b/templates/web/base/report/banner.html index c80d129eb..ee73d287a 100644 --- a/templates/web/base/report/banner.html +++ b/templates/web/base/report/banner.html @@ -9,11 +9,11 @@ [% INCLUDE banner, id = 'unknown', text = loc('Unknown') %] [% END %] [% IF problem.is_fixed %] - [% INCLUDE banner, id = 'fixed', text = loc('Fixed') %] + [% INCLUDE banner, id = 'fixed', text = prettify_state('fixed') %] [% END %] [% IF problem.is_closed %] - [% INCLUDE banner, id = 'closed', text = loc('Closed') %] + [% INCLUDE banner, id = 'closed', text = prettify_state('closed') %] [% END %] [% IF problem.is_in_progress %] - [% INCLUDE banner, id = 'progress', text = loc('In progress') %] + [% INCLUDE banner, id = 'progress', text = prettify_state(problem.state) %] [% END %] diff --git a/templates/web/base/report/inspect/state_groups_select.html b/templates/web/base/report/inspect/state_groups_select.html index 2cf1a4de5..998b99739 100644 --- a/templates/web/base/report/inspect/state_groups_select.html +++ b/templates/web/base/report/inspect/state_groups_select.html @@ -1,11 +1,6 @@ [% -SET state_groups = [ - [ loc('Open'), [ 'confirmed', 'investigating', 'in progress', 'action scheduled' ] ], - [ loc('Fixed'), [ 'fixed - council' ] ], - [ loc('Closed'), [ 'unable to fix', 'not responsible', 'duplicate', 'internal referral' ] ], - [ loc('Hidden'), [ 'hidden' ] ] -]; +SET state_groups = c.cobrand.state_groups_inspect; %] [% DEFAULT current_state = problem.state %] diff --git a/templates/web/base/report/state-list.html b/templates/web/base/report/state-list.html deleted file mode 100644 index e137c81e2..000000000 --- a/templates/web/base/report/state-list.html +++ /dev/null @@ -1,21 +0,0 @@ -[% -SET state_pretty = { - 'confirmed' = loc('Open') - 'investigating' = loc('Investigating') - 'planned' = loc('Planned') - 'in progress' = loc('In progress') - 'action scheduled' = loc('Action Scheduled') - 'fixed' = loc('Fixed') - 'fixed - user' = loc('Fixed - User') - 'fixed - council' = loc('Fixed - Council') - 'unable to fix' = loc('No further action') - 'not responsible' = loc('Not Responsible') - 'duplicate' = loc('Duplicate') - 'closed' = loc('Closed') - 'internal referral' = loc('Internal referral') - 'hidden' = loc('Hidden') - 'partial' = loc('Partial') - 'unconfirmed' = loc('Unconfirmed') -}; -state_pretty.$state -~%] diff --git a/templates/web/base/report/update.html b/templates/web/base/report/update.html index 1f1438bfc..85624669a 100644 --- a/templates/web/base/report/update.html +++ b/templates/web/base/report/update.html @@ -43,6 +43,12 @@ </div> [% END %] + [% SET update_state = update.problem_state_display(c) %] + [% IF update_state AND update_state != global.last_state AND NOT (global.last_state == "" AND update.problem_state == 'confirmed') %] + <p class="meta-2">[% loc('State changed to:') %] [% update_state %]</p> + [%- global.last_state = update_state %] + [% END %] + <p class="meta-2"> [% INCLUDE meta_line %] [% IF c.user_exists AND c.user.id == update.user_id AND !update.anonymous %] diff --git a/templates/web/base/report/update/form_update.html b/templates/web/base/report/update/form_update.html index 34abf53c5..5a1b3b602 100644 --- a/templates/web/base/report/update/form_update.html +++ b/templates/web/base/report/update/form_update.html @@ -39,7 +39,7 @@ <label for="state">[% loc( 'State' ) %]</label> [% INCLUDE 'report/inspect/state_groups_select.html' %] [% ELSE %] - [% IF (problem.is_fixed OR problem.state == 'closed') AND ((c.user_exists AND c.user.id == problem.user_id) OR alert_to_reporter) %] + [% IF (problem.is_fixed OR problem.is_closed) AND ((c.user_exists AND c.user.id == problem.user_id) OR alert_to_reporter) %] <input type="checkbox" name="reopen" id="form_reopen" value="1"[% ' checked' IF (update.mark_open || c.req.params.reopen) %]> [% IF problem.is_closed %] @@ -48,7 +48,7 @@ <label class="inline" for="form_reopen">[% loc('This problem has not been fixed') %]</label> [% END %] - [% ELSIF !problem.is_fixed %] + [% ELSIF !problem.is_fixed AND has_fixed_state %] <div class="checkbox-group"> <input type="checkbox" name="fixed" id="form_fixed" value="1"[% ' checked' IF update.mark_fixed %]> diff --git a/templates/web/base/reports/_list-filters.html b/templates/web/base/reports/_list-filters.html index ef7c7ad78..50e88857d 100644 --- a/templates/web/base/reports/_list-filters.html +++ b/templates/web/base/reports/_list-filters.html @@ -1,6 +1,6 @@ [% select_status = BLOCK %] <select class="form-control js-multiple" name="status" id="statuses" multiple - data-all="[% loc('All reports') %]" data-all-options='["open","closed","fixed"]' + data-all="[% loc('All') %]" data-all-options='["open","closed","fixed"]' [%~ IF c.cobrand.on_map_default_status == 'open' %] data-none="[% loc('Unfixed reports') %]" [%~ END ~%] @@ -10,19 +10,18 @@ <option value="unshortlisted"[% ' selected' IF filter_status.unshortlisted %]>[% loc('Unshortlisted') %]</option> [% END %] [% IF c.user_exists AND c.user.is_superuser OR c.user.belongs_to_body(body.id) %] - <option value="confirmed"[% ' selected' IF filter_status.confirmed %]>[% loc('Open') %]</option> - <option value="investigating"[% ' selected' IF filter_status.investigating %]>[% loc('Investigating') %]</option> - <option value="in progress"[% ' selected' IF filter_status.in_progress %]>[% loc('In progress') %]</option> - <option value="action scheduled"[% ' selected' IF filter_status.action_scheduled %]>[% loc('Action scheduled') %]</option> - <option value="fixed"[% ' selected' IF filter_status.fixed %]>[% loc('Fixed reports') %]</option> - <option value="unable to fix"[% ' selected' IF filter_status.unable_to_fix %]>[% loc('No further action') %]</option> - <option value="not responsible"[% ' selected' IF filter_status.not_responsible %]>[% loc('Not responsible') %]</option> - <option value="internal referral"[% ' selected' IF filter_status.internal_referral %]>[% loc('Internal referral') %]</option> - <option value="duplicate"[% ' selected' IF filter_status.duplicate %]>[% loc('Duplicate') %]</option> + [% FOR group IN filter_states %] + [% FOR state IN group.1 %] + [% NEXT IF state == 'hidden' %] + <option value="[% state %]"[% ' selected' IF filter_status.$state %]>[% prettify_state(state, 1) %]</option> + [% END %] + [% END %] [% ELSE %] - <option value="open"[% ' selected' IF filter_status.open %]>[% loc('Unfixed reports') %]</option> - <option value="closed"[% ' selected' IF filter_status.closed %]>[% loc('Closed reports') %]</option> - <option value="fixed"[% ' selected' IF filter_status.fixed %]>[% loc('Fixed reports') %]</option> + <option value="open"[% ' selected' IF filter_status.open %]>[% prettify_state('confirmed') %]</option> + <option value="closed"[% ' selected' IF filter_status.closed %]>[% prettify_state('closed') %]</option> + [% IF has_fixed_state %] + <option value="fixed"[% ' selected' IF filter_status.fixed %]>[% prettify_state('fixed') %]</option> + [% END %] [% END %] </select> [% END %] @@ -46,7 +45,7 @@ [% END %] <p class="report-list-filters"> - [% tprintf(loc('<label for="statuses">Show</label> %s <label for="filter_categories">about</label> %s', "The first %s is a dropdown of all/fixed/etc, the second is a dropdown of categories"), select_status, select_category) %] + [% tprintf(loc('<label for="statuses">Show</label> %s reports <label for="filter_categories">about</label> %s', "The first %s is a dropdown of all/fixed/etc, the second is a dropdown of categories"), select_status, select_category) %] <input type="submit" name="filter_update" value="[% loc('Go') %]"> </p> diff --git a/templates/web/oxfordshire/report/inspect/state_groups_select.html b/templates/web/oxfordshire/report/inspect/state_groups_select.html deleted file mode 100644 index d36fb0191..000000000 --- a/templates/web/oxfordshire/report/inspect/state_groups_select.html +++ /dev/null @@ -1,12 +0,0 @@ -[% - -SET state_groups = [ - [ loc('New'), [ 'confirmed', 'investigating' ] ], - [ loc('Scheduled'), [ 'action scheduled' ] ], - [ loc('Fixed'), [ 'fixed - council' ] ], - [ loc('Closed'), [ 'not responsible', 'duplicate', 'unable to fix' ] ] -]; - -%] -[% DEFAULT current_state = problem.state %] -[% INCLUDE 'report/_state_select_field.html' single_fixed=1 %] |