diff options
24 files changed, 877 insertions, 31 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 520fd3963..95886d478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,12 @@ - New features: - Body and category names can now be translated in the admin. #1244 - Body users can now create reports as an anonymous user. #1796 + - Extra fields can be added to report form site-wide. #1743 - Front end improvements: - Always show pagination figures even if only one page. - Admin improvements: - Highlight current shortlisted user in list tooltip. + - Extra fields on contacts can be edited. #1743 - Bugfixes: - Set up action scheduled field when report loaded. #1789 - Stop errors from JS validator due to form in form. diff --git a/bin/update-schema b/bin/update-schema index 82632faa6..32c00ff5e 100755 --- a/bin/update-schema +++ b/bin/update-schema @@ -195,6 +195,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 '0053' if table_exists('report_extra_fields'); return '0052' if table_exists('translation'); return '0051' if column_exists('contacts', 'state'); return '0050' if table_exists('defect_types'); diff --git a/db/downgrade_0053---0052.sql b/db/downgrade_0053---0052.sql new file mode 100644 index 000000000..fdad6d0b8 --- /dev/null +++ b/db/downgrade_0053---0052.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE report_extra_fields; + +COMMIT; diff --git a/db/schema.sql b/db/schema.sql index af6570b7a..ed930a13e 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -538,3 +538,11 @@ CREATE TABLE translation ( msgstr text not null, unique(tbl, object_id, col, lang) ); + +CREATE TABLE report_extra_fields ( + id serial not null primary key, + name text not null, + cobrand text, + language text, + extra text +); diff --git a/db/schema_0053-add-report-extra-fields-table.sql b/db/schema_0053-add-report-extra-fields-table.sql new file mode 100644 index 000000000..be92abd47 --- /dev/null +++ b/db/schema_0053-add-report-extra-fields-table.sql @@ -0,0 +1,11 @@ +BEGIN; + +CREATE TABLE report_extra_fields ( + id serial not null primary key, + name text not null, + cobrand text, + language text, + extra text +); + +COMMIT; diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index aab114576..cd1246134 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -375,6 +375,8 @@ sub update_contacts : Private { $contact->set_extra_metadata( reputation_threshold => int($c->get_param('reputation_threshold')) ); } + $c->forward('update_extra_fields', [ $contact ]); + if ( %errors ) { $c->stash->{updated} = _('Please correct the errors below'); $c->stash->{contact} = $contact; @@ -1973,6 +1975,46 @@ sub fetch_body_areas : Private { $c->stash->{fetched_areas_body_id} = $body->id; } +sub update_extra_fields : Private { + my ($self, $c, $object) = @_; + + my @indices = grep { /^metadata\[\d+\]\.code/ } keys %{ $c->req->params }; + @indices = sort map { /(\d+)/ } @indices; + + my @extra_fields; + foreach my $i (@indices) { + my $meta = {}; + $meta->{code} = $c->get_param("metadata[$i].code"); + next unless $meta->{code}; + $meta->{order} = int $c->get_param("metadata[$i].order"); + $meta->{datatype} = $c->get_param("metadata[$i].datatype"); + my $required = $c->get_param("metadata[$i].required") && $c->get_param("metadata[$i].required") eq 'on'; + $meta->{required} = $required ? 'true' : 'false'; + my $notice = $c->get_param("metadata[$i].notice") && $c->get_param("metadata[$i].notice") eq 'on'; + $meta->{variable} = $notice ? 'false' : 'true'; + $meta->{description} = $c->get_param("metadata[$i].description"); + $meta->{datatype_description} = $c->get_param("metadata[$i].datatype_description"); + + if ( $meta->{datatype} eq "singlevaluelist" ) { + $meta->{values} = []; + my $re = qr{^metadata\[$i\]\.values\[\d+\]\.key}; + my @vindices = grep { /$re/ } keys %{ $c->req->params }; + @vindices = sort map { /values\[(\d+)\]/ } @vindices; + foreach my $j (@vindices) { + my $name = $c->get_param("metadata[$i].values[$j].name"); + my $key = $c->get_param("metadata[$i].values[$j].key"); + push(@{$meta->{values}}, { + name => $name, + key => $key, + }) if $name; + } + } + push @extra_fields, $meta; + } + @extra_fields = sort { $a->{order} <=> $b->{order} } @extra_fields; + $object->set_extra_fields(@extra_fields); +} + sub trim { my $self = shift; my $e = shift; diff --git a/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm b/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm new file mode 100644 index 000000000..d5ec64698 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm @@ -0,0 +1,61 @@ +package FixMyStreet::App::Controller::Admin::ReportExtraFields; +use Moose; +use namespace::autoclean; +use List::MoreUtils qw(uniq); + +BEGIN { extends 'Catalyst::Controller'; } + + +sub begin : Private { + my ( $self, $c ) = @_; + + $c->forward('/admin/begin'); +} + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + + my @extras = $c->model('DB::ReportExtraFields')->search( + undef, + { + order_by => 'name' + } + ); + + $c->stash->{extra_fields} = \@extras; +} + +sub edit : Path : Args(1) { + my ( $self, $c, $extra_id ) = @_; + + my $extra; + if ( $extra_id eq 'new' ) { + $extra = $c->model('DB::ReportExtraFields')->new({}); + } else { + $extra = $c->model('DB::ReportExtraFields')->find( $extra_id ) + or $c->detach( '/page_error_404_not_found' ); + } + + if ($c->req->method eq 'POST') { + $c->forward('/auth/check_csrf_token'); + + foreach (qw/name cobrand language/) { + $extra->$_($c->get_param($_)); + } + $c->forward('/admin/update_extra_fields', [ $extra ]); + + $extra->update_or_insert; + } + + $c->forward('/auth/get_csrf_token'); + $c->forward('/admin/fetch_languages'); + + my @cobrands = uniq sort map { $_->{moniker} } FixMyStreet::Cobrand->available_cobrand_classes; + $c->stash->{cobrands} = \@cobrands; + + $c->stash->{extra} = $extra; +} + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index a8d5b995e..00fe7dd7a 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -99,6 +99,7 @@ sub report_new : Path : Args(0) { # create a problem from the submitted details $c->stash->{template} = "report/new/fill_in_details.html"; $c->forward('setup_categories_and_bodies'); + $c->forward('setup_report_extra_fields'); $c->forward('generate_map'); $c->forward('check_for_category'); @@ -138,6 +139,7 @@ sub report_new_ajax : Path('mobile') : Args(0) { } $c->forward('setup_categories_and_bodies'); + $c->forward('setup_report_extra_fields'); $c->forward('process_user'); $c->forward('process_report'); $c->forward('/photo/process_photo'); @@ -185,6 +187,7 @@ sub report_form_ajax : Path('ajax') : Args(0) { } $c->forward('setup_categories_and_bodies'); + $c->forward('setup_report_extra_fields'); # render templates to get the html my $category = $c->render_fragment( 'report/new/category.html'); @@ -235,6 +238,7 @@ sub category_extras_ajax : Path('category_extras') : Args(0) { return 1; } $c->forward('setup_categories_and_bodies'); + $c->forward('setup_report_extra_fields'); $c->forward('check_for_category'); my $category = $c->stash->{category} || ""; @@ -254,6 +258,9 @@ sub category_extras_ajax : Path('category_extras') : Args(0) { if ($c->stash->{unresponsive}->{$category}) { $generate = 1; } + if ($c->stash->{report_extra_fields}) { + $generate = 1; + } if ($generate) { $category_extra = $c->render_fragment('report/new/category_extras.html', $vars); } @@ -689,6 +696,15 @@ sub setup_categories_and_bodies : Private { $c->stash->{missing_details_body_names} = \@missing_details_body_names; } +sub setup_report_extra_fields : Private { + my ( $self, $c ) = @_; + + return unless $c->cobrand->allow_report_extra_fields; + + my @extras = $c->model('DB::ReportExtraFields')->for_cobrand($c->cobrand)->for_language($c->stash->{lang_code})->all; + $c->stash->{report_extra_fields} = \@extras; +} + =head2 check_form_submitted $bool = $c->forward('check_form_submitted'); @@ -946,6 +962,23 @@ sub set_report_extras : Private { } } + foreach my $extra_fields (@{ $c->stash->{report_extra_fields} }) { + my $metas = $extra_fields->get_extra_fields; + $param_prefix = "extra[" . $extra_fields->id . "]"; + foreach my $field ( @$metas ) { + if ( lc( $field->{required} ) eq 'true' && !$c->cobrand->category_extra_hidden($field->{code})) { + unless ( $c->get_param($param_prefix . $field->{code}) ) { + $c->stash->{field_errors}->{ $field->{code} } = _('This information is required'); + } + } + push @extra, { + name => $field->{code}, + description => $field->{description}, + value => $c->get_param($param_prefix . $field->{code}) || '', + }; + } + } + $c->cobrand->process_open311_extras( $c, @$contacts[0]->body, \@extra ) if ( scalar @$contacts ); diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 852f380f0..5dcdc9a4b 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -668,6 +668,10 @@ sub admin_pages { $pages->{users} = [ _('Users'), 6 ]; $pages->{user_edit} = [ undef, undef ]; } + if ( $self->allow_report_extra_fields && $user->has_body_permission_to('category_edit') ) { + $pages->{reportextrafields} = [ _('Extra Fields'), 10 ]; + $pages->{reportextrafields_edit} = [ undef, undef ]; + } return $pages; } @@ -1221,5 +1225,16 @@ the 'n days ago' format is used. By default the absolute date is always used. =cut sub display_days_ago_threshold { 0 } +=head2 allow_report_extra_fields + +Used to control whether site-wide extra fields are available. If true, +users with the category_edit permission can add site-wide fields via the +admin. + +=cut + +sub allow_report_extra_fields { 0 } + + 1; diff --git a/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm b/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm new file mode 100644 index 000000000..27a6bd2c6 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm @@ -0,0 +1,45 @@ +use utf8; +package FixMyStreet::DB::Result::ReportExtraFields; + +# 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("report_extra_fields"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "report_extra_fields_id_seq", + }, + "name", + { data_type => "text", is_nullable => 0 }, + "cobrand", + { data_type => "text", is_nullable => 1 }, + "language", + { data_type => "text", is_nullable => 1 }, + "extra", + { data_type => "text", is_nullable => 1 }, +); +__PACKAGE__->set_primary_key("id"); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-28 09:51:34 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:LkfbsUInnEyXowdcCEPjUQ + +__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); +__PACKAGE__->rabx_column('extra'); + +use Moo; +use namespace::clean -except => [ 'meta' ]; + +with 'FixMyStreet::Roles::Extra'; + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm b/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm new file mode 100644 index 000000000..1348df3c2 --- /dev/null +++ b/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm @@ -0,0 +1,25 @@ +package FixMyStreet::DB::ResultSet::ReportExtraFields; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings; + +sub for_cobrand { + my ( $rs, $cobrand ) = @_; + + my $result = $rs->search( + { cobrand => [ undef, $cobrand->moniker, '' ] } + ); + return $result; +} + +sub for_language { + my ( $rs, $language ) = @_; + + my $result = $rs->search( + { language => [ undef, $language, '' ] } + ); + return $result; +} + +1; diff --git a/t/app/controller/admin_reportextrafields.t b/t/app/controller/admin_reportextrafields.t new file mode 100644 index 000000000..4c706687a --- /dev/null +++ b/t/app/controller/admin_reportextrafields.t @@ -0,0 +1,312 @@ +use strict; +use warnings; + +package FixMyStreet::Cobrand::Tester; + +use parent 'FixMyStreet::Cobrand::FixMyStreet'; + +sub allow_report_extra_fields { 1 } + +sub area_types { [ 'UTA' ] } + + +package FixMyStreet::Cobrand::SecondTester; + +use parent 'FixMyStreet::Cobrand::FixMyStreet'; + +sub allow_report_extra_fields { 1 } + +sub area_types { [ 'UTA' ] } + + +package FixMyStreet::Cobrand::NoExtras; + +use parent 'FixMyStreet::Cobrand::FixMyStreet'; + +sub allow_report_extra_fields { 0 } + +sub area_types { [ 'UTA' ] } + +package main; + +use FixMyStreet::TestMech; + +my $mech = FixMyStreet::TestMech->new; + +my $user = $mech->create_user_ok('superuser@example.com', name => 'Super User', is_superuser => 1); +my $body = $mech->create_body_ok(2237, 'Oxfordshire County Council'); +my $contact = $mech->create_contact_ok( body_id => $body->id, category => 'Potholes', email => 'potholes@example.com' ); + +my $body2 = $mech->create_body_ok(2651, 'Edinburgh City Council'); +my $contact2 = $mech->create_contact_ok( body_id => $body2->id, category => 'Potholes', email => 'potholes@example.com' ); + + +FixMyStreet::override_config { + ALLOWED_COBRANDS => [ { 'tester' => '.' } ], + MAPIT_URL => 'http://mapit.uk/', + LANGUAGES => [ + 'en-gb,English,en_GB', + 'de,German,de_DE' + ] +}, sub { + $mech->log_in_ok( $user->email ); + + subtest 'add extra fields to Contacts' => sub { + my $contact_extra_fields = []; + + is_deeply $contact->get_extra_fields, $contact_extra_fields, 'contact has empty extra fields'; + $mech->get_ok("/admin/body/" . $body->id . "/" . $contact->category); + + $mech->submit_form_ok( { with_fields => { + "metadata[0].order" => "1", + "metadata[0].code" => "string_test", + "metadata[0].required" => "on", + "metadata[0].notice" => "", + "metadata[0].description" => "this is a test description", + "metadata[0].datatype_description" => "hint here", + "metadata[0].datatype" => "string", + "note" => "Added extra field", + }}); + $mech->content_contains('Values updated'); + + push @$contact_extra_fields, { + order => "1", + code => "string_test", + required => "true", + variable => "true", + description => "this is a test description", + datatype_description => "hint here", + datatype => "string", + }; + $contact->discard_changes; + is_deeply $contact->get_extra_fields, $contact_extra_fields, 'new string field was added'; + + + $mech->get_ok("/admin/body/" . $body->id . "/" . $contact->category); + $mech->submit_form_ok( { with_fields => { + "metadata[1].order" => "2", + "metadata[1].code" => "list_test", + "metadata[1].required" => undef, + "metadata[1].notice" => "", + "metadata[1].description" => "this field is a list", + "metadata[1].datatype_description" => "", + "metadata[1].datatype" => "list", + "metadata[1].values[0].key" => "key1", + "metadata[1].values[0].name" => "name1", + "note" => "Added extra list field", + }}); + $mech->content_contains('Values updated'); + + push @$contact_extra_fields, { + order => "2", + code => "list_test", + required => "false", + variable => "true", + description => "this field is a list", + datatype_description => "", + datatype => "singlevaluelist", + values => [ + { name => "name1", key => "key1" }, + ] + }; + $contact->discard_changes; + is_deeply $contact->get_extra_fields, $contact_extra_fields, 'new list field was added'; + + $contact->set_extra_fields(); + $contact->update; + }; + + subtest 'Create and update new ReportExtraFields' => sub { + my $extra_fields = []; + + my $model = FixMyStreet::App->model('DB::ReportExtraFields'); + is $model->count, 0, 'no ReportExtraFields yet'; + + $mech->get_ok("/admin/reportextrafields/new"); + $mech->submit_form_ok({ with_fields => { + name => "Test extra fields", + cobrand => "tester", + language => undef, + "metadata[0].order" => "1", + "metadata[0].code" => "string_test", + "metadata[0].required" => "on", + "metadata[0].notice" => "", + "metadata[0].description" => "this is a test description", + "metadata[0].datatype_description" => "hint here", + "metadata[0].datatype" => "string", + }}); + is $model->count, 1, 'new ReportExtraFields created'; + + my $object = $model->first; + push @$extra_fields, { + order => "1", + code => "string_test", + required => "true", + variable => "true", + description => "this is a test description", + datatype_description => "hint here", + datatype => "string", + }; + is_deeply $object->get_extra_fields, $extra_fields, 'new string field was added'; + is $object->cobrand, 'tester', 'Correct cobrand set'; + is $object->language, undef, 'Correct language set'; + + $mech->get_ok("/admin/reportextrafields/" . $object->id); + $mech->submit_form_ok( { with_fields => { + "language" => "en-gb", + "metadata[1].order" => "2", + "metadata[1].code" => "list_test", + "metadata[1].required" => undef, + "metadata[1].notice" => "", + "metadata[1].description" => "this field is a list", + "metadata[1].datatype_description" => "", + "metadata[1].datatype" => "list", + "metadata[1].values[0].key" => "key1", + "metadata[1].values[0].name" => "name1", + }}); + + push @$extra_fields, { + order => "2", + code => "list_test", + required => "false", + variable => "true", + description => "this field is a list", + datatype_description => "", + datatype => "singlevaluelist", + values => [ + { name => "name1", key => "key1" }, + ] + }; + $object->discard_changes; + is_deeply $object->get_extra_fields, $extra_fields, 'new list field was added'; + is $object->language, "en-gb", "Correct language was set"; + + $mech->get_ok("/admin/reportextrafields/" . $object->id); + $mech->submit_form_ok( { with_fields => { + "metadata[1].values[1].key" => "key2", + "metadata[1].values[1].name" => "name2", + }}); + + push @{$extra_fields->[1]->{values}}, { name => "name2", key => "key2" }; + $object->discard_changes; + is_deeply $object->get_extra_fields, $extra_fields, 'options can be added to list field'; + }; + + subtest 'Fields appear on /report/new' => sub { + $mech->get_ok("/report/new?longitude=-1.351488&latitude=51.847235&category=" . $contact->category); + $mech->content_contains("this is a test description"); + $mech->content_contains("this field is a list"); + }; +}; + +FixMyStreet::override_config { + ALLOWED_COBRANDS => [ { 'tester' => '.' } ], + MAPIT_URL => 'http://mapit.uk/', + LANGUAGES => [ 'de,German,de_DE' ] +}, sub { + subtest 'Language-specific fields are missing from /report/new for other language' => sub { + $mech->get_ok("/report/new?longitude=-1.351488&latitude=51.847235&category=" . $contact->category); + $mech->content_lacks("this is a test description"); + $mech->content_lacks("this field is a list"); + }; +}; + +FixMyStreet::override_config { + ALLOWED_COBRANDS => [ { 'secondtester' => '.' } ], + MAPIT_URL => 'http://mapit.uk/', + LANGUAGES => [ 'en-gb,English,en_GB' ] +}, sub { + subtest 'Cobrand-specific fields are missing from /report/new for other cobrand' => sub { + $mech->get_ok("/report/new?longitude=-1.351488&latitude=51.847235&category=" . $contact->category); + $mech->content_lacks("this is a test description"); + $mech->content_lacks("this field is a list"); + }; +}; + +FixMyStreet::override_config { + ALLOWED_COBRANDS => [ { 'noextras' => '.' } ], + MAPIT_URL => 'http://mapit.uk/', + LANGUAGES => [ 'en-gb,English,en_GB' ] +}, sub { + subtest "Extra fields are missing from cobrand that doesn't allow them" => sub { + my $object = FixMyStreet::App->model('DB::ReportExtraFields')->first; + $object->update({ language => "", cobrand => ""}); + + $mech->get_ok("/report/new?longitude=-1.351488&latitude=51.847235&category=" . $contact->category); + $mech->content_lacks("this is a test description"); + $mech->content_lacks("this field is a list"); + }; +}; + +FixMyStreet::App->model('DB::ReportExtraFields')->delete_all; +$mech->log_out_ok; + +subtest 'Reports are created with correct extra metadata' => sub { + FixMyStreet::override_config { + ALLOWED_COBRANDS => [ 'tester' ], + MAPIT_URL => 'http://mapit.uk/', + }, sub { + my $model = FixMyStreet::App->model('DB::ReportExtraFields'); + my $extra_fields = $model->find_or_create({ + name => "Test extra fields", + language => "", + cobrand => "" + }); + $extra_fields->push_extra_fields({ + order => "1", + code => "string_test", + required => "true", + variable => "true", + description => "this is a test description", + datatype_description => "hint here", + datatype => "string", + }); + $extra_fields->push_extra_fields({ + order => "2", + code => "list_test", + required => "false", + variable => "true", + description => "this field is a list", + datatype_description => "", + datatype => "singlevaluelist", + values => [ + { name => "name1", key => "key1" }, + ] + }); + $extra_fields->update; + + my $user = $mech->create_user_ok('testuser@example.com', name => 'Test User'); + $mech->log_in_ok($user->email); + + $mech->get_ok('/report/new?latitude=55.952055&longitude=-3.189579'); + $mech->content_contains($contact2->category); + + my $extra_id = $extra_fields->id; + $mech->submit_form_ok( { + with_fields => { + title => "Test Report", + detail => "This is a test report", + category => $contact2->category, + "extra[$extra_id]string_test" => "Problem meta string", + "extra[$extra_id]list_test" => "key1", + } + } ); + + my $report = $user->problems->first; + is_deeply $report->get_extra_fields, [ + { + name => 'string_test', + description => 'this is a test description', + value => 'Problem meta string', + }, + { + name => 'list_test', + description => 'this field is a list', + value => 'key1', + } + ], 'Report has correct extra data'; + }; +}; + + +done_testing(); diff --git a/templates/web/base/admin/category_edit.html b/templates/web/base/admin/category_edit.html index ea3fbaa79..7ae4e59b4 100644 --- a/templates/web/base/admin/category_edit.html +++ b/templates/web/base/admin/category_edit.html @@ -21,31 +21,6 @@ [% INCLUDE 'admin/contact-form.html' %] -[% IF contact.extra %] -<h2>[% loc('Extra data:') %] </h2> -<dl> - [% FOR pair IN contact.get_extra_metadata %] - <dt>[% pair.key %]</dt> <dd>[% pair.value %]</dd> - [% END %] -</dl> -<ul> - [% FOR meta IN contact.get_metadata_for_input %] - <li> - [% meta.order %], <code>[% meta.code %]</code>, [% meta.datatype %], - [% meta.required == 'true' ? loc('required') : loc('optional') %] - <br><small>[% meta.description %]</small> - [% IF meta.variable != 'false' AND meta.exists('values') %] - <ul> - [% FOR option IN meta.values %] - <li>[% option.name %] <small>([% option.key %])</small></li> - [% END %] - </ul> - [%- END %] - </li> - [%- END %] -</ul> -[% END %] - <h2>[% loc('History') %]</h2> <table border="1"> <tr> diff --git a/templates/web/base/admin/contact-form.html b/templates/web/base/admin/contact-form.html index 375b3eb99..1157e781e 100644 --- a/templates/web/base/admin/contact-form.html +++ b/templates/web/base/admin/contact-form.html @@ -137,4 +137,12 @@ as well.") %] <input type="hidden" name="token" value="[% csrf_token %]" > <input type="submit" class="btn" name="Create category" value="[% contact.in_storage ? loc('Save changes') : loc('Create category') %]" > </p> + + <h2>[% loc('Extra data:') %] </h2> + <dl> + [% FOR pair IN contact.get_extra_metadata %] + <dt>[% pair.key %]</dt> <dd>[% pair.value %]</dd> + [% END %] + </dl> + [% INCLUDE 'admin/extra-metadata-form.html' metas=(contact.get_metadata_for_input OR []) %] </form> diff --git a/templates/web/base/admin/extra-metadata-form.html b/templates/web/base/admin/extra-metadata-form.html new file mode 100644 index 000000000..6a88a3c1e --- /dev/null +++ b/templates/web/base/admin/extra-metadata-form.html @@ -0,0 +1,81 @@ +<ul class="js-metadata-items"> + [% FOR meta IN metas.merge([{}]) %] + <li class="js-metadata-item [% IF loop.last %]hidden-js js-metadata-item-template[% END %]" data-index="[% loop.index %]"> + <button class="btn btn--small js-metadata-item-remove hidden-nojs">[% loc('Remove field') %]</button> + + <div class="admin-hint"><p>[% loc('The ordering of this field on the report page. Fields are shown in ascending order according to this value.') %]</p></div> + <label> + [% loc('Order') %] + <input name="metadata[[% loop.index %]].order" data-field-name="order" type=text value="[% meta.order | html %]"> + </label> + + <div class="admin-hint"><p>[% loc('The code used to store this field value in the database. e.g. <code>address</code> would be available as <code>problem.extra.address</code> in the templates.') %]</p></div> + <label> + [% loc('Code') %] + <input name="metadata[[% loop.index %]].code" data-field-name="code" type=text value="[% meta.code | html %]"> + </label> + + <div class="admin-hint"><p>[% loc('Whether the user is required to provide a value for this field.') %]</p></div> + <label> + [% loc('Required') %] + <input name="metadata[[% loop.index %]].required" data-field-name="required" type=checkbox [% meta.required == 'true' ? 'checked' : '' %]> + </label> + + <div class="admin-hint"><p>[% loc('If ticked the user won’t see an input field, just the ‘Description’ text.') %]</p></div> + <label> + [% loc('Notice') %] + <input name="metadata[[% loop.index %]].notice" data-field-name="notice" type=checkbox [% meta.variable == 'false' ? 'checked' : '' %]> + </label> + + <div class="admin-hint"><p>[% loc('The field name as shown to the user on the report form.') %]</p></div> + <label> + [% loc('Description') %] + <input name="metadata[[% loop.index %]].description" data-field-name="description" type=text value="[% meta.description | html %]"> + </label> + + <div class="admin-hint"><p>[% loc('Can be used to display extra text to the user alongside the field. The default template does not show this (<code>meta.datatype_description</code>), you must add it in <code>category_extras_fields.html</code>') %]</p></div> + <label> + [% loc('Hint') %] + <input name="metadata[[% loop.index %]].datatype_description" data-field-name="datatype_description" type=text value="[% meta.datatype_description | html %]"> + </label> + + <div class="admin-hint"><p>[% loc('The type of input field to show to the user. <strong>Text</strong> is a simple text field, <strong>List</strong> is a drop-down selection.') %]</p></div> + <label> + [% loc('Type') %] + <select name="metadata[[% loop.index %]].datatype" data-field-name="datatype" class="js-metadata-item-type"> + <option value="string" [% meta.datatype == 'string' ? 'selected' : '' %]>[% loc('String') %]</option> + <option value="singlevaluelist" [% meta.datatype == 'singlevaluelist' ? 'selected' : '' %]>[% loc('List') %]</option> + </select> + </label> + + <div class="js-metadata-options"> + <div class="admin-hint"><p>[% loc('For each option, <strong>Key</strong> is the value which is stored in the database for that option and <strong>Name</strong> is the value displayed to the user.') %]</p></div> + [% loc('Options') %]<span class="hidden-js"> [% loc('(ignored if type is "String")') %]</span> + <ul> + [% outer_loop = loop %] + [% values = meta.values OR [] %] + [% FOREACH option IN values.merge([{}]) %] + [%# the .merge() call is so there's an empty one on the end %] + <li class="js-metadata-option [% IF loop.last %]hidden-js js-metadata-option-template[% END %]"> + <label> + [% loc('Key') %] + <input class="js-metadata-option-key" name="metadata[[% outer_loop.index %]].values[[% loop.index %]].key" type="text" value="[% option.key | html %]"> + </label> + <label> + [% loc('Name') %] + <input class="js-metadata-option-name" name="metadata[[% outer_loop.index %]].values[[% loop.index %]].name" type="text" value="[% option.name | html %]"> + </label> + <button class="btn btn--small js-metadata-option-remove hidden-nojs">[% loc('Remove') %]</button> + </li> + [% END %] + <li class="hidden-nojs"> + <button class="btn btn--small js-metadata-option-add">[% loc('Add option') %]</button> + </li> + </ul> + </div> + </li> + [%- END %] + <li class="hidden-nojs"> + <button class="btn btn--small js-metadata-item-add">[% loc('Add field') %]</button> + </li> +</ul> diff --git a/templates/web/base/admin/reportextrafields/edit.html b/templates/web/base/admin/reportextrafields/edit.html new file mode 100644 index 000000000..bc2f60ab3 --- /dev/null +++ b/templates/web/base/admin/reportextrafields/edit.html @@ -0,0 +1,68 @@ +[% INCLUDE 'admin/header.html' title=loc('Extra Fields') -%] + +<form method=post action="[% c.uri_for('', extra.id || 'new' ) %]"> + <div class="admin-hint"> + <p> + [% loc('Give this collection of fields a name. It is not shown publicly, just here in the admin.') %] + </p> + </div> + <p> + <label> + [% loc('Name') %] + <input type=text name="name" value="[% extra.name | html %]" /> + </label> + </p> + + [% IF cobrands.size > 1 %] + <div class="admin-hint"> + <p> + [% loc('To limit this collection of fields to a single cobrand, select it here.') %] + </p> + </div> + <p> + <label> + [% loc('Cobrand') %] + <select name="cobrand"> + <option value="">[% loc('All cobrands') %]</option> + [% FOREACH cobrand IN cobrands %] + <option value="[% cobrand | html %]" [% IF cobrand == extra.cobrand %]selected[% END %]>[% cobrand | html %]</option> + [% END %] + </select> + </label> + </p> + [% ELSE %] + <input type=hidden name=cobrand value="[% extra.cobrand | html %]" /> + [% END %] + + [% IF languages.size > 1 %] + <div class="admin-hint"> + <p> + [% loc('To limit this collection of fields to a single language, select it here.') %] + </p> + </div> + <p> + <label> + [% loc('Language') %] + <select name="language"> + <option value="">[% loc('All languages') %]</option> + [% FOREACH lang IN languages.pairs %] + <option value="[% lang.key | html %]" [% IF lang.key == extra.language %]selected[% END %]>[% lang.value.name | html %]</option> + [% END %] + </select> + </label> + </p> + [% ELSE %] + <input type=hidden name=language value="[% extra.language | html %]" /> + [% END %] + <p> + <label>[% loc('Fields') %]</label> + [% INCLUDE 'admin/extra-metadata-form.html' metas=extra.get_extra_fields %] + </p> + + <p> + <input type="hidden" name="token" value="[% csrf_token %]" > + <input type="submit" class="btn" name="save" value="[% extra.in_storage ? loc('Save changes') : loc('Save new fields') %]"> + </p> +</form> + +[% INCLUDE 'admin/footer.html' %] diff --git a/templates/web/base/admin/reportextrafields/index.html b/templates/web/base/admin/reportextrafields/index.html new file mode 100644 index 000000000..14d6f60d4 --- /dev/null +++ b/templates/web/base/admin/reportextrafields/index.html @@ -0,0 +1,26 @@ +[% INCLUDE 'admin/header.html' title=loc('Extra Fields') -%] + +<table> + <thead> + <tr> + <th>[% loc('Name') %]</th> + <th>[% loc('Cobrand') %]</th> + <th>[% loc('Languages') %]</th> + <th>[% loc('Fields') %]</th> + </tr> + </thead> + <tbody> + [% FOR f IN extra_fields %] + <tr> + <td><a href="[% c.uri_for('', f.id) %]">[% f.name | html %]</a></td> + <td>[% f.cobrand | html %]</td> + <td>[% f.language | html %]</td> + <td>[% f.get_extra_fields.size %]</td> + </tr> + [% END %] + </tbody> +</table> + +<a href="[% c.uri_for('', 'new') %]" class="btn">[% loc('Add extra fields') %]</a> + +[% INCLUDE 'admin/footer.html' %] diff --git a/templates/web/base/report/_inspect.html b/templates/web/base/report/_inspect.html index 91c2a8ad5..58b50a3ae 100644 --- a/templates/web/base/report/_inspect.html +++ b/templates/web/base/report/_inspect.html @@ -58,9 +58,9 @@ data-defect-types='[% category_defect_types.$cat_name %]' data-templates='[% templates_by_category.$cat_name %]'> [% IF cat_name == problem.category %] - [% INCLUDE 'report/new/category_extras_fields.html' %] + [% INCLUDE 'report/new/category_extras_fields.html' metas=category_extras.$category %] [% ELSE %] - [% INCLUDE 'report/new/category_extras_fields.html' report_meta='' %] + [% INCLUDE 'report/new/category_extras_fields.html' metas=category_extras.$category report_meta='' %] [% END %] </p> [% END %] diff --git a/templates/web/base/report/new/category_extras.html b/templates/web/base/report/new/category_extras.html index 084dd2d93..fd2752388 100644 --- a/templates/web/base/report/new/category_extras.html +++ b/templates/web/base/report/new/category_extras.html @@ -15,7 +15,14 @@ list_of_names.join( '</strong>' _ loc(' or ') _ '<strong>' ) ); %] </p> - [% INCLUDE 'report/new/category_extras_fields.html' %] + [% INCLUDE 'report/new/category_extras_fields.html' metas=category_extras.$category %] </div> [%- END %] + + [%- IF report_extra_fields %] + [% FOREACH extras IN report_extra_fields %] + [% cat_prefix = "extra[" _ extras.id _ "]" %] + [% INCLUDE 'report/new/category_extras_fields.html' metas=extras.get_extra_fields %] + [% END %] + [%- END %] </div> diff --git a/templates/web/base/report/new/category_extras_fields.html b/templates/web/base/report/new/category_extras_fields.html index 012007e06..9c2731730 100644 --- a/templates/web/base/report/new/category_extras_fields.html +++ b/templates/web/base/report/new/category_extras_fields.html @@ -1,4 +1,4 @@ -[%- FOR meta IN category_extras.$category %] +[%- FOR meta IN metas %] [%- meta_name = meta.code -%] [% IF c.cobrand.category_extra_hidden(meta_name) %] diff --git a/templates/web/base/report/new/category_wrapper.html b/templates/web/base/report/new/category_wrapper.html index 0343f86e6..291f5e923 100644 --- a/templates/web/base/report/new/category_wrapper.html +++ b/templates/web/base/report/new/category_wrapper.html @@ -12,6 +12,6 @@ [% END %] </div> -[%- IF category_extras %] +[%- IF category_extras OR report_extra_fields %] [% PROCESS "report/new/category_extras.html" %] [%- END %] diff --git a/web/cobrands/fixmystreet/admin.js b/web/cobrands/fixmystreet/admin.js index f7fcaf276..2af950b28 100644 --- a/web/cobrands/fixmystreet/admin.js +++ b/web/cobrands/fixmystreet/admin.js @@ -40,7 +40,7 @@ $(function(){ // admin hints: maybe better implemented as tooltips? - $(".admin-hint").on('click', function(){ + $(".admin").on('click', ".admin-hint", function(){ if ($(this).hasClass('admin-hint-show')) { $(this).removeClass('admin-hint-show'); } else { @@ -123,5 +123,84 @@ $(function(){ } } }); + + // Bits for the report extra fields form builder: + + // If type is changed to 'singlevaluelist' show the options list + $(".js-metadata-items").on("change", ".js-metadata-item-type", function() { + var $this = $(this); + var shown = $this.val() === 'singlevaluelist'; + var $list = $this.closest(".js-metadata-item").find('.js-metadata-options'); + $list.toggle(shown); + }); + // call immediately to perform page setup + $(".js-metadata-item-type").change(); + + // Options can be removed by clicking the 'remove' button + $(".js-metadata-items").on("click", ".js-metadata-option-remove", function(e) { + e.preventDefault(); + var $this = $(this); + var $item = $this.closest(".js-metadata-item"); + $this.closest('li').remove(); + return true; + }); + + // New options can be added by clicking the appropriate button + $(".js-metadata-items").on("click", ".js-metadata-option-add", function(e) { + e.preventDefault(); + var $ul = $(this).closest("ul"); + var $template_option = $ul.find(".js-metadata-option-template"); + var $new_option = $template_option.clone(); + $new_option.removeClass("hidden-js js-metadata-option-template"); + $new_option.show(); + $new_option.insertBefore($template_option); + $new_option.find("input").first().focus(); + renumber_metadata_options($(this).closest(".js-metadata-item")); + return true; + }); + + // Fields can be added/removed + $(".js-metadata-item-add").on("click", function(e) { + e.preventDefault(); + var $template_item = $(".js-metadata-items .js-metadata-item-template"); + var $new_item = $template_item.clone(); + $new_item.data('index', Math.max.apply( + null, + $(".js-metadata-item").map(function() { + return $(this).data('index'); + }).get() + ) + 1); + renumber_metadata_fields($new_item); + $new_item.removeClass("hidden-js js-metadata-item-template"); + $new_item.show(); + $new_item.insertBefore($template_item); + $new_item.find("input").first().focus(); + return true; + }); + $(".js-metadata-items").on("click", ".js-metadata-item-remove", function(e) { + e.preventDefault(); + $(this).closest(".js-metadata-item").remove(); + return true; + }); + + function renumber_metadata_fields($item) { + var item_index = $item.data("index"); + $item.find("input[data-field-name").each(function(i) { + var $input = $(this); + var prefix = "metadata["+item_index+"]."; + var name = prefix + $input.data("fieldName"); + $input.attr("name", name); + }); + } + + function renumber_metadata_options($item) { + var item_index = $item.data("index"); + $item.find(".js-metadata-option").each(function(i) { + var $li = $(this); + var prefix = "metadata["+item_index+"].values["+i+"]"; + $li.find(".js-metadata-option-key").attr("name", prefix+".key"); + $li.find(".js-metadata-option-name").attr("name", prefix+".name"); + }); + } }); diff --git a/web/cobrands/sass/_admin.scss b/web/cobrands/sass/_admin.scss index 58917a8ce..8a16b3f00 100644 --- a/web/cobrands/sass/_admin.scss +++ b/web/cobrands/sass/_admin.scss @@ -163,3 +163,41 @@ $button_bg_col: #a1a1a1; // also search bar (tables) float: left; } } + +.js-metadata-items { + margin: 0; + + li { + list-style: none; + position: relative; + } + + .js-metadata-item:nth-child(odd) { + background-color: #eee; + } + + .js-metadata-options { + li { + list-style: none; + + label, input[type=text] { + display: inline-block; + margin: 0; + padding: 0.25em; + } + + &:nth-child(even) { + background-color: #ddd; + } + &:nth-child(odd) { + background-color: #ccc; + } + } + } + + .js-metadata-item-remove { + position: absolute; + top: 0.25em; + right: 0.25em; + } +} diff --git a/web/cobrands/sass/_base.scss b/web/cobrands/sass/_base.scss index 3a7501993..679e7da3b 100644 --- a/web/cobrands/sass/_base.scss +++ b/web/cobrands/sass/_base.scss @@ -841,6 +841,10 @@ input.final-submit { text-align: center; } +.btn--small { + font-size: 0.8em; +} + .js #js-social-email-hide { display: none; } |