aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rwxr-xr-xbin/update-schema1
-rw-r--r--db/downgrade_0053---0052.sql5
-rw-r--r--db/schema.sql8
-rw-r--r--db/schema_0053-add-report-extra-fields-table.sql11
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm42
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/ReportExtraFields.pm61
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm33
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm15
-rw-r--r--perllib/FixMyStreet/DB/Result/ReportExtraFields.pm45
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm25
-rw-r--r--t/app/controller/admin_reportextrafields.t312
-rw-r--r--templates/web/base/admin/category_edit.html25
-rw-r--r--templates/web/base/admin/contact-form.html8
-rw-r--r--templates/web/base/admin/extra-metadata-form.html81
-rw-r--r--templates/web/base/admin/reportextrafields/edit.html68
-rw-r--r--templates/web/base/admin/reportextrafields/index.html26
-rw-r--r--templates/web/base/report/_inspect.html4
-rw-r--r--templates/web/base/report/new/category_extras.html9
-rw-r--r--templates/web/base/report/new/category_extras_fields.html2
-rw-r--r--templates/web/base/report/new/category_wrapper.html2
-rw-r--r--web/cobrands/fixmystreet/admin.js81
-rw-r--r--web/cobrands/sass/_admin.scss38
-rw-r--r--web/cobrands/sass/_base.scss4
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;
}