aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/fixmystreet.com/one-off-status-update4
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm63
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm2
-rw-r--r--perllib/FixMyStreet/TestMech.pm27
-rw-r--r--t/Mock/OpenIDConnect.pm2
-rw-r--r--t/app/controller/admin/reportextrafields.t122
-rw-r--r--t/app/controller/admin/update_edit.t14
-rw-r--r--t/app/controller/admin/users.t8
-rw-r--r--t/app/controller/alert_new.t28
-rw-r--r--t/app/controller/auth_profile.t3
-rw-r--r--t/app/controller/auth_social.t13
-rw-r--r--t/app/controller/contact.t1
-rw-r--r--t/app/controller/contact_enquiry.t10
-rw-r--r--t/app/controller/moderate.t2
-rw-r--r--t/app/controller/report_as_other.t11
-rw-r--r--t/app/controller/report_new.t6
-rw-r--r--t/app/controller/report_new_mobile.t5
-rw-r--r--t/app/controller/report_updates.t17
-rw-r--r--t/app/model/session.t4
-rw-r--r--t/app/model/user.t2
-rw-r--r--t/cobrand/bathnes.t10
-rw-r--r--t/script/inactive.t4
-rw-r--r--templates/web/base/admin/bodies/contact-form.html2
-rw-r--r--templates/web/base/admin/extra-metadata-form.html114
-rw-r--r--templates/web/base/admin/extra-metadata-item.html110
-rw-r--r--templates/web/base/admin/extra-metadata-option.html29
-rw-r--r--templates/web/base/common_scripts.html1
-rw-r--r--web/cobrands/fixmystreet/admin.js128
-rw-r--r--web/cobrands/fixmystreet/assets.js2
-rw-r--r--web/cobrands/fixmystreet/fixmystreet.js48
-rw-r--r--web/cobrands/sass/_admin.scss134
-rw-r--r--web/cobrands/sass/_base.scss3
-rwxr-xr-xweb/vendor/html5sortable.min.js2
33 files changed, 552 insertions, 379 deletions
diff --git a/bin/fixmystreet.com/one-off-status-update b/bin/fixmystreet.com/one-off-status-update
index 5a3af226a..78a27b67f 100755
--- a/bin/fixmystreet.com/one-off-status-update
+++ b/bin/fixmystreet.com/one-off-status-update
@@ -133,7 +133,7 @@ if ($northants) {
$updated++;
$p->update({
state => $new_state,
- last_update => \'current_timestamp',
+ lastupdate => \'current_timestamp',
});
my $comment = FixMyStreet::DB->resultset('Comment')->new({
@@ -164,7 +164,7 @@ sub get_state {
my $report = shift;
if ( !$report->{Reason_for_Closure} && !$report->{DEFECT_STATUS} && $report->{Enquiry_TASK_STATUS} eq 'Completed') {
- return "in progress";
+ return "closed";
}
my $fms_state = $alloy_to_fms_map{$report->{Reason_for_Closure}};
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index c2c4e7588..b00c34777 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -1187,37 +1187,44 @@ sub update_extra_fields : Private {
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';
- my $protected = $c->get_param("metadata[$i].protected") && $c->get_param("metadata[$i].protected") eq 'on';
- $meta->{protected} = $protected ? 'true' : 'false';
- my $disable_form = $c->get_param("metadata[$i].disable_form") && $c->get_param("metadata[$i].disable_form") eq 'on';
- $meta->{disable_form} = $disable_form ? 'true' : 'false';
- $meta->{description} = $c->get_param("metadata[$i].description");
- $meta->{datatype_description} = $c->get_param("metadata[$i].datatype_description");
- $meta->{automated} = $c->get_param("metadata[$i].automated")
- if $c->get_param("metadata[$i].automated");
-
- 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");
- my $disable = $c->get_param("metadata[$i].values[$j].disable");
- push(@{$meta->{values}}, {
- name => $name,
- key => $key,
- $disable ? (disable => 1) : (),
- }) if $name;
+ $meta->{protected} = $c->get_param("metadata[$i].protected") ? 'true' : 'false';
+
+ my $behaviour = $c->get_param("metadata[$i].behaviour") || 'question';
+ if ($behaviour eq 'question') {
+ $meta->{required} = $c->get_param("metadata[$i].required") ? 'true' : 'false';
+ $meta->{variable} = 'true';
+ $meta->{description} = $c->get_param("metadata[$i].description");
+ $meta->{datatype} = $c->get_param("metadata[$i].datatype");
+
+ 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");
+ my $disable = $c->get_param("metadata[$i].values[$j].disable");
+ my $disable_message = $c->get_param("metadata[$i].values[$j].disable_message");
+ push(@{$meta->{values}}, {
+ name => $name,
+ key => $key,
+ $disable ? (disable => 1, disable_message => $disable_message) : (),
+ }) if $name;
+ }
}
+ } elsif ($behaviour eq 'notice') {
+ $meta->{variable} = 'false';
+ $meta->{description} = $c->get_param("metadata[$i].description");
+ $meta->{disable_form} = $c->get_param("metadata[$i].disable_form") ? 'true' : 'false';
+ } elsif ($behaviour eq 'hidden') {
+ $meta->{automated} = 'hidden_field';
+ } elsif ($behaviour eq 'server') {
+ $meta->{automated} = 'server_set';
}
+
push @extra_fields, $meta;
}
@extra_fields = sort { $a->{order} <=> $b->{order} } @extra_fields;
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index 9c43eb227..5407ec937 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -321,7 +321,7 @@ sub disable_form_message : Private {
} elsif (($_->{variable} || '') eq 'true' && @{$_->{values} || []}) {
foreach my $opt (@{$_->{values}}) {
if ($opt->{disable}) {
- $out{message} = $_->{datatype_description};
+ $out{message} = $opt->{disable_message} || $_->{datatype_description};
$out{code} = $_->{code};
push @{$out{answers}}, $opt->{key};
}
diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm
index 927e4556c..cc2020b63 100644
--- a/perllib/FixMyStreet/TestMech.pm
+++ b/perllib/FixMyStreet/TestMech.pm
@@ -56,6 +56,26 @@ sub logged_in_ok {
"logged in" );
}
+=head2 uniquify_email
+
+Given an email address, will add the caller to it so that it can be unique per
+file. You can pass a caller file in yourself if e.g. you're another function in
+this file.
+
+=cut
+
+sub uniquify_email {
+ my ($self, $email, $file) = @_;
+
+ $file = (caller)[1] unless $file;
+ (my $pkg = $file) =~ s{/}{}g;
+
+ if ($email =~ /@/ && $email !~ /^pkg-/) {
+ $email = "pkg-$pkg-$email";
+ }
+ return $email;
+}
+
=head2 create_user_ok
$user = $mech->create_user_ok( $email );
@@ -68,8 +88,9 @@ sub create_user_ok {
my $self = shift;
my ( $username, %extra ) = @_;
+ $username = $self->uniquify_email($username, (caller)[1]);
my $params = { %extra };
- $username =~ /@/ ? $params->{email} = $username : $params->{phone} = $username;
+ $username =~ /@/ ? ($params->{email} = $username) : ($params->{phone} = $username);
my $user = FixMyStreet::DB->resultset('User')->find_or_create($params);
ok $user, "found/created user for $username";
@@ -88,6 +109,7 @@ sub log_in_ok {
my $mech = shift;
my $username = shift;
+ $username = $mech->uniquify_email($username, (caller)[1]);
my $user = $mech->create_user_ok($username);
# remember the old password and then change it to a known one
@@ -663,8 +685,9 @@ sub create_problems_for_body {
my $dt = $params->{dt} || DateTime->now();
+ my $email = $mech->uniquify_email('test@example.com', (caller)[1]);
my $user = $params->{user} ||
- FixMyStreet::DB->resultset('User')->find_or_create( { email => 'test@example.com', name => 'Test User' } );
+ FixMyStreet::DB->resultset('User')->find_or_create( { email => $email, name => 'Test User' } );
delete $params->{user};
delete $params->{dt};
diff --git a/t/Mock/OpenIDConnect.pm b/t/Mock/OpenIDConnect.pm
index 079c354fc..1a1428758 100644
--- a/t/Mock/OpenIDConnect.pm
+++ b/t/Mock/OpenIDConnect.pm
@@ -55,7 +55,7 @@ sub dispatch_request {
extension_CrmContactId => "1c304134-ef12-c128-9212-123908123901",
nonce => 'MyAwesomeRandomValue',
};
- $payload->{emails} = ['oidc@example.org'] if $self->returns_email;
+ $payload->{emails} = ['pkg-tappcontrollerauth_social.t-oidc@example.org'] if $self->returns_email;
my $signature = "dummy";
my $id_token = join(".", (
encode_base64($self->json->encode($header), ''),
diff --git a/t/app/controller/admin/reportextrafields.t b/t/app/controller/admin/reportextrafields.t
index 070e3e2fc..1714e8521 100644
--- a/t/app/controller/admin/reportextrafields.t
+++ b/t/app/controller/admin/reportextrafields.t
@@ -62,13 +62,12 @@ FixMyStreet::override_config {
$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",
+ "metadata[9999].order" => "1",
+ "metadata[9999].code" => "string_test",
+ "metadata[9999].required" => 1,
+ "metadata[9999].behaviour" => "question",
+ "metadata[9999].description" => "this is a test description",
+ "metadata[9999].datatype" => "string",
"note" => "Added extra field",
}});
$mech->content_contains('Values updated');
@@ -80,26 +79,21 @@ FixMyStreet::override_config {
variable => "true",
protected => "false",
description => "this is a test description",
- datatype_description => "hint here",
datatype => "string",
- disable_form => "false",
};
$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].disable_form" => "on",
- "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",
+ "metadata[9999].order" => "2",
+ "metadata[9999].code" => "list_test",
+ "metadata[9999].required" => undef,
+ "metadata[9999].behaviour" => "question",
+ "metadata[9999].description" => "this field is a list",
+ "metadata[9999].datatype" => "singlevaluelist",
+ "metadata[9999].values[8888].key" => "key1",
+ "metadata[9999].values[8888].name" => "name1",
"note" => "Added extra list field",
}});
$mech->content_contains('Values updated');
@@ -110,9 +104,7 @@ FixMyStreet::override_config {
required => "false",
variable => "true",
protected => "false",
- disable_form => "true",
description => "this field is a list",
- datatype_description => "",
datatype => "singlevaluelist",
values => [
{ name => "name1", key => "key1" },
@@ -121,6 +113,28 @@ FixMyStreet::override_config {
$contact->discard_changes;
is_deeply $contact->get_extra_fields, $contact_extra_fields, 'new list field was added';
+ $mech->get_ok("/admin/body/" . $body->id . "/" . $contact->category);
+ $mech->submit_form_ok( { with_fields => {
+ "metadata[9999].order" => "3",
+ "metadata[9999].code" => "emergency",
+ "metadata[9999].behaviour" => "notice",
+ "metadata[9999].disable_form" => "1",
+ "metadata[9999].description" => "please ring",
+ "note" => "Added notice field",
+ }});
+ $mech->content_contains('Values updated');
+
+ push @$contact_extra_fields, {
+ order => "3",
+ code => "emergency",
+ protected => "false",
+ description => "please ring",
+ disable_form => 'true',
+ variable => 'false',
+ };
+ $contact->discard_changes;
+ is_deeply $contact->get_extra_fields, $contact_extra_fields, 'new field was added';
+
$contact->set_extra_fields();
$contact->update;
};
@@ -142,13 +156,7 @@ FixMyStreet::override_config {
is $contact->email, 'test4@example.com', 'contact updated';
is_deeply $meta_data, [ {
order => 0,
- datatype => 'string',
- datatype_description => '',
- description => '',
- required => 'false',
- variable => 'true',
protected => 'false',
- disable_form => 'false',
code => 'POT',
automated => 'server_set'
} ], "automated fields not unset";
@@ -168,13 +176,12 @@ FixMyStreet::override_config {
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",
+ "metadata[9999].order" => "1",
+ "metadata[9999].code" => "string_test",
+ "metadata[9999].required" => 1,
+ "metadata[9999].behaviour" => "question",
+ "metadata[9999].description" => "this is a test description",
+ "metadata[9999].datatype" => "string",
}});
is $model->count, 1, 'new ReportExtraFields created';
@@ -185,9 +192,7 @@ FixMyStreet::override_config {
required => "true",
variable => "true",
protected => "false",
- disable_form => "false",
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';
@@ -197,15 +202,14 @@ FixMyStreet::override_config {
$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",
+ "metadata[9999].order" => "2",
+ "metadata[9999].code" => "list_test",
+ "metadata[9999].required" => undef,
+ "metadata[9999].behaviour" => "question",
+ "metadata[9999].description" => "this field is a list",
+ "metadata[9999].datatype" => "singlevaluelist",
+ "metadata[9999].values[8888].key" => "key1",
+ "metadata[9999].values[8888].name" => "name1",
}});
push @$extra_fields, {
@@ -214,9 +218,7 @@ FixMyStreet::override_config {
required => "false",
variable => "true",
protected => "false",
- disable_form => "false",
description => "this field is a list",
- datatype_description => "",
datatype => "singlevaluelist",
values => [
{ name => "name1", key => "key1" },
@@ -229,26 +231,16 @@ FixMyStreet::override_config {
$mech->get_ok("/admin/reportextrafields/" . $object->id);
$mech->submit_form_ok({ with_fields => {
- "metadata[2].order" => "3",
- "metadata[2].code" => "automated_test",
- "metadata[2].required" => undef,
- "metadata[2].notice" => "",
- "metadata[2].description" => "",
- "metadata[2].datatype_description" => "",
- "metadata[2].datatype" => "string",
- "metadata[2].automated" => "server_set",
+ "metadata[9999].order" => "3",
+ "metadata[9999].code" => "automated_test",
+ "metadata[9999].required" => undef,
+ "metadata[9999].behaviour" => "server",
}});
push @$extra_fields, {
order => "3",
code => "automated_test",
- required => "false",
- variable => "true",
protected => "false",
- disable_form => "false",
- description => "",
- datatype_description => "",
- datatype => "string",
automated => "server_set",
};
@@ -258,8 +250,8 @@ FixMyStreet::override_config {
$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",
+ "metadata[1].values[8888].key" => "key2",
+ "metadata[1].values[8888].name" => "name2",
}});
push @{$extra_fields->[1]->{values}}, { name => "name2", key => "key2" };
@@ -333,7 +325,6 @@ subtest 'Reports are created with correct extra metadata' => sub {
required => "true",
variable => "true",
description => "this is a test description",
- datatype_description => "hint here",
datatype => "string",
});
$extra_fields->push_extra_fields({
@@ -342,7 +333,6 @@ subtest 'Reports are created with correct extra metadata' => sub {
required => "false",
variable => "true",
description => "this field is a list",
- datatype_description => "",
datatype => "singlevaluelist",
values => [
{ name => "name1", key => "key1" },
diff --git a/t/app/controller/admin/update_edit.t b/t/app/controller/admin/update_edit.t
index cf371651b..57c8973d4 100644
--- a/t/app/controller/admin/update_edit.t
+++ b/t/app/controller/admin/update_edit.t
@@ -83,7 +83,7 @@ for my $test (
state => 'confirmed',
name => '',
anonymous => 1,
- username => 'test@example.com',
+ username => $update->user->email,
},
changes => {
text => 'this is a changed update',
@@ -98,7 +98,7 @@ for my $test (
state => 'confirmed',
name => '',
anonymous => 1,
- username => 'test@example.com',
+ username => $update->user->email,
},
changes => {
name => 'A User',
@@ -113,7 +113,7 @@ for my $test (
state => 'confirmed',
name => 'A User',
anonymous => 1,
- username => 'test@example.com',
+ username => $update->user->email,
},
changes => {
anonymous => 0,
@@ -128,10 +128,10 @@ for my $test (
state => 'confirmed',
name => 'A User',
anonymous => 0,
- username => 'test@example.com',
+ username => $update->user->email,
},
changes => {
- username => 'test2@example.com',
+ username => $user2->email,
},
log_count => 4,
log_entries => [qw/edit edit edit edit/],
@@ -144,7 +144,7 @@ for my $test (
state => 'confirmed',
name => 'A User',
anonymous => 0,
- username => 'test2@example.com',
+ username => $user2->email,
},
changes => {
state => 'unconfirmed',
@@ -159,7 +159,7 @@ for my $test (
state => 'unconfirmed',
name => 'A User',
anonymous => 0,
- username => 'test2@example.com',
+ username => $user2->email,
},
changes => {
text => 'this is a twice changed update',
diff --git a/t/app/controller/admin/users.t b/t/app/controller/admin/users.t
index b6da170ba..7361ab619 100644
--- a/t/app/controller/admin/users.t
+++ b/t/app/controller/admin/users.t
@@ -264,7 +264,7 @@ FixMyStreet::override_config {
desc => 'edit user name',
fields => {
name => '',
- email => 'test@example.com',
+ email => $user->email,
email_verified => 1,
body => $haringey->id,
phone => '',
@@ -285,7 +285,7 @@ FixMyStreet::override_config {
desc => 'edit user email',
fields => {
name => 'Changed User',
- email => 'test@example.com',
+ email => $user->email,
email_verified => 1,
body => $haringey->id,
phone => '',
@@ -464,7 +464,7 @@ FixMyStreet::override_config {
$mech->create_problems_for_body(2, 2514, 'Title', { user => $existing_user });
my $count = FixMyStreet::DB->resultset('Problem')->search({ user_id => $user->id })->count;
$mech->get_ok( '/admin/users/' . $user->id );
- $mech->submit_form_ok( { with_fields => { email => 'existing@example.com' } }, 'submit email change' );
+ $mech->submit_form_ok( { with_fields => { email => $existing_user->email } }, 'submit email change' );
is $mech->uri->path, '/admin/users/' . $existing_user->id, 'redirected';
my $p = FixMyStreet::DB->resultset('Problem')->search({ user_id => $existing_user->id })->count;
is $p, $count + 2, 'reports merged';
@@ -518,7 +518,7 @@ subtest "Send login email from admin for unverified email" => sub {
is $email->header('Subject'), "Your FixMyStreet account details",
"subject is correct";
- is $email->header('To'), 'test@example.com', "to is correct";
+ is $email->header('To'), $user->email, "to is correct";
my $link = $mech->get_link_from_email($email);
diff --git a/t/app/controller/alert_new.t b/t/app/controller/alert_new.t
index f77114d86..7318d5c65 100644
--- a/t/app/controller/alert_new.t
+++ b/t/app/controller/alert_new.t
@@ -4,56 +4,56 @@ use FixMyStreet::Script::Alerts;
my $mech = FixMyStreet::TestMech->new;
-$mech->log_in_ok('test@example.com');
+my $user = $mech->log_in_ok('test@example.com');
$mech->get_ok('/alert/subscribe?id=1');
my ($csrf) = $mech->content =~ /name="token" value="([^"]*)"/;
foreach my $test (
{
- email => 'test@example.com',
+ email => $user->email,
type => 'area_problems',
content => 'Click the link in our confirmation email to activate your alert',
email_text => "confirms that you'd like to receive an email",
uri =>
-'/alert/subscribe?type=local&rznvy=test@example.com&feed=area:1000:A_Location',
+'/alert/subscribe?type=local&rznvy=' . $user->email . '&feed=area:1000:A_Location',
param1 => 1000
},
{
- email => 'test@example.com',
+ email => $user->email,
type => 'council_problems',
content => 'Click the link in our confirmation email to activate your alert',
email_text => "confirms that you'd like to receive an email",
uri =>
-'/alert/subscribe?type=local&rznvy=test@example.com&feed=council:1000:A_Location',
+'/alert/subscribe?type=local&rznvy=' . $user->email . '&feed=council:1000:A_Location',
param1 => 1000,
param2 => 1000,
},
{
- email => 'test@example.com',
+ email => $user->email,
type => 'ward_problems',
content => 'Click the link in our confirmation email to activate your alert',
email_text => "confirms that you'd like to receive an email",
uri =>
-'/alert/subscribe?type=local&rznvy=test@example.com&feed=ward:1000:1001:A_Location:Diff_Location',
+'/alert/subscribe?type=local&rznvy=' . $user->email . '&feed=ward:1000:1001:A_Location:Diff_Location',
param1 => 1000,
param2 => 1001,
},
{
- email => 'test@example.com',
+ email => $user->email,
type => 'local_problems',
content => 'Click the link in our confirmation email to activate your alert',
email_text => "confirms that you'd like to receive an email",
uri =>
-'/alert/subscribe?type=local&rznvy=test@example.com&feed=local:10.2:20.1',
+'/alert/subscribe?type=local&rznvy=' . $user->email . '&feed=local:10.2:20.1',
param1 => 20.1,
param2 => 10.2,
},
{
- email => 'test@example.com',
+ email => $user->email,
type => 'new_updates',
content => 'Click the link in our confirmation email to activate your alert',
email_text => "confirms that you'd like to receive an email",
- uri => '/alert/subscribe?type=updates&rznvy=test@example.com&id=1',
+ uri => '/alert/subscribe?type=updates&rznvy=' . $user->email . '&id=1',
param1 => 1,
}
)
@@ -163,7 +163,7 @@ foreach my $test (
# clear existing data so we can be sure we're creating it
ok $alert->delete() if $alert && !$test->{exist};
- $mech->get_ok( '/alert/subscribe?type=local&rznvy=test-new@example.com&feed=area:1000:A_Location&token=' . $csrf );
+ $mech->get_ok( '/alert/subscribe?type=local&rznvy=' . $user->email . '&feed=area:1000:A_Location&token=' . $csrf );
$alert = FixMyStreet::App->model('DB::Alert')->find(
{
@@ -232,11 +232,11 @@ foreach my $test (
for my $test (
{
- email => 'test@example.com',
+ email => $user->email,
type => 'new_updates',
content => 'Click the link in our confirmation email to activate your alert',
email_text => 'confirm the alert',
- uri => '/alert/subscribe?type=updates&rznvy=test@example.com&id=1',
+ uri => '/alert/subscribe?type=updates&rznvy=' . $user->email . '&id=1',
param1 => 1,
}
)
diff --git a/t/app/controller/auth_profile.t b/t/app/controller/auth_profile.t
index 4be1be12c..815098caa 100644
--- a/t/app/controller/auth_profile.t
+++ b/t/app/controller/auth_profile.t
@@ -428,10 +428,9 @@ subtest "Test generate token page" => sub {
};
subtest "Test two-factor authentication admin" => sub {
- my $user = FixMyStreet::App->model('DB::User')->find( { email => $test_email } );
+ my $user = $mech->log_in_ok($test_email);
ok $user->update({ is_superuser => 1 }), 'user set to superuser';
- $mech->log_in_ok($test_email);
$mech->get_ok('/auth/generate_token');
ok !$user->get_extra_metadata('2fa_secret');
diff --git a/t/app/controller/auth_social.t b/t/app/controller/auth_social.t
index f499da659..f5f64248c 100644
--- a/t/app/controller/auth_social.t
+++ b/t/app/controller/auth_social.t
@@ -17,6 +17,7 @@ END { FixMyStreet::App->log->enable('info'); }
my $body = $mech->create_body_ok(2504, 'Westminster City Council');
my ($report) = $mech->create_problems_for_body(1, $body->id, 'My Test Report');
+my $test_email = $report->user->email;
my $contact = $mech->create_contact_ok(
body_id => $body->id, category => 'Damaged bin', email => 'BIN',
@@ -44,7 +45,7 @@ for my $test (
MAPIT_URL => 'http://mapit.uk/',
},
update => 1,
- email => 'facebook@example.org',
+ email => $mech->uniquify_email('facebook@example.org'),
uid => 123456789,
mock => 't::Mock::Facebook',
mock_hosts => ['www.facebook.com', 'graph.facebook.com'],
@@ -71,7 +72,7 @@ for my $test (
}
}
},
- email => 'oidc@example.org',
+ email => $mech->uniquify_email('oidc@example.org'),
uid => "westminster:example_client_id:my_cool_user_id",
mock => 't::Mock::OpenIDConnect',
mock_hosts => ['oidc.example.org'],
@@ -113,7 +114,7 @@ for my $state ( 'refused', 'no email', 'existing UID', 'okay' ) {
if ($page eq 'my' && $state eq 'existing UID') {
$report->update({ user_id => FixMyStreet::App->model( 'DB::User' )->find( { email => $test->{email} } )->id });
} else {
- $report->update({ user_id => FixMyStreet::App->model( 'DB::User' )->find( { email => 'test@example.com' } )->id });
+ $report->update({ user_id => FixMyStreet::App->model( 'DB::User' )->find( { email => $test_email } )->id });
}
# Set up a mock to catch (most, see below) requests to the OAuth API
@@ -269,11 +270,11 @@ FixMyStreet::override_config {
MAPIT_URL => 'http://mapit.uk/',
}, sub {
-$resolver->mock('address', sub { 'twitter@example.org' });
-
-my $tw_email = 'twitter@example.org';
+my $tw_email = $mech->uniquify_email('twitter@example.org');
my $tw_uid = 987654321;
+$resolver->mock('address', sub { $tw_email });
+
# Twitter has no way of getting the email, so no "okay" state here
for my $tw_state ( 'refused', 'existing UID', 'no email' ) {
for my $page ( 'my', 'report', 'update' ) {
diff --git a/t/app/controller/contact.t b/t/app/controller/contact.t
index 908ec971b..5b0fd2581 100644
--- a/t/app/controller/contact.t
+++ b/t/app/controller/contact.t
@@ -382,6 +382,7 @@ for my $test (
$mech->clear_emails_ok;
$mech->get_ok('/contact');
+ $test->{fields}{em} = $user->email;
$mech->submit_form_ok( { with_fields => $test->{fields} } );
my $email = $mech->get_email;
diff --git a/t/app/controller/contact_enquiry.t b/t/app/controller/contact_enquiry.t
index 483289d5f..aca4b6de4 100644
--- a/t/app/controller/contact_enquiry.t
+++ b/t/app/controller/contact_enquiry.t
@@ -95,10 +95,10 @@ FixMyStreet::override_config {
subtest 'Enquiry can be submitted when logged in' => sub {
my $problems = FixMyStreet::App->model('DB::Problem')->to_body( $body->id );
- my $user = $problems->first->user;
+ my $prob_user = $problems->first->user;
$problems->delete_all;
- $mech->log_in_ok( $user->email );
+ my $user = $mech->log_in_ok( $prob_user->email );
$mech->get_ok( '/contact/enquiry' );
$mech->submit_form_ok( {
@@ -123,7 +123,7 @@ FixMyStreet::override_config {
ok $problem->confirmed, 'problem confirmed';
is $problem->name, 'Test User', 'Report created with correct name';
is $problem->user->name, 'Test User', 'User name updated in DB';
- is $problem->user->email, 'testuser@example.org', 'Report user has correct email';
+ is $problem->user->email, $user->email, 'Report user has correct email';
$mech->log_out_ok;
};
@@ -139,7 +139,7 @@ FixMyStreet::override_config {
$mech->submit_form_ok( {
with_fields => {
name => 'Simon Neil',
- username => 'testuser@example.org',
+ username => $user->email,
category => 'General Enquiry',
detail => 'This is a general enquiry',
}
@@ -158,7 +158,7 @@ FixMyStreet::override_config {
is $problem->longitude, -0.35, 'Problem has correct longitude';
ok $problem->confirmed, 'problem confirmed';
is $problem->name, 'Simon Neil', 'Report created with correct name';
- is $problem->user->email, 'testuser@example.org', 'Report user has correct email';
+ is $problem->user->email, $user->email, 'Report user has correct email';
$user->discard_changes;
is $user->name, 'Test User', 'User name in DB not changed';
diff --git a/t/app/controller/moderate.t b/t/app/controller/moderate.t
index 256122d9b..e22d9edbc 100644
--- a/t/app/controller/moderate.t
+++ b/t/app/controller/moderate.t
@@ -204,7 +204,7 @@ subtest 'Problem moderation' => sub {
is $report->state, 'hidden', 'Is hidden';
my $email = $mech->get_email;
- is $email->header('To'), '"Test User 2" <test-moderation2@example.com>', 'Sent to correct email';
+ is $email->header('To'), '"Test User 2" <' . $user2->email . '>', 'Sent to correct email';
my $url = $mech->get_link_from_email($email);
ok $url, "extracted complain url '$url'";
diff --git a/t/app/controller/report_as_other.t b/t/app/controller/report_as_other.t
index 0c8b7d995..5f9ba7600 100644
--- a/t/app/controller/report_as_other.t
+++ b/t/app/controller/report_as_other.t
@@ -106,7 +106,7 @@ subtest "Body user, has permission to add report as another (existing) user with
FixMyStreet::Script::Reports::send();
$mech->clear_emails_ok;
- $mech->create_user_ok('existing@example.net', name => 'Existing User');
+ my $existing = $mech->create_user_ok('existing@example.net', name => 'Existing User');
my $report = add_report(
'contribute_as_another_user',
form_as => 'another_user',
@@ -114,11 +114,11 @@ subtest "Body user, has permission to add report as another (existing) user with
detail => 'Test report details.',
category => 'Potholes',
name => 'Existing Yooser',
- username => 'existing@example.net',
+ username => $existing->email,
);
is $report->name, 'Existing Yooser', 'report name is given name';
is $report->user->name, 'Existing User', 'user name remains same';
- is $report->user->email, 'existing@example.net', 'user email correct';
+ is $report->user->email, $existing->email, 'user email correct';
isnt $report->user->id, $user->id, 'user does not match';
like $mech->get_text_body_from_email, qr/Your report to Oxfordshire County Council has been logged/;
push @users, $report->user;
@@ -244,16 +244,17 @@ subtest "Body user, has permission to add update as another user with landline p
};
subtest "Body user, has permission to add update as another (existing) user with email" => sub {
+ my $existing = $mech->create_user_ok('existing@example.net', name => 'Existing User');
my $update = add_update(
'contribute_as_another_user',
form_as => 'another_user',
update => 'Test Update',
name => 'Existing Yooser',
- username => 'existing@example.net',
+ username => $existing->email,
);
is $update->name, 'Existing Yooser', 'update name is given name';
is $update->user->name, 'Existing User', 'user name remains same';
- is $update->user->email, 'existing@example.net', 'user email correct';
+ is $update->user->email, $existing->email, 'user email correct';
isnt $update->user->id, $user->id, 'user does not match';
like $mech->get_text_body_from_email, qr/Your update has been logged/;
};
diff --git a/t/app/controller/report_new.t b/t/app/controller/report_new.t
index 417d91ef9..20eecb50e 100644
--- a/t/app/controller/report_new.t
+++ b/t/app/controller/report_new.t
@@ -963,7 +963,7 @@ foreach my $test (
title => 'Test Report',
detail => 'Test report details.',
photo1 => '',
- username => $test_email,
+ username => $user->email,
password_sign_in => 'secret2',
category => 'Street lighting',
}
@@ -1347,7 +1347,7 @@ subtest "test report creation for a category that is non public" => sub {
title => 'Test Report',
detail => 'Test report details.',
photo1 => '',
- username => 'test-2@example.com',
+ username => $user->email,
name => 'Joe Bloggs',
category => 'Street lighting',
}
@@ -1801,7 +1801,7 @@ subtest "test Hart" => sub {
# check that the user has been created/ not changed
$user =
- FixMyStreet::App->model('DB::User')->find( { email => $test_email } );
+ FixMyStreet::App->model('DB::User')->find( { email => $user ? $user->email : $test_email } );
ok $user, "user found";
# find the report
diff --git a/t/app/controller/report_new_mobile.t b/t/app/controller/report_new_mobile.t
index 296007ce3..def140a72 100644
--- a/t/app/controller/report_new_mobile.t
+++ b/t/app/controller/report_new_mobile.t
@@ -15,21 +15,20 @@ subtest "Check signed up for alert when logged in" => sub {
MAPIT_URL => 'http://mapit.zurich',
MAPIT_TYPES => [ 'O08' ],
}, sub {
- $mech->log_in_ok('user@example.org');
+ my $user = $mech->log_in_ok('user@example.org');
$mech->post_ok( '/report/new/mobile', {
service => 'iPhone',
title => 'Title',
detail => 'Problem detail',
lat => 47.381817,
lon => 8.529156,
- email => 'user@example.org',
+ email => $user->email,
pc => '',
name => 'Name',
});
my $res = $mech->response;
ok $res->header('Content-Type') =~ m{^application/json\b}, 'response should be json';
- my $user = FixMyStreet::DB->resultset('User')->search({ email => 'user@example.org' })->first;
my $a = FixMyStreet::DB->resultset('Alert')->search({ user_id => $user->id })->first;
isnt $a, undef, 'User is signed up for alert';
};
diff --git a/t/app/controller/report_updates.t b/t/app/controller/report_updates.t
index 8ff5b4d24..983305661 100644
--- a/t/app/controller/report_updates.t
+++ b/t/app/controller/report_updates.t
@@ -1247,6 +1247,7 @@ for my $test (
subtest $test->{desc} => sub {
# Set things up
my $user = $mech->create_user_ok( $test->{form_values}->{username} );
+ $test->{form_values}{username} = $user->email;
my $pw = 'secret2';
$user->update( { name => 'Mr Reg', password => $pw } );
$report->comments->delete;
@@ -1303,7 +1304,7 @@ subtest 'submit an update for a registered user, creating update by email' => su
$mech->submit_form_ok( {
with_fields => {
submit_update => 1,
- username => 'registered@example.com',
+ username => $user->email,
update => 'Update from a user',
add_alert => undef,
name => 'New Name',
@@ -1338,7 +1339,7 @@ subtest 'submit an update for a registered user, creating update by email' => su
ok $update, 'found update in database';
is $update->state, 'unconfirmed', 'update unconfirmed';
- is $update->user->email, 'registered@example.com', 'update email';
+ is $update->user->email, $user->email, 'update email';
is $update->text, 'Update from a user', 'update text';
$mech->get_ok( $url );
@@ -1505,7 +1506,7 @@ for my $test (
$mech->clear_emails_ok();
- $mech->log_in_ok( $test->{email} );
+ my $user = $mech->log_in_ok( $test->{email} );
$mech->get_ok("/report/$report_id");
my $values = $mech->visible_form_values( 'updateForm' );
@@ -1548,7 +1549,7 @@ for my $test (
};
is $update->text, $results->{update}, 'update text';
- is $update->user->email, $test->{email}, 'update user';
+ is $update->user->email, $user->email, 'update user';
is $update->state, 'confirmed', 'update confirmed';
is $update->anonymous, $test->{anonymous}, 'user anonymous';
@@ -1678,7 +1679,7 @@ foreach my $test (
$mech->clear_emails_ok();
- $mech->log_in_ok( $test->{email} );
+ my $user = $mech->log_in_ok( $test->{email} );
$mech->get_ok("/report/$report_id");
my $values = $mech->visible_form_values('updateForm');
@@ -1708,7 +1709,7 @@ foreach my $test (
my $update = $report->comments->first;
ok $update, 'found update';
is $update->text, $results->{update}, 'update text';
- is $update->user->email, $test->{email}, 'update user';
+ is $update->user->email, $user->email, 'update user';
is $update->state, 'confirmed', 'update confirmed';
is $update->anonymous, $test->{anonymous}, 'user anonymous';
@@ -1752,7 +1753,7 @@ for my $test (
fields => {
submit_update => 1,
name => 'Test User',
- username => 'test@example.com',
+ username => $report->user->email,
may_show_name => 1,
update => 'update from owner',
add_alert => undef,
@@ -1774,7 +1775,7 @@ for my $test (
submit_update => 1,
name => 'Test User',
may_show_name => 1,
- username => 'test@example.com',
+ username => $report->user->email,
update => 'update from owner',
add_alert => undef,
fixed => 1,
diff --git a/t/app/model/session.t b/t/app/model/session.t
index f76533727..f460204e5 100644
--- a/t/app/model/session.t
+++ b/t/app/model/session.t
@@ -2,13 +2,13 @@ use FixMyStreet::TestMech;
my $mech = FixMyStreet::TestMech->new;
-$mech->log_in_ok('test@example.com');
+my $user = $mech->log_in_ok('test@example.com');
my $session = FixMyStreet::DB->resultset("Session")->first;
my $id = $session->id;
$id =~ s/\s+$//;
is $id, "session:" . $session->id_code;
-is $session->user->email, 'test@example.com';
+is $session->user->email, $user->email;
done_testing;
diff --git a/t/app/model/user.t b/t/app/model/user.t
index 88b29ca84..c1981c858 100644
--- a/t/app/model/user.t
+++ b/t/app/model/user.t
@@ -67,7 +67,7 @@ FixMyStreet::override_config {
};
subtest 'Check non-existent methods on user object die' => sub {
- my $c = ctx_request(POST '/auth', { username => 'test@example.com', password_sign_in => 'secret' });
+ my $c = ctx_request(POST '/auth', { username => $problem->user->email, password_sign_in => 'secret' });
throws_ok(
sub { $c->user->is_super_user },
qr/Can't locate object method 'is_super_user'/,
diff --git a/t/cobrand/bathnes.t b/t/cobrand/bathnes.t
index 1ebddd05a..d434d1160 100644
--- a/t/cobrand/bathnes.t
+++ b/t/cobrand/bathnes.t
@@ -156,28 +156,28 @@ subtest 'extra CSV columns are present if permission granted' => sub {
is $rows[1]->[18], 'iOS', 'Site Used shows whether report made via app';
is $rows[1]->[19], '', 'Reported As is empty if not made on behalf of another user/body';
- is $rows[1]->[20], 'normaluser@example.com', 'User email is correct';
+ is $rows[1]->[20], $normaluser->email, 'User email is correct';
is $rows[1]->[21], '+447123456789', 'User phone number is correct';
is $rows[1]->[22], '', 'Staff User is empty if not made on behalf of another user';
is $rows[1]->[23], 'width = 10cm; depth = 25cm', 'Attribute Data is correct';
is $rows[2]->[18], 'bathnes', 'Site Used shows correct cobrand';
is $rows[2]->[19], 'body', 'Reported As is correct if made on behalf of body';
- is $rows[2]->[20], 'counciluser@example.com', 'User email is correct';
+ is $rows[2]->[20], $counciluser->email, 'User email is correct';
is $rows[2]->[21], '', 'User phone number is correct';
is $rows[2]->[22], '', 'Staff User is empty if not made on behalf of another user';
is $rows[2]->[23], '', 'Attribute Data is correct';
is $rows[3]->[18], 'bathnes', 'Site Used shows correct cobrand';
is $rows[3]->[19], 'another_user', 'Reported As is set if reported on behalf of another user';
- is $rows[3]->[20], 'normaluser@example.com', 'User email is correct';
+ is $rows[3]->[20], $normaluser->email, 'User email is correct';
is $rows[3]->[21], '+447123456789', 'User phone number is correct';
- is $rows[3]->[22], 'counciluser@example.com', 'Staff User is correct if made on behalf of another user';
+ is $rows[3]->[22], $counciluser->email, 'Staff User is correct if made on behalf of another user';
is $rows[3]->[23], '', 'Attribute Data is correct';
is $rows[4]->[18], 'bathnes', 'Site Used shows correct cobrand';
is $rows[4]->[19], 'anonymous_user', 'Reported As is set if reported on behalf of another user';
- is $rows[4]->[20], 'counciluser@example.com', 'User email is correct';
+ is $rows[4]->[20], $counciluser->email, 'User email is correct';
is $rows[4]->[21], '', 'User phone number is correct';
is $rows[4]->[22], '', 'Staff User is empty if not made on behalf of another user';
is $rows[4]->[23], '', 'Attribute Data is correct';
diff --git a/t/script/inactive.t b/t/script/inactive.t
index 4667c989b..489ff55ca 100644
--- a/t/script/inactive.t
+++ b/t/script/inactive.t
@@ -5,12 +5,12 @@ use_ok 'FixMyStreet::Script::Inactive';
my $in = FixMyStreet::Script::Inactive->new( anonymize => 6, email => 3 );
my $mech = FixMyStreet::TestMech->new;
-my $user = FixMyStreet::DB->resultset("User")->find_or_create({ email => 'test@example.com' });
+my $user = $mech->create_user_ok('test@example.com');
my $t = DateTime->new(year => 2016, month => 1, day => 1, hour => 12);
$user->last_active($t);
$user->update;
-my $user_inactive = FixMyStreet::DB->resultset("User")->find_or_create({ email => 'inactive@example.com' });
+my $user_inactive = $mech->create_user_ok('inactive@example.com');
$t = DateTime->now->subtract(months => 4);
$user_inactive->last_active($t);
$user_inactive->update;
diff --git a/templates/web/base/admin/bodies/contact-form.html b/templates/web/base/admin/bodies/contact-form.html
index 7a58644e5..7f559274d 100644
--- a/templates/web/base/admin/bodies/contact-form.html
+++ b/templates/web/base/admin/bodies/contact-form.html
@@ -140,7 +140,7 @@
</dl>
[% INCLUDE 'admin/extra-metadata-form.html' metas=(contact.get_metadata_for_editing OR []) %]
- <p class="form-group">
+ <p class="form-group" style="margin-top: 2em">
<label for="note">[% loc('Summarise your changes') %]</label>
<span class="form-hint" id="note-hint">[% loc("If you’ve made changes, leave a note explaining what, for other admins to see.") %]</span>
<input class="form-control" type="text" id="note" name="note" size="30" aria-describedby="note-hint" required>
diff --git a/templates/web/base/admin/extra-metadata-form.html b/templates/web/base/admin/extra-metadata-form.html
index b82eca966..9b8e4909f 100644
--- a/templates/web/base/admin/extra-metadata-form.html
+++ b/templates/web/base/admin/extra-metadata-form.html
@@ -1,107 +1,11 @@
-<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 type="button" class="btn btn--small js-metadata-item-remove hidden-nojs">[% loc('Remove field') %]</button>
+<div class="extra-metadata-form js-metadata-items">
+ [% FOR meta IN metas %]
+ [% INCLUDE 'admin/extra-metadata-item.html' i=loop.index collapsed=1 %]
+ [% END %]
+</div>
- <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 OR 0 | html %]">
- </label>
+<div class="hidden-js" id="js-template-extra-metadata-item">
+ [% INCLUDE 'admin/extra-metadata-item.html' meta={} i=9999 collapsed=0 %]
+</div>
- <div class="admin-hint"><p>[% loc('Whether the field is displayed to the user, included as a hidden field and automatically populated, or set by the server upon Open311 submission. This field is usually set automatically.') %]</p></div>
- <label>
- [% loc('Automated') %]
- <select name="metadata[[% loop.index %]].automated" data-field-name="automated">
- <option value="" [% meta.automated == '' ? 'selected' : '' %]></option>
- <option value="server_set" [% meta.automated == 'server_set' ? 'selected' : '' %]>[% loc('Server Set') %]</option>
- <option value="hidden_field" [% meta.automated == 'hidden_field' ? 'selected' : '' %]>[% loc('Hidden Field') %]</option>
- </select>
- </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>report.get_extra_field_value("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('If ticked this extra data will not be edited or deleted by the Open311 population script.') %]</p></div>
- <label>
- [% loc('Protected') %]
- <input name="metadata[[% loop.index %]].protected" data-field-name="protected" type=checkbox [% meta.protected == 'true' ? 'checked' : '' %]>
- </label>
-
- <div class="admin-hint"><p>[% loc('If ticked the entire report form will be disabled when this category is selected.') %]</p></div>
- <label>
- [% loc('Disable form') %]
- <input name="metadata[[% loop.index %]].disable_form" data-field-name="disable_form" type=checkbox [% meta.disable_form == 'true' ? '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 %]
- [% SET values = meta.item('values') ? meta.values : [] %]
- [% 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>
- <label>
- [% loc('Disable form') %]
- <input class="js-metadata-option-disable" name="metadata[[% outer_loop.index %]].values[[% loop.index %]].disable" type="checkbox"[% ' checked' IF option.disable %]>
- </label>
- <button type="button" class="btn btn--small js-metadata-option-remove hidden-nojs">[% loc('Remove') %]</button>
- </li>
- [% END %]
- <li class="hidden-nojs">
- <button type="button" class="btn btn--small js-metadata-option-add">[% loc('Add option') %]</button>
- </li>
- </ul>
- </div>
- </li>
- [%- END %]
- <li class="hidden-nojs">
- <button type="button" class="btn btn--small js-metadata-item-add">[% loc('Add field') %]</button>
- </li>
-</ul>
+<button type="button" class="btn btn--small js-metadata-item-add hidden-nojs">[% loc('Add field') %]</button>
diff --git a/templates/web/base/admin/extra-metadata-item.html b/templates/web/base/admin/extra-metadata-item.html
new file mode 100644
index 000000000..66cb6f79c
--- /dev/null
+++ b/templates/web/base/admin/extra-metadata-item.html
@@ -0,0 +1,110 @@
+[%
+SET behaviour = '';
+DEFAULT behaviour = 'server' IF meta.automated == 'server_set';
+DEFAULT behaviour = 'hidden' IF meta.automated == 'hidden_field';
+DEFAULT behaviour = 'notice' IF meta.variable == 'false';
+DEFAULT behaviour = 'question';
+~%]
+
+<fieldset class="extra-metadata-item js-metadata-item" data-i="[% i %]">
+ <legend class="visuallyhidden">Extra data field [% i %]</legend>
+
+ <header class="extra-metadata-item__header hidden-nojs">
+ <div class="js-metadata-item-header-grab extra-metadata-item__header__grab" aria-label="Drag to reorder"></div>
+ <button type="button" class="js-metadata-item-header-title extra-metadata-item__header__title" data-default="[% loc('New field') %]" data-toggle-visibility="#metadata-fieldset-[% i %]">
+ [%~ IF meta.code ~%]
+ <strong>[% meta.code | html %]</strong>
+ / [% behaviour %]
+ [% IF meta.description %] / [% meta.description.substr(0, 50) | html %][% END %]
+ [%~ ELSE ~%]
+ <strong>[% loc('New field') %]</strong>
+ [%~ END ~%]
+ </button>
+ <button type="button" class="js-metadata-item-remove extra-metadata-item__header__remove">[% loc('Remove field') %]</button>
+ </header>
+
+ <div class="extra-metadata-item__body [% IF collapsed %]hidden-js[% END %]" id="metadata-fieldset-[% i %]">
+
+ <div class="form-group">
+ <label for="metadata-[% i %]-code">[% loc('Code') %]</label>
+ <span class="form-hint" id="metadata-[% i %]-code-hint">[% loc('The code used to store this field value in the database.') %]</span>
+ <input class="form-control" name="metadata[[% i %]].code" id="metadata-[% i %]-code" aria-describedby="metadata-[% i %]-code-hint" type=text value="[% meta.code | html %]">
+ </div>
+
+ <fieldset>
+ <legend>[% loc('Behaviour') %]</legend>
+ <div class="form-check form-check--inline">
+ <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-question" value="question" data-show="#metadata-[% i %]-description-group, #metadata-[% i %]-description-label-question, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group, #metadata-[% i %]-protected-group" data-hide="#metadata-[% i %]-description-label-notice, #metadata-[% i %]-disabled-group"[% ' checked' IF behaviour == 'question' %]>
+ <label for="metadata-[% i %]-behaviour-question">[% loc('Extra question shown to user') %]</label>
+ </div>
+ <div class="form-check form-check--inline">
+ <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-notice" value="notice" data-show="#metadata-[% i %]-description-group, #metadata-[% i %]-description-label-notice, #metadata-[% i %]-protected-group, #metadata-[% i %]-disabled-group" data-hide="#metadata-[% i %]-description-label-question, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group"[% ' checked' IF behaviour == 'notice' %]>
+ <label for="metadata-[% i %]-behaviour-notice">[% loc('Notice shown to user') %]</label>
+ </div>
+ <div class="form-check form-check--inline">
+ <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-hidden" value="hidden" data-show="#metadata-[% i %]-protected-group" data-hide="#metadata-[% i %]-description-group, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group, #metadata-[% i %]-disabled-group"[% ' checked' IF behaviour == 'hidden' %]>
+ <label for="metadata-[% i %]-behaviour-hidden">[% loc('Hidden data in reporting form') %]</label>
+ </div>
+ <div class="form-check form-check--inline">
+ <input type="radio" name="metadata[[% i %]].behaviour" id="metadata-[% i %]-behaviour-server" value="server" data-show="#metadata-[% i %]-protected-group" data-hide="#metadata-[% i %]-description-group, #metadata-[% i %]-datatype-group, #metadata-[% i %]-required-group, #metadata-[% i %]-disabled-group"[% ' checked' IF behaviour == 'server' %]>
+ <label for="metadata-[% i %]-behaviour-server">[% loc('Internal data set by Open311') %]</label>
+ </div>
+ </fieldset>
+
+ <div class="form-group js-sort-order">
+ <label for="metadata-[% i %]-order">[% loc('Order') %]</label>
+ <input class="form-control" name="metadata[[% i %]].order" id="metadata-[% i %]-order" type=text value="[% meta.order OR 0 | html %]">
+ </div>
+
+ <div class="form-group hidden-js" id="metadata-[% i %]-description-group">
+ <label for="metadata-[% i %]-description" id="metadata-[% i %]-description-label-question" class="[% 'hidden-js' UNLESS behaviour == 'question' %]">[% loc('Question text') %]</label>
+ <label for="metadata-[% i %]-description" id="metadata-[% i %]-description-label-notice" class="[% 'hidden-js' UNLESS behaviour == 'notice' %]">[% loc('Notice text') %]</label>
+ <textarea class="form-control" name="metadata[[% i %]].description" id="metadata-[% i %]-description" rows="2">[% meta.description | html %]</textarea>
+ </div>
+
+ <div class="hidden-js" id="metadata-[% i %]-datatype-group">
+ <div class="form-group">
+ <label for="metadata-[% i %]-datatype">[% loc('Display as') %]</label>
+ <select class="form-control" name="metadata[[% i %]].datatype" id="metadata-[% i %]-datatype">
+ <option value="string" data-hide="#metadata-[% i %]-options-group" [% 'selected' IF meta.datatype == 'string' %]>[% loc('Text field') %]</option>
+ <option value="singlevaluelist" data-show="#metadata-[% i %]-options-group" [% 'selected' IF meta.datatype == 'singlevaluelist' %]>[% loc('Drop-down list') %]</option>
+ </select>
+ </div>
+
+ <div class="hidden-js" id="metadata-[% i %]-options-group">
+ <p class="label">[% loc('Options') %]</p>
+ <div class="js-metadata-options">
+ [% SET options = meta.item('values') ? meta.values : [] %]
+ [% FOR option IN options %]
+ [% INCLUDE 'admin/extra-metadata-option.html' option=option i=i j=loop.index %]
+ [% END %]
+ </div>
+
+ <div class="hidden-js"[% IF i==9999 %] id="js-template-extra-metadata-option"[% END %]>
+ [% INCLUDE 'admin/extra-metadata-option.html' option={} i=i j=8888 %]
+ </div>
+
+ <button type="button" class="btn btn--small js-metadata-option-add hidden-nojs">[% loc('Add option') %]</button>
+ </div>
+ </div>
+
+ <p class="form-check hidden-js" id="metadata-[% i %]-required-group">
+ <input type="checkbox" name="metadata[[% i %]].required" value="1" id="metadata-[% i %]-required" aria-describedby="metadata-[% i %]-required-hint" [% ' checked' IF meta.required == 'true' %]>
+ <label for="metadata-[% i %]-required">[% loc('Required') %]</label>
+ <span class="form-hint" id="metadata-[% i %]-required-hint">[% loc('Prevent user from submitting the form until this field is filled in.') %]</span>
+ </p>
+
+ <p class="form-check hidden-js" id="metadata-[% i %]-disabled-group">
+ <input type="checkbox" name="metadata[[% i %]].disable_form" value="1" id="metadata-[% i %]-disable_form" aria-describedby="metadata-[% i %]-disable_form-hint" [% ' checked' IF meta.disable_form == 'true' %]>
+ <label for="metadata-[% i %]-disable_form">[% loc('Disable form when this category is selected') %]</label>
+ <span class="form-hint" id="metadata[[% i %]]-disable_form-hint">[% loc('If ticked, the form will be disabled and this item’s notice text will be displayed.') %]</span>
+ </p>
+
+ <p class="form-check hidden-js" id="metadata-[% i %]-protected-group">
+ <input type="checkbox" name="metadata[[% i %]].protected" value="1" id="metadata-[% i %]-protected" aria-describedby="metadata-[% i %]-protected-hint" [% ' checked' IF meta.protected == 'true' %]>
+ <label for="metadata-[% i %]-protected">[% loc('Protect from Open311 changes') %]</label>
+ <span class="form-hint" id="metadata[[% i %]]-protected-hint">[% loc('If ticked, this extra data will not be edited or deleted by the Open311 population script.') %]</span>
+ </p>
+
+ </div>
+</fieldset>
diff --git a/templates/web/base/admin/extra-metadata-option.html b/templates/web/base/admin/extra-metadata-option.html
new file mode 100644
index 000000000..360dfb208
--- /dev/null
+++ b/templates/web/base/admin/extra-metadata-option.html
@@ -0,0 +1,29 @@
+<fieldset class="extra-metadata-option js-metadata-option">
+ <legend class="visuallyhidden">Option [% j %]</legend>
+
+ <div class="row">
+ <div class="col">
+ <label for="metadata-[% i %]-values-[% j %]-key">[% loc('Internal key') %]</label>
+ <input type="text" class="form-control" name="metadata[[% i %]].values[[% j %]].key" id="metadata-[% i %]-values-[% j %]-key" value="[% option.key | html %]">
+ </div>
+ <div class="col">
+ <label for="metadata-[% i %]-values-[% j %]-name">[% loc('Displayed label') %]</label>
+ <input type="text" class="form-control" name="metadata[[% i %]].values[[% j %]].name" id="metadata-[% i %]-values-[% j %]-name" value="[% option.name | html %]">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col">
+ <p class="form-check">
+ <input type="checkbox" name="metadata[[% i %]].values[[% j %]].disable" value="1" id="metadata-[% i %]-values-[% j %]-disable" data-toggle-visibility="#js-disable-form-message-box-[% i %]-[% j %]" [% ' checked' IF option.disable %]>
+ <label for="metadata-[% i %]-values-[% j %]-disable">[% loc('Disable form when this option is selected') %]</label>
+ </p>
+ <p class="form-group [% 'hidden-js' IF NOT option.disable %]" id="js-disable-form-message-box-[% i %]-[% j %]">
+ <label for="disabled-message-[% i %]-[% j %]">[% loc('Message to show when form is disabled (HTML permitted)') %]</label>
+ <textarea id="disabled-message-[% i %]-[% j %]" name="metadata[[% i %]].values[[% j %]].disable_message" class="form-control">[% option.disable_message OR meta.datatype_description | html %]</textarea>
+ </p>
+ </div>
+ <div class="col">
+ <button type="button" class="js-metadata-option-remove">[% loc('Remove option') %]</button>
+ </div>
+ </div>
+</fieldset>
diff --git a/templates/web/base/common_scripts.html b/templates/web/base/common_scripts.html
index fd7011763..60775fcae 100644
--- a/templates/web/base/common_scripts.html
+++ b/templates/web/base/common_scripts.html
@@ -68,6 +68,7 @@ END;
IF admin;
scripts.push(
version('/cobrands/fixmystreet/admin.js'),
+ version('/vendor/html5sortable.min.js'),
);
END;
diff --git a/web/cobrands/fixmystreet/admin.js b/web/cobrands/fixmystreet/admin.js
index b598f52dd..d3df27f33 100644
--- a/web/cobrands/fixmystreet/admin.js
+++ b/web/cobrands/fixmystreet/admin.js
@@ -115,39 +115,7 @@ $(function(){
$(".js-extra-fields-ui").removeClass("hidden-js");
});
- // 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;
- });
-
+ // For "parent categories"
$(".js-group-item-add").on("click", function(e) {
e.preventDefault();
var $template_item = $(".js-group-item-template");
@@ -158,49 +126,67 @@ $(function(){
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;
+ $('.js-metadata-item-add').on('click', function(){
+ var $container = $(this).prevAll('.js-metadata-items');
+ var i = $container.children().length + 1;
+ var html = $('#js-template-extra-metadata-item').html().replace(/9999/g, i);
+ $container.append(html);
+ fixmystreet.set_up.toggle_visibility();
+ reloadSortableMetadataItems();
});
- function renumber_metadata_fields($item) {
- var item_index = $item.data("index");
- $item.find("[data-field-name]").each(function(i) {
- var $input = $(this);
- var prefix = "metadata["+item_index+"].";
- var name = prefix + $input.data("fieldName");
- $input.attr("name", name);
+ $('.js-metadata-items').on('click', '.js-metadata-item-remove', function(){
+ $(this).parents('.js-metadata-item').remove();
+ }).on('change', '.js-metadata-item', updateMetadataItemTitle);
+
+ sortable('.js-metadata-items', {
+ forcePlaceholderSize: true,
+ handle: '.js-metadata-item-header-grab',
+ placeholder: '<div class="extra-metadata-item-placeholder"></div>'
+ })[0].addEventListener('sortupdate', function(e) {
+ $(e.detail.destination.items).each(function(i){
+ $(this).find('.js-sort-order input').val(i);
});
+ });
+ $('.js-sort-order').addClass('hidden-js');
+
+ function reloadSortableMetadataItems(){
+ sortable('.js-metadata-items', 'reload');
+ $('.js-sort-order').addClass('hidden-js');
}
- 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");
- $li.find(".js-metadata-option-disable").attr("name", prefix+".disable");
- });
+ $('.js-metadata-item').each(updateMetadataItemTitle);
+
+ function updateMetadataItemTitle(){
+ var $title = $(this).find('.js-metadata-item-header-title');
+ var defaultTitle = $title.attr('data-default');
+ var html = '<strong>' + defaultTitle + '</strong>';
+ var code = $(this).find('input[name$=".code"]').val();
+ if ( code ) {
+ html = '<strong>' + code + '</strong>';
+ var behaviour = $(this).find('input[name$=".behaviour"]:checked');
+ if ( behaviour.length ) {
+ html += ' / ' + behaviour.val();
+ }
+ var description = $(this).find('textarea[name$=".description"]').val();
+ if ( description && (behaviour.val() == 'question' || behaviour.val() == 'notice') ) {
+ html += ' / ' + description.substring(0, 50);
+ }
+ }
+ $title.html(html);
}
+
+ $('.js-metadata-items').on('click', '.js-metadata-option-add', function(){
+ var $container = $(this).prevAll('.js-metadata-options');
+ var i = $(this).parents('.js-metadata-item').attr('data-i');
+ var j = $container.children().length + 1;
+ var html = $('#js-template-extra-metadata-option').html().replace(/9999/g, i).replace(/8888/g, j);
+ $container.append(html);
+ fixmystreet.set_up.toggle_visibility();
+ });
+
+ $('.js-metadata-items').on('click', '.js-metadata-option-remove', function(){
+ $(this).parents('.js-metadata-option').remove();
+ });
});
diff --git a/web/cobrands/fixmystreet/assets.js b/web/cobrands/fixmystreet/assets.js
index 0b2205076..a0cf2e29d 100644
--- a/web/cobrands/fixmystreet/assets.js
+++ b/web/cobrands/fixmystreet/assets.js
@@ -1030,7 +1030,7 @@ fixmystreet.message_controller = (function() {
function show_responsibility_error(id, asset_item, asset_type) {
$("#js-roads-responsibility").removeClass("hidden");
$("#js-roads-responsibility .js-responsibility-message").addClass("hidden");
- var asset_strings = $('.js-roads-asset');
+ var asset_strings = $(id).find('.js-roads-asset');
if (asset_item) {
asset_strings.html('a <b class="asset-' + asset_type + '">' + asset_item + '</b>');
} else {
diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js
index c7749c729..6b88bc3d3 100644
--- a/web/cobrands/fixmystreet/fixmystreet.js
+++ b/web/cobrands/fixmystreet/fixmystreet.js
@@ -992,14 +992,48 @@ $.extend(fixmystreet.set_up, {
},
toggle_visibility: function() {
- $('input[type="checkbox"][data-toggle-visibility]').each(function(){
- var input = this;
- var $target = $( $(this).attr('data-toggle-visibility') );
- var update = function() {
- $target.toggleClass('hidden-js', ! input.checked );
+ $('[data-toggle-visibility]').each(function(){
+ var $target = $( $(this).attr('data-toggle-visibility') );
+ if ( $(this).is(':checkbox') ){
+ var input = this;
+ var update = function() {
+ $target.toggleClass('hidden-js', ! input.checked );
+ };
+ $(this).off('change.togglevisibility').on('change.togglevisibility', update);
+ update();
+ } else {
+ $(this).off('click.togglevisibility').on('click.togglevisibility', function(){
+ $target.toggleClass('hidden-js');
+ });
+ }
+ });
+
+ $('input[type="radio"][data-show], input[type="radio"][data-hide]').each(function(){
+ var update = function(){
+ if ( this.checked ) {
+ var $showTarget = $( $(this).attr('data-show') );
+ var $hideTarget = $( $(this).attr('data-hide') );
+ $showTarget.removeClass('hidden-js');
+ $hideTarget.addClass('hidden-js');
+ }
+ };
+ // off/on to make sure event handler is only bound once.
+ $(this).off('change.togglevisibility').on('change.togglevisibility', update);
+ update.call(this); // pass DOM element as `this`
+ });
+
+ $('option[data-show], option[data-hide]').each(function(){
+ var $select = $(this).parent();
+ var update = function(){
+ var $option = $(this).find('option:selected');
+ var $showTarget = $( $option.attr('data-show') );
+ var $hideTarget = $( $option.attr('data-hide') );
+ $showTarget.removeClass('hidden-js');
+ $hideTarget.addClass('hidden-js');
};
- $(input).on('change', update);
- update();
+ // off/on to make sure event handler is only bound once.
+ $select.off('change.togglevisibility').on('change.togglevisibility', update);
+ update.call($select[0]); // pass DOM element as `this`
});
},
diff --git a/web/cobrands/sass/_admin.scss b/web/cobrands/sass/_admin.scss
index d36c8ced0..3b47ea9aa 100644
--- a/web/cobrands/sass/_admin.scss
+++ b/web/cobrands/sass/_admin.scss
@@ -173,41 +173,125 @@ $button_bg_col: #a1a1a1; // also search bar (tables)
}
}
-.js-metadata-items {
- margin: 0;
+.extra-metadata-item,
+.extra-metadata-option {
+ border: 1px solid $table_border_color;
+ margin: 1em 0;
+ border-radius: 4px;
+ overflow: hidden;
- li {
- list-style: none;
- position: relative;
+ // Make it look more "grabbable" if javascript available.
+ html.js & {
+ box-shadow: 0 0.1em 0.2em rgba(0, 0, 0, 0.1);
}
+}
- .js-metadata-item:nth-child(odd) {
- background-color: #eee;
- }
+.extra-metadata-item-placeholder {
+ border: 1px solid #fff;
+ margin: 1em 0;
+}
- .js-metadata-options {
- li {
- list-style: none;
+.extra-metadata-item__header {
+ @include flex-container();
+ line-height: 1;
+ background: #f3f3f3;
- label, input[type=text] {
- display: inline-block;
- margin: 0;
- padding: 0.25em;
- }
+ & > * {
+ padding: 1em;
+ border: none;
+ background: transparent;
+ text-align: inherit;
+ font-family: inherit;
+ font-size: 1em;
+ -webkit-appearance: none;
+ }
- &:nth-child(even) {
- background-color: #ddd;
- }
- &:nth-child(odd) {
- background-color: #ccc;
- }
+ .extra-metadata-item__header__remove {
+ color: #DB0030;
+
+ &:hover,
+ &:focus {
+ background: #ffe1e1;
+ color: #AD0026;
}
}
+}
+
+.extra-metadata-item__header__grab {
+ cursor: grab;
- .js-metadata-item-remove {
+ // Overlap padding-left of the title element
+ margin-#{$right}: -1em;
+ position: relative;
+ z-index: 1;
+ width: 1em;
+
+ &:before {
+ content: "";
+ display: block;
+ width: 1em;
+ height: 2px;
+ background: #000;
+ box-shadow: 0 -4px 0 0 #000, 0 4px 0 0 #000;
position: absolute;
- top: 0.25em;
- #{$right}: 0.25em;
+ top: 50%;
+ left: 1em;
+ margin-top: -1px;
+ }
+
+ .sortable-dragging & {
+ cursor: grabbing;
+ }
+}
+
+.extra-metadata-item__header__title {
+ @include flex(1 0 auto);
+ cursor: pointer;
+}
+
+.extra-metadata-item__body {
+ padding: 0 1em 1em 1em; // 0em to compensate for first label margin-top
+ border-top: 1px solid $table_border_color;
+}
+
+.extra-metadata-option {
+ margin: 0 0 1em 0;
+ padding: 0 1em;
+
+ .row {
+ @include flex-container();
+ margin: 0 -1em;
+ }
+
+ .col {
+ @include box-sizing(border-box);
+ @include flex(1 0 auto);
+ padding: 0 1em;
+ width: 50%;
+ }
+
+ .form-control {
+ margin: 0;
+ }
+
+ button {
+ border: none;
+ background: transparent;
+ text-align: inherit;
+ font-family: inherit;
+ font-size: 1em;
+ -webkit-appearance: none;
+ color: #DB0030;
+ float: right;
+ padding: 0.5em;
+ margin-top: 1em;
+ border-radius: 4px;
+
+ &:hover,
+ &:focus {
+ background: #ffe1e1;
+ color: #AD0026;
+ }
}
}
diff --git a/web/cobrands/sass/_base.scss b/web/cobrands/sass/_base.scss
index 63ff19524..8e460f102 100644
--- a/web/cobrands/sass/_base.scss
+++ b/web/cobrands/sass/_base.scss
@@ -278,7 +278,8 @@ textarea {
}
label,
-legend {
+legend,
+.label {
display: block;
margin-top: 1.25em;
margin-bottom: 0.5em;
diff --git a/web/vendor/html5sortable.min.js b/web/vendor/html5sortable.min.js
new file mode 100755
index 000000000..3a7241283
--- /dev/null
+++ b/web/vendor/html5sortable.min.js
@@ -0,0 +1,2 @@
+var sortable=function(){"use strict";function c(e,t,n){if(void 0===n)return e&&e.h5s&&e.h5s.data&&e.h5s.data[t];e.h5s=e.h5s||{},e.h5s.data=e.h5s.data||{},e.h5s.data[t]=n}var d=function(e,t){if(!(e instanceof NodeList||e instanceof HTMLCollection||e instanceof Array))throw new Error("You must provide a nodeList/HTMLCollection/Array of elements to be filtered.");return"string"!=typeof t?Array.from(e):Array.from(e).filter(function(e){return 1===e.nodeType&&e.matches(t)})},u=new Map,t=function(){function e(){this._config=new Map,this._placeholder=void 0,this._data=new Map}return Object.defineProperty(e.prototype,"config",{get:function(){var n={};return this._config.forEach(function(e,t){n[t]=e}),n},set:function(e){if("object"!=typeof e)throw new Error("You must provide a valid configuration object to the config setter.");var t=Object.assign({},e);this._config=new Map(Object.entries(t))},enumerable:!0,configurable:!0}),e.prototype.setConfig=function(e,t){if(!this._config.has(e))throw new Error("Trying to set invalid configuration item: "+e);this._config.set(e,t)},e.prototype.getConfig=function(e){if(!this._config.has(e))throw new Error("Invalid configuration item requested: "+e);return this._config.get(e)},Object.defineProperty(e.prototype,"placeholder",{get:function(){return this._placeholder},set:function(e){if(!(e instanceof HTMLElement)&&null!==e)throw new Error("A placeholder must be an html element or null.");this._placeholder=e},enumerable:!0,configurable:!0}),e.prototype.setData=function(e,t){if("string"!=typeof e)throw new Error("The key must be a string.");this._data.set(e,t)},e.prototype.getData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.get(e)},e.prototype.deleteData=function(e){if("string"!=typeof e)throw new Error("The key must be a string.");return this._data.delete(e)},e}(),p=function(e){if(!(e instanceof HTMLElement))throw new Error("Please provide a sortable to the store function.");return u.has(e)||u.set(e,new t),u.get(e)};function a(e,t,n){if(e instanceof Array)for(var r=0;r<e.length;++r)a(e[r],t,n);else e.addEventListener(t,n),p(e).setData("event"+t,n)}function i(e,t){if(e instanceof Array)for(var n=0;n<e.length;++n)i(e[n],t);else e.removeEventListener(t,p(e).getData("event"+t)),p(e).deleteData("event"+t)}function l(e,t,n){if(e instanceof Array)for(var r=0;r<e.length;++r)l(e[r],t,n);else e.setAttribute(t,n)}function r(e,t){if(e instanceof Array)for(var n=0;n<e.length;++n)r(e[n],t);else e.removeAttribute(t)}var m=function(e){if(!e.parentElement||0===e.getClientRects().length)throw new Error("target element must be part of the dom");var t=e.getClientRects()[0];return{left:t.left+window.pageXOffset,right:t.right+window.pageXOffset,top:t.top+window.pageYOffset,bottom:t.bottom+window.pageYOffset}},h=function(n,r){var o;return void 0===r&&(r=0),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];clearTimeout(o),o=setTimeout(function(){n.apply(void 0,e)},r)}},g=function(e,t){if(!(e instanceof HTMLElement&&(t instanceof NodeList||t instanceof HTMLCollection||t instanceof Array)))throw new Error("You must provide an element and a list of elements.");return Array.from(t).indexOf(e)},v=function(e){if(!(e instanceof HTMLElement))throw new Error("Element is not a node element.");return null!==e.parentNode},n=function(e,t,n){if(!(e instanceof HTMLElement&&e.parentElement instanceof HTMLElement))throw new Error("target and element must be a node");e.parentElement.insertBefore(t,"before"===n?e:e.nextElementSibling)},y=function(e,t){return n(e,t,"before")},E=function(e,t){return n(e,t,"after")},o=function(t,n,e){if(void 0===n&&(n=function(e,t){return e}),void 0===e&&(e=function(e){return e}),!(t instanceof HTMLElement)||!0==!t.isSortable)throw new Error("You need to provide a sortableContainer to be serialized.");if("function"!=typeof n||"function"!=typeof e)throw new Error("You need to provide a valid serializer for items and the container.");var r=c(t,"opts").items,o=d(t.children,r),i=o.map(function(e){return{parent:t,node:e,html:e.outerHTML,index:g(e,o)}});return{container:e({node:t,itemCount:i.length}),items:i.map(function(e){return n(e,t)})}},w=function(e,t,n){var r;if(void 0===n&&(n="sortable-placeholder"),!(e instanceof HTMLElement))throw new Error("You must provide a valid element as a sortable.");if(!(t instanceof HTMLElement)&&void 0!==t)throw new Error("You must provide a valid element as a placeholder or set ot to undefined.");return void 0===t&&(["UL","OL"].includes(e.tagName)?t=document.createElement("li"):["TABLE","TBODY"].includes(e.tagName)?(t=document.createElement("tr")).innerHTML='<td colspan="100"></td>':t=document.createElement("div")),"string"==typeof n&&(r=t.classList).add.apply(r,n.split(" ")),t},b=function(e){if(!(e instanceof HTMLElement))throw new Error("You must provide a valid dom element");var n=window.getComputedStyle(e);return["height","padding-top","padding-bottom"].map(function(e){var t=parseInt(n.getPropertyValue(e),10);return isNaN(t)?0:t}).reduce(function(e,t){return e+t})},s=function(e,t){if(!(e instanceof Array))throw new Error("You must provide a Array of HTMLElements to be filtered.");return"string"!=typeof t?e:e.filter(function(e){return e.querySelector(t)instanceof HTMLElement||e.shadowRoot&&e.shadowRoot.querySelector(t)instanceof HTMLElement}).map(function(e){return e.querySelector(t)||e.shadowRoot&&e.shadowRoot.querySelector(t)})},T=function(e){return e.composedPath&&e.composedPath()[0]||e.target},f=function(e,t,n){return{element:e,posX:n.pageX-t.left,posY:n.pageY-t.top}},L=function(e,t,n){if(!(e instanceof Event))throw new Error("setDragImage requires a DragEvent as the first argument.");if(!(t instanceof HTMLElement))throw new Error("setDragImage requires the dragged element as the second argument.");if(n||(n=f),e.dataTransfer&&e.dataTransfer.setDragImage){var r=n(t,m(t),e);if(!(r.element instanceof HTMLElement)||"number"!=typeof r.posX||"number"!=typeof r.posY)throw new Error("The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].");e.dataTransfer.effectAllowed="copyMove",e.dataTransfer.setData("text/plain",T(e).id),e.dataTransfer.setDragImage(r.element,r.posX,r.posY)}},C=function(e,t){if(!0===e.isSortable){var n=p(e).getConfig("acceptFrom");if(null!==n&&!1!==n&&"string"!=typeof n)throw new Error('HTML5Sortable: Wrong argument, "acceptFrom" must be "null", "false", or a valid selector string.');if(null!==n)return!1!==n&&0<n.split(",").filter(function(e){return 0<e.length&&t.matches(e)}).length;if(e===t)return!0;if(void 0!==p(e).getConfig("connectWith")&&null!==p(e).getConfig("connectWith"))return p(e).getConfig("connectWith")===p(t).getConfig("connectWith")}return!1},M={items:null,connectWith:null,disableIEFix:null,acceptFrom:null,copy:!1,placeholder:null,placeholderClass:"sortable-placeholder",draggingClass:"sortable-dragging",hoverClass:!1,debounce:0,throttleTime:100,maxItems:0,itemSerializer:void 0,containerSerializer:void 0,customDragImage:null};var D,x,H,A,I,S,_,Y,O=function(e,t){if("string"==typeof p(e).getConfig("hoverClass")){var o=p(e).getConfig("hoverClass").split(" ");!0===t?(a(e,"mousemove",function(r,o){var i=this;if(void 0===o&&(o=250),"function"!=typeof r)throw new Error("You must provide a function as the first argument for throttle.");if("number"!=typeof o)throw new Error("You must provide a number as the second argument for throttle.");var a=null;return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=Date.now();(null===a||o<=n-a)&&(a=n,r.apply(i,e))}}(function(r){0===r.buttons&&d(e.children,p(e).getConfig("items")).forEach(function(e){var t,n;e!==r.target?(t=e.classList).remove.apply(t,o):(n=e.classList).add.apply(n,o)})},p(e).getConfig("throttleTime"))),a(e,"mouseleave",function(){d(e.children,p(e).getConfig("items")).forEach(function(e){var t;(t=e.classList).remove.apply(t,o)})})):(i(e,"mousemove"),i(e,"mouseleave"))}},P=function(e){i(e,"dragstart"),i(e,"dragend"),i(e,"dragover"),i(e,"dragenter"),i(e,"drop"),i(e,"mouseenter"),i(e,"mouseleave")},W=function(e,t){var n=e;return!0===p(t).getConfig("copy")&&(l(n=e.cloneNode(!0),"aria-copied","true"),e.parentElement.appendChild(n),n.style.display="none",n.oldDisplay=e.style.display),n},F=function(e){var t;(t=e).h5s&&delete t.h5s.data,r(e,"aria-dropeffect")},N=function(e){r(e,"aria-grabbed"),r(e,"aria-copied"),r(e,"draggable"),r(e,"role")};function j(e,t){if(t.composedPath)return t.composedPath().find(function(e){return e.isSortable});for(;!0!==e.isSortable;)e=e.parentElement;return e}function q(e,t){var n=c(e,"opts"),r=d(e.children,n.items).filter(function(e){return e.contains(t)||e.shadowRoot&&e.shadowRoot.contains(t)});return 0<r.length?r[0]:t}var z=function(e){var t=c(e,"opts"),n=d(e.children,t.items),r=s(n,t.handle);(l(e,"aria-dropeffect","move"),c(e,"_disabled","false"),l(r,"draggable","true"),!1===t.disableIEFix)&&("function"==typeof(document||window.document).createElement("span").dragDrop&&a(r,"mousedown",function(){if(-1!==n.indexOf(this))this.dragDrop();else{for(var e=this.parentElement;-1===n.indexOf(e);)e=e.parentElement;e.dragDrop()}}))},R=function(e){var t=c(e,"opts"),n=d(e.children,t.items),r=s(n,t.handle);c(e,"_disabled","false"),P(n),i(r,"mousedown"),i(e,"dragover"),i(e,"dragenter"),i(e,"drop")};function X(e,f){var i=String(f);return f=f||{},"string"==typeof e&&(e=document.querySelectorAll(e)),e instanceof HTMLElement&&(e=[e]),e=Array.prototype.slice.call(e),/serialize/.test(i)?e.map(function(e){var t=c(e,"opts");return o(e,t.itemSerializer,t.containerSerializer)}):(e.forEach(function(s){if(/enable|disable|destroy/.test(i))return X[i](s);["connectWith","disableIEFix"].forEach(function(e){f.hasOwnProperty(e)&&null!==f[e]&&console.warn('HTML5Sortable: You are using the deprecated configuration "'+e+'". This will be removed in an upcoming version, make sure to migrate to the new options when updating.')}),f=Object.assign({},M,p(s).config,f),p(s).config=f,c(s,"opts",f),s.isSortable=!0,R(s);var e,t=d(s.children,f.items);if(null!==f.placeholder&&void 0!==f.placeholder){var n=document.createElement(s.tagName);f.placeholder instanceof HTMLElement?n.appendChild(f.placeholder):n.innerHTML=f.placeholder,e=n.children[0]}p(s).placeholder=w(s,e,f.placeholderClass),c(s,"items",f.items),f.acceptFrom?c(s,"acceptFrom",f.acceptFrom):f.connectWith&&c(s,"connectWith",f.connectWith),z(s),l(t,"role","option"),l(t,"aria-grabbed","false"),O(s,!0),a(s,"dragstart",function(e){var t=T(e);if(!0!==t.isSortable&&(e.stopImmediatePropagation(),(!f.handle||t.matches(f.handle))&&"false"!==t.getAttribute("draggable"))){var n=j(t,e),r=q(n,t);S=d(n.children,f.items),A=S.indexOf(r),I=g(r,n.children),H=n,L(e,r,f.customDragImage),x=b(r),r.classList.add(f.draggingClass),l(D=W(r,n),"aria-grabbed","true"),n.dispatchEvent(new CustomEvent("sortstart",{detail:{origin:{elementIndex:I,index:A,container:H},item:D,originalTarget:t}}))}}),a(s,"dragenter",function(e){var t=T(e),n=j(t,e);n&&n!==_&&(Y=d(n.children,c(n,"items")).filter(function(e){return e!==p(s).placeholder}),n.dispatchEvent(new CustomEvent("sortenter",{detail:{origin:{elementIndex:I,index:A,container:H},destination:{container:n,itemsBeforeUpdate:Y},item:D,originalTarget:t}}))),_=n}),a(s,"dragend",function(e){if(D){D.classList.remove(f.draggingClass),l(D,"aria-grabbed","false"),"true"===D.getAttribute("aria-copied")&&"true"!==c(D,"dropped")&&D.remove(),D.style.display=D.oldDisplay,delete D.oldDisplay;var t=Array.from(u.values()).map(function(e){return e.placeholder}).filter(function(e){return e instanceof HTMLElement}).filter(v)[0];t&&t.remove(),s.dispatchEvent(new CustomEvent("sortstop",{detail:{origin:{elementIndex:I,index:A,container:H},item:D}})),x=D=_=null}}),a(s,"drop",function(e){if(C(s,D.parentElement)){e.preventDefault(),e.stopPropagation(),c(D,"dropped","true");var t=Array.from(u.values()).map(function(e){return e.placeholder}).filter(function(e){return e instanceof HTMLElement}).filter(v)[0];E(t,D),t.remove(),s.dispatchEvent(new CustomEvent("sortstop",{detail:{origin:{elementIndex:I,index:A,container:H},item:D}}));var n=p(s).placeholder,r=d(H.children,f.items).filter(function(e){return e!==n}),o=!0===this.isSortable?this:this.parentElement,i=d(o.children,c(o,"items")).filter(function(e){return e!==n}),a=g(D,Array.from(D.parentElement.children).filter(function(e){return e!==n})),l=g(D,i);I===a&&H===o||s.dispatchEvent(new CustomEvent("sortupdate",{detail:{origin:{elementIndex:I,index:A,container:H,itemsBeforeUpdate:S,items:r},destination:{index:l,elementIndex:a,container:o,itemsBeforeUpdate:Y,items:i},item:D}}))}});var o=h(function(t,e,n){if(D)if(f.forcePlaceholderSize&&(p(t).placeholder.style.height=x+"px"),-1<Array.from(t.children).indexOf(e)){var r=b(e),o=g(p(t).placeholder,e.parentElement.children),i=g(e,e.parentElement.children);if(x<r){var a=r-x,l=m(e).top;if(o<i&&n<l)return;if(i<o&&l+r-a<n)return}void 0===D.oldDisplay&&(D.oldDisplay=D.style.display),"none"!==D.style.display&&(D.style.display="none");var s=!1;try{s=m(e).top+e.offsetHeight/2<=n}catch(e){s=o<i}s?E(e,p(t).placeholder):y(e,p(t).placeholder),Array.from(u.values()).filter(function(e){return void 0!==e.placeholder}).forEach(function(e){e.placeholder!==p(t).placeholder&&e.placeholder.remove()})}else{var c=Array.from(u.values()).filter(function(e){return void 0!==e.placeholder}).map(function(e){return e.placeholder});-1!==c.indexOf(e)||t!==e||d(e.children,f.items).length||(c.forEach(function(e){return e.remove()}),e.appendChild(p(t).placeholder))}},f.debounce),r=function(e){var t=e.target,n=!0===t.isSortable?t:j(t,e);if(t=q(n,t),D&&C(n,D.parentElement)&&"true"!==c(n,"_disabled")){var r=c(n,"opts");parseInt(r.maxItems)&&d(n.children,c(n,"items")).length>=parseInt(r.maxItems)&&D.parentElement!==n||(e.preventDefault(),e.stopPropagation(),e.dataTransfer.dropEffect=!0===p(n).getConfig("copy")?"copy":"move",o(n,t,e.pageY))}};a(t.concat(s),"dragover",r),a(t.concat(s),"dragenter",r)}),e)}return X.destroy=function(e){var t,n,r,o;n=c(t=e,"opts")||{},r=d(t.children,n.items),o=s(r,n.handle),i(t,"dragover"),i(t,"dragenter"),i(t,"drop"),F(t),i(o,"mousedown"),P(r),N(r)},X.enable=function(e){z(e)},X.disable=function(e){var t,n,r,o;n=c(t=e,"opts"),r=d(t.children,n.items),o=s(r,n.handle),l(t,"aria-dropeffect","none"),c(t,"_disabled","true"),l(o,"draggable","false"),i(o,"mousedown")},X.__testing={_data:c,_removeItemEvents:P,_removeItemData:N,_removeSortableData:F},X}();
+//# sourceMappingURL=html5sortable.min.js.map