aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/open311-populate-service-list3
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm40
-rw-r--r--perllib/FixMyStreet/Cobrand.pm10
-rw-r--r--perllib/FixMyStreet/Cobrand/Bromley.pm33
-rw-r--r--perllib/FixMyStreet/Cobrand/Greenwich.pm9
-rw-r--r--perllib/FixMyStreet/Cobrand/Oxfordshire.pm23
-rw-r--r--perllib/FixMyStreet/Cobrand/UK.pm10
-rw-r--r--perllib/FixMyStreet/Cobrand/WestBerkshire.pm16
-rw-r--r--perllib/FixMyStreet/DB/Result/Body.pm15
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm4
-rw-r--r--perllib/FixMyStreet/SendReport/Open311.pm84
-rw-r--r--t/app/sendreport/open311.t267
-rw-r--r--templates/web/base/admin/index.html6
-rw-r--r--templates/web/base/common_scripts.html8
-rw-r--r--web/cobrands/fixmystreet/admin.js (renamed from web/js/fixmystreet-admin.js)0
-rw-r--r--web/cobrands/fixmystreet/fixmystreet.js360
-rw-r--r--web/cobrands/fixmystreet/staff.js342
-rw-r--r--web/cobrands/sass/_admin.scss110
18 files changed, 847 insertions, 493 deletions
diff --git a/bin/open311-populate-service-list b/bin/open311-populate-service-list
index 211061258..8cb41a47b 100755
--- a/bin/open311-populate-service-list
+++ b/bin/open311-populate-service-list
@@ -23,7 +23,8 @@ my ($opt, $usage) = describe_options(
print($usage->text), exit if $opt->help;
my $bodies = FixMyStreet::DB->resultset('Body')->search( {
- id => { '!=', 2237 }, # XXX Until Oxfordshire does do so
+ # Until Oxfordshire does, and Bristol stops erroring
+ name => { -not_in => [ 'Oxfordshire County Council', 'Bristol City Council' ] },
send_method => 'Open311'
} );
my $verbose = 0;
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index a7b9fb169..87a4191af 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -789,11 +789,9 @@ sub report_edit : Path('report_edit') : Args(1) {
$c->forward( '/admin/report_edit_category', [ $problem ] );
- if ( $c->get_param('email') ne $problem->user->email ) {
- my $user = $c->model('DB::User')->find_or_create(
- { email => $c->get_param('email') }
- );
-
+ my $email = lc $c->get_param('email');
+ if ( $email ne $problem->user->email ) {
+ my $user = $c->model('DB::User')->find_or_create({ email => $email });
$user->insert unless $user->in_storage;
$problem->user( $user );
}
@@ -1117,8 +1115,9 @@ sub update_edit : Path('update_edit') : Args(1) {
# $update->name can be null which makes ne unhappy
my $name = $update->name || '';
+ my $email = lc $c->get_param('email');
if ( $c->get_param('name') ne $name
- || $c->get_param('email') ne $update->user->email
+ || $email ne $update->user->email
|| $c->get_param('anonymous') ne $update->anonymous
|| $c->get_param('text') ne $update->text ) {
$edited = 1;
@@ -1138,11 +1137,8 @@ sub update_edit : Path('update_edit') : Args(1) {
$update->anonymous( $c->get_param('anonymous') );
$update->state( $new_state );
- if ( $c->get_param('email') ne $update->user->email ) {
- my $user =
- $c->model('DB::User')
- ->find_or_create( { email => $c->get_param('email') } );
-
+ if ( $email ne $update->user->email ) {
+ my $user = $c->model('DB::User')->find_or_create({ email => $email });
$user->insert unless $user->in_storage;
$update->user($user);
}
@@ -1207,7 +1203,7 @@ sub user_add : Path('user_edit') : Args(0) {
my $user = $c->model('DB::User')->find_or_create( {
name => $c->get_param('name'),
- email => $c->get_param('email'),
+ email => lc $c->get_param('email'),
phone => $c->get_param('phone') || undef,
from_body => $c->get_param('body') || undef,
flagged => $c->get_param('flagged') || 0,
@@ -1257,7 +1253,8 @@ sub user_edit : Path('user_edit') : Args(1) {
my $edited = 0;
- if ( $user->email ne $c->get_param('email') ||
+ my $email = lc $c->get_param('email');
+ if ( $user->email ne $email ||
$user->name ne $c->get_param('name') ||
($user->phone || "") ne $c->get_param('phone') ||
($user->from_body && $c->get_param('body') && $user->from_body->id ne $c->get_param('body')) ||
@@ -1267,7 +1264,8 @@ sub user_edit : Path('user_edit') : Args(1) {
}
$user->name( $c->get_param('name') );
- $user->email( $c->get_param('email') );
+ my $original_email = $user->email;
+ $user->email( $email );
$user->phone( $c->get_param('phone') ) if $c->get_param('phone');
$user->flagged( $c->get_param('flagged') || 0 );
# Only superusers can grant superuser status
@@ -1368,11 +1366,17 @@ sub user_edit : Path('user_edit') : Args(1) {
return if %{$c->stash->{field_errors}};
my $existing_user = $c->model('DB::User')->search({ email => $user->email, id => { '!=', $user->id } })->first;
- if ($existing_user) {
+ my $existing_user_cobrand = $c->cobrand->users->search({ email => $user->email, id => { '!=', $user->id } })->first;
+ if ($existing_user_cobrand) {
$existing_user->adopt($user);
$c->forward( 'log_edit', [ $id, 'user', 'merge' ] );
$c->res->redirect( $c->uri_for( 'user_edit', $existing_user->id ) );
} else {
+ if ($existing_user) {
+ # Tried to change email to an existing one lacking permission
+ # so make sure it's switched back
+ $user->email($original_email);
+ }
$user->update;
if ($edited) {
$c->forward( 'log_edit', [ $id, 'user', 'edit' ] );
@@ -1627,7 +1631,7 @@ accordingly
sub ban_user : Private {
my ( $self, $c ) = @_;
- my $email = $c->get_param('email');
+ my $email = lc $c->get_param('email');
return unless $email;
@@ -1654,7 +1658,7 @@ Sets the flag on a user with the given email
sub flag_user : Private {
my ( $self, $c ) = @_;
- my $email = $c->get_param('email');
+ my $email = lc $c->get_param('email');
return unless $email;
@@ -1682,7 +1686,7 @@ Remove the flag on a user with the given email
sub remove_user_flag : Private {
my ( $self, $c ) = @_;
- my $email = $c->get_param('email');
+ my $email = lc $c->get_param('email');
return unless $email;
diff --git a/perllib/FixMyStreet/Cobrand.pm b/perllib/FixMyStreet/Cobrand.pm
index 9f61635d8..4b9f2bd0b 100644
--- a/perllib/FixMyStreet/Cobrand.pm
+++ b/perllib/FixMyStreet/Cobrand.pm
@@ -153,4 +153,14 @@ sub exists {
return 0;
}
+sub body_handler {
+ my ($class, $areas) = @_;
+
+ foreach my $avail ( $class->available_cobrand_classes ) {
+ my $cobrand = $class->get_class_for_moniker($avail->{moniker})->new({});
+ next unless $cobrand->can('council_id');
+ return $cobrand if $areas->{$cobrand->council_id};
+ }
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm
index 2d0cb86f1..169175947 100644
--- a/perllib/FixMyStreet/Cobrand/Bromley.pm
+++ b/perllib/FixMyStreet/Cobrand/Bromley.pm
@@ -3,6 +3,7 @@ use parent 'FixMyStreet::Cobrand::UKCouncils';
use strict;
use warnings;
+use DateTime::Format::W3CDTF;
sub council_id { return 2482; }
sub council_area { return 'Bromley'; }
@@ -111,5 +112,37 @@ sub title_list {
return ["MR", "MISS", "MRS", "MS", "DR"];
}
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ my $extra = $row->get_extra_fields;
+ push @$extra,
+ { name => 'report_url',
+ value => $h->{url} },
+ { name => 'report_title',
+ value => $row->title },
+ { name => 'public_anonymity_required',
+ value => $row->anonymous ? 'TRUE' : 'FALSE' },
+ { name => 'email_alerts_requested',
+ value => 'FALSE' }, # always false as can never request them
+ { name => 'requested_datetime',
+ value => DateTime::Format::W3CDTF->format_datetime($row->confirmed->set_nanosecond(0)) },
+ { name => 'email',
+ value => $row->user->email };
+
+ # make sure we have last_name attribute present in row's extra, so
+ # it is passed correctly to Bromley as attribute[]
+ if ( $row->cobrand ne 'bromley' ) {
+ my ( $firstname, $lastname ) = ( $row->name =~ /(\w+)\.?\s+(.+)/ );
+ push @$extra, { name => 'last_name', value => $lastname };
+ }
+
+ $row->set_extra_fields(@$extra);
+
+ $params->{always_send_latlong} = 0;
+ $params->{send_notpinpointed} = 1;
+ $params->{extended_description} = 0;
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Greenwich.pm b/perllib/FixMyStreet/Cobrand/Greenwich.pm
index 7d6058145..700a12782 100644
--- a/perllib/FixMyStreet/Cobrand/Greenwich.pm
+++ b/perllib/FixMyStreet/Cobrand/Greenwich.pm
@@ -61,4 +61,13 @@ sub on_map_default_max_pin_age {
return '21 days';
}
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ my $extra = $row->get_extra_fields;
+ # Greenwich doesn't have category metadata to fill this
+ push @$extra, { name => 'external_id', value => $row->id };
+ $row->set_extra_fields( @$extra );
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
index 2820719b9..d585a5328 100644
--- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
@@ -112,6 +112,29 @@ sub pin_colour {
return 'yellow';
}
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ my $extra = $row->get_extra_fields;
+ push @$extra, { name => 'external_id', value => $row->id };
+
+ if ($h->{closest_address}) {
+ push @$extra, { name => 'closest_address', value => $h->{closest_address} }
+ }
+ if ( $row->used_map || ( !$row->used_map && !$row->postcode ) ) {
+ push @$extra, { name => 'northing', value => $h->{northing} };
+ push @$extra, { name => 'easting', value => $h->{easting} };
+ }
+ $row->set_extra_fields( @$extra );
+
+ $params->{extended_description} = 'oxfordshire';
+}
+
+sub open311_pre_send {
+ my ($self, $row, $open311) = @_;
+ $open311->endpoints( { requests => 'open311_service_request.cgi' } );
+}
+
sub on_map_default_status { return 'open'; }
sub contact_email {
diff --git a/perllib/FixMyStreet/Cobrand/UK.pm b/perllib/FixMyStreet/Cobrand/UK.pm
index 08ecf0b7d..945af48f8 100644
--- a/perllib/FixMyStreet/Cobrand/UK.pm
+++ b/perllib/FixMyStreet/Cobrand/UK.pm
@@ -1,5 +1,6 @@
package FixMyStreet::Cobrand::UK;
use base 'FixMyStreet::Cobrand::Default';
+use strict;
use JSON::MaybeXS;
use mySociety::MaPit;
@@ -354,13 +355,8 @@ sub get_body_handler_for_problem {
my @bodies = values %{$row->bodies};
my %areas = map { %{$_->areas} } @bodies;
- foreach my $avail ( FixMyStreet::Cobrand->available_cobrand_classes ) {
- my $class = FixMyStreet::Cobrand->get_class_for_moniker($avail->{moniker});
- my $cobrand = $class->new({});
- next unless $cobrand->can('council_id');
- return $cobrand if $areas{$cobrand->council_id};
- }
-
+ my $cobrand = FixMyStreet::Cobrand->body_handler(\%areas);
+ return $cobrand if $cobrand;
return ref $self ? $self : $self->new;
}
diff --git a/perllib/FixMyStreet/Cobrand/WestBerkshire.pm b/perllib/FixMyStreet/Cobrand/WestBerkshire.pm
new file mode 100644
index 000000000..1ffdf0286
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/WestBerkshire.pm
@@ -0,0 +1,16 @@
+package FixMyStreet::Cobrand::WestBerkshire;
+use base 'FixMyStreet::Cobrand::UKCouncils';
+
+use strict;
+use warnings;
+
+sub council_id { 2619 }
+
+# non standard west berks end points
+sub open311_pre_send {
+ my ($self, $row, $open311) = @_;
+ $open311->endpoints( { services => 'Services', requests => 'Requests' } );
+}
+
+1;
+
diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm
index 037b69352..6dac8821c 100644
--- a/perllib/FixMyStreet/DB/Result/Body.pm
+++ b/perllib/FixMyStreet/DB/Result/Body.pm
@@ -127,4 +127,19 @@ sub areas {
return \%ids;
}
+=head2 get_cobrand_handler
+
+Get a cobrand object for this body, if there is one.
+
+e.g.
+ * if the problem was sent to Bromley it will return ::Bromley
+ * if the problem was sent to Camden it will return nothing
+
+=cut
+
+sub get_cobrand_handler {
+ my $self = shift;
+ return FixMyStreet::Cobrand->body_handler($self->areas);
+}
+
1;
diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm
index 4ccad3690..dcd5ecc71 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -1107,9 +1107,7 @@ has traffic_management_options => (
default => sub {
my $self = shift;
my $cobrand = $self->get_cobrand_logged;
- if ( $cobrand->can('get_body_handler_for_problem') ) {
- $cobrand = $cobrand->get_body_handler_for_problem( $self );
- }
+ $cobrand = $cobrand->call_hook(get_body_handler_for_problem => $self) || $cobrand;
return $cobrand->traffic_management_options;
},
);
diff --git a/perllib/FixMyStreet/SendReport/Open311.pm b/perllib/FixMyStreet/SendReport/Open311.pm
index 9c55683ed..059690612 100644
--- a/perllib/FixMyStreet/SendReport/Open311.pm
+++ b/perllib/FixMyStreet/SendReport/Open311.pm
@@ -5,14 +5,7 @@ use namespace::autoclean;
BEGIN { extends 'FixMyStreet::SendReport'; }
-use DateTime::Format::W3CDTF;
use Open311;
-use Readonly;
-
-Readonly::Scalar my $COUNCIL_ID_OXFORDSHIRE => 2237;
-Readonly::Scalar my $COUNCIL_ID_WARWICKSHIRE => 2243;
-Readonly::Scalar my $COUNCIL_ID_GREENWICH => 2493;
-Readonly::Scalar my $COUNCIL_ID_BROMLEY => 2482;
has open311_test_req_used => (
is => 'rw',
@@ -27,47 +20,18 @@ sub send {
foreach my $body ( @{ $self->bodies } ) {
my $conf = $self->body_config->{ $body->id };
- my $always_send_latlong = 1;
- my $send_notpinpointed = 0;
- my $use_service_as_deviceid = 0;
-
- my $extended_desc = 1;
-
- my $extra = $row->get_extra_fields();
+ my %open311_params = (
+ jurisdiction => $conf->jurisdiction,
+ endpoint => $conf->endpoint,
+ api_key => $conf->api_key,
+ always_send_latlong => 1,
+ send_notpinpointed => 0,
+ use_service_as_deviceid => 0,
+ extended_description => 1,
+ );
- # Extra bromley fields
- if ( $row->bodies_str eq $COUNCIL_ID_BROMLEY ) {
- push @$extra, { name => 'report_url', value => $h->{url} };
- push @$extra, { name => 'report_title', value => $row->title };
- push @$extra, { name => 'public_anonymity_required', value => $row->anonymous ? 'TRUE' : 'FALSE' };
- push @$extra, { name => 'email_alerts_requested', value => 'FALSE' }; # always false as can never request them
- push @$extra, { name => 'requested_datetime', value => DateTime::Format::W3CDTF->format_datetime($row->confirmed->set_nanosecond(0)) };
- push @$extra, { name => 'email', value => $row->user->email };
- # make sure we have last_name attribute present in row's extra, so
- # it is passed correctly to Bromley as attribute[]
- if ( $row->cobrand ne 'bromley' ) {
- my ( $firstname, $lastname ) = ( $row->name =~ /(\w+)\.?\s+(.+)/ );
- push @$extra, { name => 'last_name', value => $lastname };
- }
- $always_send_latlong = 0;
- $send_notpinpointed = 1;
- $extended_desc = 0;
- } elsif ( $row->bodies_str =~ /\b$COUNCIL_ID_OXFORDSHIRE\b/ ) {
- # Oxfordshire doesn't have category metadata to fill these
- $extended_desc = 'oxfordshire';
- push @$extra, { name => 'external_id', value => $row->id };
- push @$extra, { name => 'closest_address', value => $h->{closest_address} } if $h->{closest_address};
- if ( $row->used_map || ( !$row->used_map && !$row->postcode ) ) {
- push @$extra, { name => 'northing', value => $h->{northing} };
- push @$extra, { name => 'easting', value => $h->{easting} };
- }
- } elsif ( $row->bodies_str =~ /\b$COUNCIL_ID_WARWICKSHIRE\b/ ) {
- $extended_desc = 'warwickshire';
- push @$extra, { name => 'closest_address', value => $h->{closest_address} } if $h->{closest_address};
- } elsif ( $row->bodies_str == $COUNCIL_ID_GREENWICH ) {
- # Greenwich doesn't have category metadata to fill this
- push @$extra, { name => 'external_id', value => $row->id };
- }
+ my $cobrand = $body->get_cobrand_handler || $row->get_cobrand_logged;
+ $cobrand->call_hook(open311_config => $row, $h, \%open311_params);
# Try and fill in some ones that we've been asked for, but not asked the user for
@@ -77,6 +41,8 @@ sub send {
category => $row->category
} );
+ my $extra = $row->get_extra_fields();
+
my $id_field = $contact->id_field;
foreach (@{$contact->get_extra_fields}) {
if ($_->{code} eq $id_field) {
@@ -92,15 +58,6 @@ sub send {
$row->set_extra_fields( @$extra ) if @$extra;
- my %open311_params = (
- jurisdiction => $conf->jurisdiction,
- endpoint => $conf->endpoint,
- api_key => $conf->api_key,
- always_send_latlong => $always_send_latlong,
- send_notpinpointed => $send_notpinpointed,
- use_service_as_deviceid => $use_service_as_deviceid,
- extended_description => $extended_desc,
- );
if (FixMyStreet->test_mode) {
my $test_res = HTTP::Response->new();
$test_res->code(200);
@@ -112,20 +69,7 @@ sub send {
my $open311 = Open311->new( %open311_params );
- # non standard west berks end points
- if ( $row->bodies_str =~ /2619/ ) {
- $open311->endpoints( { services => 'Services', requests => 'Requests' } );
- }
-
- # non-standard Oxfordshire endpoint (because it's just a script, not a full Open311 service)
- if ( $row->bodies_str =~ /$COUNCIL_ID_OXFORDSHIRE/ ) {
- $open311->endpoints( { requests => 'open311_service_request.cgi' } );
- }
-
- # required to get round issues with CRM constraints
- if ( $row->bodies_str =~ /2218/ ) {
- $row->user->name( $row->user->id . ' ' . $row->user->name );
- }
+ $cobrand->call_hook(open311_pre_send => $row, $open311);
my $resp = $open311->send_service_request( $row, $h, $contact->email );
if (FixMyStreet->test_mode) {
diff --git a/t/app/sendreport/open311.t b/t/app/sendreport/open311.t
new file mode 100644
index 000000000..c4c17577c
--- /dev/null
+++ b/t/app/sendreport/open311.t
@@ -0,0 +1,267 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Deep;
+
+use Open311;
+use FixMyStreet::SendReport::Open311;
+use FixMyStreet::DB;
+
+use Data::Dumper;
+
+package main;
+sub test_overrides; # defined below
+
+use constant TEST_USER_EMAIL => 'fred@example.com';
+
+my %standard_open311_parameters = (
+ 'use_extended_updates' => 0,
+ 'send_notpinpointed' => 0,
+ 'extended_description' => 1,
+ 'use_service_as_deviceid' => 0,
+ 'extended_statuses' => 0,
+ 'always_send_latlong' => 1,
+ 'debug' => 0,
+ 'error' => '',
+ 'endpoints' => {
+ 'requests' => 'requests.xml',
+ 'service_request_updates' => 'servicerequestupdates.xml',
+ 'services' => 'services.xml',
+ 'update' => 'servicerequestupdates.xml',
+ },
+);
+
+test_overrides oxfordshire =>
+ {
+ body_name => 'Oxfordshire',
+ body_id => 2237,
+ row_data => {
+ postcode => 'OX1 1AA',
+ },
+ extra => {
+ northing => 100,
+ easting => 100,
+ closest_address => '49 St Giles',
+ },
+ },
+ superhashof({
+ handler => isa('FixMyStreet::Cobrand::Oxfordshire'),
+ discard_changes => 1,
+ 'open311' => noclass(superhashof({
+ %standard_open311_parameters,
+ 'extended_description' => 'oxfordshire',
+ 'endpoints' => {
+ 'requests' => 'open311_service_request.cgi'
+ },
+ })),
+ problem_extra => bag(
+ { name => 'northing', value => 100 },
+ { name => 'easting', value => 100 },
+ { name => 'closest_address' => value => '49 St Giles' },
+ { name => 'external_id', value => re('[0-9]+') },
+ ),
+ });
+
+my $bromley_check =
+ superhashof({
+ handler => isa('FixMyStreet::Cobrand::Bromley'),
+ discard_changes => 1,
+ 'open311' => noclass(superhashof({
+ %standard_open311_parameters,
+ 'send_notpinpointed' => 1,
+ 'extended_description' => 0,
+ 'use_service_as_deviceid' => 0,
+ 'always_send_latlong' => 0,
+ })),
+ problem_extra => bag(
+ { name => 'report_url' => value => 'http://example.com/1234' },
+ { name => 'report_title', value => 'Problem' },
+ { name => 'public_anonymity_required', value => 'TRUE' },
+ { name => 'email_alerts_requested', value => 'FALSE' },
+ { name => 'requested_datetime', value => re(qr/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)/) },
+ { name => 'email', value => TEST_USER_EMAIL },
+ { name => 'last_name', value => 'Bloggs' },
+ ),
+ });
+
+test_overrides bromley =>
+ {
+ body_name => 'Bromley',
+ body_id => 2482,
+ row_data => {
+ postcode => 'BR1 1AA',
+ extra => [ { name => 'last_name', value => 'Bloggs' } ],
+ },
+ extra => {
+ northing => 100,
+ easting => 100,
+ url => 'http://example.com/1234',
+ },
+ },
+ $bromley_check;
+
+test_overrides fixmystreet =>
+ {
+ body_name => 'Bromley',
+ body_id => 2482,
+ row_data => {
+ postcode => 'BR1 1AA',
+ # NB: we don't pass last_name here, as main cobrand doesn't know to do this!
+ },
+ extra => {
+ northing => 100,
+ easting => 100,
+ url => 'http://example.com/1234',
+ },
+ },
+ $bromley_check;
+
+test_overrides greenwich =>
+ {
+ body_name => 'Greenwich',
+ body_id => 2493,
+ },
+ superhashof({
+ handler => isa('FixMyStreet::Cobrand::Greenwich'),
+ 'open311' => noclass(superhashof({
+ %standard_open311_parameters,
+ })),
+ problem_extra => bag(
+ { name => 'external_id', value => re('[0-9]+') },
+ ),
+ });
+
+test_overrides fixmystreet =>
+ {
+ body_name => 'West Berkshire',
+ body_id => 2619,
+ row_data => {
+ postcode => 'RG1 1AA',
+ },
+ },
+ superhashof({
+ handler => isa('FixMyStreet::Cobrand::WestBerkshire'),
+ 'open311' => noclass(superhashof({
+ %standard_open311_parameters,
+ 'endpoints' => {
+ 'requests' => 'Requests',
+ 'services' => 'Services',
+ },
+ })),
+ });
+
+sub test_overrides {
+ # NB: Open311 and ::SendReport::Open311 are mocked below in BEGIN { ... }
+ my ($cobrand, $input, $expected_data) = @_;
+ subtest "$cobrand ($input->{body_name}) overrides" => sub {
+
+ FixMyStreet::override_config {
+ ALLOWED_COBRANDS => ['fixmystreet', 'oxfordshire', 'bromley', 'westberkshire', 'greenwich'],
+ }, sub {
+ my $db = FixMyStreet::DB->storage->schema;
+ $db->txn_begin;
+
+ my $params = { id => $input->{body_id}, name => $input->{body_name} };
+ my $body = $db->resultset('Body')->find_or_create($params);
+ $body->body_areas->create({ area_id => $input->{body_id} });
+ ok $body, "found/created body " . $input->{body_name};
+ $body->update({ can_be_devolved => 1 });
+
+ my $contact = $body->contacts->find_or_create(
+ confirmed => 1,
+ email => 'ZZ',
+ category => 'ZZ',
+ deleted => 0,
+ editor => 'test suite',
+ note => '',
+ whenedited => DateTime->now,
+ jurisdiction => '1234',
+ api_key => 'SEEKRIT',
+ body_id => $input->{body_id},
+ );
+ $contact->update({ send_method => 'Open311', endpoint => 'http://example.com/open311' });
+
+ my $user = $db->resultset('User')->create( {
+ name => 'Fred Bloggs',
+ email => TEST_USER_EMAIL,
+ password => 'dummy',
+ });
+
+ my $row = $db->resultset('Problem')->create( {
+ title => 'Problem',
+ detail => 'A big problem',
+ used_map => 1,
+ name => 'Fred Bloggs',
+ anonymous => 1,
+ state => 'unconfirmed',
+ bodies_str => $input->{body_id},
+ areas => (sprintf ',%d,', $input->{body_id}),
+ category => 'ZZ',
+ cobrand => $cobrand,
+ user => $user,
+ postcode => 'ZZ1 1AA',
+ latitude => 100,
+ longitude => 100,
+ confirmed => DateTime->now(),
+ %{ $input->{row_data} || {} },
+ } );
+
+ my $sr = FixMyStreet::SendReport::Open311->new;
+ $sr->add_body($body, $contact);
+ $sr->send( $row, $input->{extra} || {} );
+
+ cmp_deeply (Open311->_get_test_data, $expected_data, 'Data as expected')
+ or diag Dumper( Open311->_get_test_data );
+
+ Open311->_reset_test_data();
+ $db->txn_rollback;
+ };
+ }
+}
+
+BEGIN {
+ # Prepare the %data variable to write data from Open311 calls to...
+ my %data;
+ package Open311;
+ use Class::Method::Modifiers;
+ around new => sub {
+ my $orig = shift;
+ my ($class, %params) = @_;
+ my $self = $class->$orig(%params);
+ $data{open311} = $self;
+ $self;
+ };
+ around send_service_request => sub {
+ my $orig = shift;
+ my ($self, $problem, $extra, $service_code) = @_;
+ $data{problem} = { $problem->get_columns };
+ $data{extra} = $extra;
+ $data{problem_extra} = $problem->get_extra_fields;
+ $data{problem_user} = { $problem->user->get_columns };
+ $data{service_code} = $service_code;
+ # don't actually send the service request!
+ };
+
+ sub _get_test_data { return +{ %data } }
+ sub _reset_test_data { %data = () }
+
+ package FixMyStreet::DB::Result::Problem;
+ use Class::Method::Modifiers; # is marked as immutable by Moose
+ sub discard_changes {
+ $data{discard_changes}++;
+ # no need to actually discard, as we're in transaction anyway
+ };
+
+ package FixMyStreet::DB::Result::Body;
+ use Class::Method::Modifiers; # is marked as immutable by Moose
+ around get_cobrand_handler => sub {
+ my $orig = shift;
+ my ($self) = @_;
+ my $handler = $self->$orig();
+ $data{handler} = $handler;
+ $handler;
+ };
+}
+
+done_testing();
diff --git a/templates/web/base/admin/index.html b/templates/web/base/admin/index.html
index f573f0e7a..8498055b1 100644
--- a/templates/web/base/admin/index.html
+++ b/templates/web/base/admin/index.html
@@ -20,14 +20,18 @@ and to receive notices of updates.
</p>
[% END %]
+<div class="admin-index-search form-txt-submit-box clearfix">
+
<form method="get" action="[% c.uri_for('reports') %]" accept-charset="utf-8">
<p><label for="search_reports">[% loc('Search Reports') %]</label>
<input type="text" class="form-control" name="search" size="30" id="search_reports" value="[% searched | html %]">
+ <input type="submit" class="btn" value="[% loc('Go') %]">
</form>
<form method="get" action="[% c.uri_for('users') %]" accept-charset="utf-8">
<p><label for="search_users">[% loc('Search Users') %]</label>
<input type="text" class="form-control" name="search" size="30" id="search_users" value="[% searched | html %]">
+ <input type="submit" class="btn" value="[% loc('Go') %]">
</form>
[% IF c.user.is_superuser %]
@@ -46,6 +50,8 @@ and to receive notices of updates.
</form>
[% END %]
+</div>
+
[% IF unsent_reports.size %]
<h2>[% loc('Reports waiting to be sent') %]</h2>
diff --git a/templates/web/base/common_scripts.html b/templates/web/base/common_scripts.html
index 1d53f1d51..42c04f11f 100644
--- a/templates/web/base/common_scripts.html
+++ b/templates/web/base/common_scripts.html
@@ -16,6 +16,12 @@ scripts.push(
version('/cobrands/fixmystreet/fixmystreet.js'),
);
+IF c.user_exists AND (c.user.from_body OR c.user.is_superuser);
+ scripts.push(
+ version('/cobrands/fixmystreet/staff.js')
+ );
+END;
+
FOR script IN map_js;
scripts.push(script);
END;
@@ -28,7 +34,7 @@ scripts.push(
IF admin;
scripts.push(
version('/js/jquery-ui/js/jquery-ui-1.10.3.custom.min.js'),
- version('/js/fixmystreet-admin.js'),
+ version('/cobrands/fixmystreet/admin.js'),
);
END;
diff --git a/web/js/fixmystreet-admin.js b/web/cobrands/fixmystreet/admin.js
index 02eb30766..02eb30766 100644
--- a/web/js/fixmystreet-admin.js
+++ b/web/cobrands/fixmystreet/admin.js
diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js
index 6480f47f5..aa89f8115 100644
--- a/web/cobrands/fixmystreet/fixmystreet.js
+++ b/web/cobrands/fixmystreet/fixmystreet.js
@@ -222,6 +222,14 @@ fixmystreet.update_list_item_buttons = function($list) {
$list.children(':last-child').find('[name="shortlist-down"]').prop('disabled', true);
};
+// A tiny helper to call a function only if it exists (so we can
+// call this with staff-only functions and they won't error).
+fixmystreet.run = function(fn) {
+ if (fn) {
+ fn.call(this);
+ }
+};
+
fixmystreet.set_up = fixmystreet.set_up || {};
$.extend(fixmystreet.set_up, {
basics: function() {
@@ -406,199 +414,6 @@ $.extend(fixmystreet.set_up, {
});
},
- manage_duplicates: function() {
- // Deal with changes to report state by inspector/other staff, specifically
- // displaying nearby reports if it's changed to 'duplicate'.
- function refresh_duplicate_list() {
- var report_id = $("#report_inspect_form .js-report-id").text();
- var args = {
- filter_category: $("#report_inspect_form select#category").val(),
- latitude: $('input[name="latitude"]').val(),
- longitude: $('input[name="longitude"]').val()
- };
- $("#js-duplicate-reports ul").html("<li>Loading...</li>");
- var nearby_url = '/report/'+report_id+'/nearby.json';
- $.getJSON(nearby_url, args, function(data) {
- var duplicate_of = $("#report_inspect_form [name=duplicate_of]").val();
- var $reports = $(data.current)
- .filter("li")
- .not("[data-report-id="+report_id+"]")
- .slice(0, 5);
- $reports.filter("[data-report-id="+duplicate_of+"]").addClass("item-list--reports__item--selected");
-
- (function() {
- var timeout;
- $reports.on('mouseenter', function(){
- clearTimeout(timeout);
- fixmystreet.maps.markers_highlight(parseInt($(this).data('reportId'), 10));
- }).on('mouseleave', function(){
- timeout = setTimeout(fixmystreet.maps.markers_highlight, 50);
- });
- })();
-
- $("#js-duplicate-reports ul").empty().prepend($reports);
-
- $reports.find("a").click(function() {
- var report_id = $(this).closest("li").data('reportId');
- $("#report_inspect_form [name=duplicate_of]").val(report_id);
- $("#js-duplicate-reports ul li").removeClass("item-list--reports__item--selected");
- $(this).closest("li").addClass("item-list--reports__item--selected");
- return false;
- });
-
- show_nearby_pins(data, report_id);
- });
- }
-
- function show_nearby_pins(data, report_id) {
- var markers = fixmystreet.maps.markers_list( data.pins, true );
- // We're replacing all the features in the markers layer with the
- // possible duplicates, but the list of pins from the server doesn't
- // include the current report. So we need to extract the feature for
- // the current report and include it in the list of features we're
- // showing on the layer.
- var report_marker = fixmystreet.maps.get_marker_by_id(parseInt(report_id, 10));
- if (report_marker) {
- markers.unshift(report_marker);
- }
- fixmystreet.markers.removeAllFeatures();
- fixmystreet.markers.addFeatures( markers );
- }
-
- function state_change() {
- // The duplicate report list only makes sense when state is 'duplicate'
- if ($(this).val() !== "duplicate") {
- $("#js-duplicate-reports").addClass("hidden");
- return;
- } else {
- $("#js-duplicate-reports").removeClass("hidden");
- }
- // If this report is already marked as a duplicate of another, then
- // there's no need to refresh the list of duplicate reports
- var duplicate_of = $("#report_inspect_form [name=duplicate_of]").val();
- if (!!duplicate_of) {
- return;
- }
-
- refresh_duplicate_list();
- }
-
- $("#report_inspect_form").on("change.state", "select#state", state_change);
- $("#js-change-duplicate-report").click(refresh_duplicate_list);
- },
-
- list_item_actions: function() {
- function toggle_shortlist(btn, sw, id) {
- btn.attr('class', 'item-list__item__shortlist-' + sw);
- btn.attr('title', btn.data('label-' + sw));
- if (id) {
- sw += '-' + id;
- }
- btn.attr('name', 'shortlist-' + sw);
- }
-
- $('.item-list--reports').on('click', ':submit', function(e) {
- e.preventDefault();
-
- var $submitButton = $(this);
- var whatUserWants = $submitButton.prop('name');
- var data;
- var $item;
- var $list;
- var $hiddenInput;
- var report_id;
- if (fixmystreet.page === 'around') {
- // Deal differently because one big form
- var parts = whatUserWants.split('-');
- whatUserWants = parts[0] + '-' + parts[1];
- report_id = parts[2];
- var token = $('[name=token]').val();
- data = whatUserWants + '=1&token=' + token + '&id=' + report_id;
- } else {
- var $form = $(this).parents('form');
- $item = $form.parent('.item-list__item');
- $list = $item.parent('.item-list');
-
- // The server expects to be told which button/input triggered the form
- // submission. But $form.serialize() doesn't know that. So we inject a
- // hidden input into the form, that can pass the name and value of the
- // submit button to the server, as it expects.
- $hiddenInput = $('<input>').attr({
- type: 'hidden',
- name: whatUserWants,
- value: $submitButton.prop('value')
- }).appendTo($form);
- data = $form.serialize() + '&ajax=1';
- }
-
- // Update UI while the ajax request is sent in the background.
- if ('shortlist-down' === whatUserWants) {
- $item.insertAfter( $item.next() );
- } else if ('shortlist-up' === whatUserWants) {
- $item.insertBefore( $item.prev() );
- } else if ('shortlist-remove' === whatUserWants) {
- toggle_shortlist($submitButton, 'add', report_id);
- } else if ('shortlist-add' === whatUserWants) {
- toggle_shortlist($submitButton, 'remove', report_id);
- }
-
- // Items have moved around. We need to make sure the "up" button on the
- // first item, and the "down" button on the last item, are disabled.
- fixmystreet.update_list_item_buttons($list);
-
- $.ajax({
- url: '/my/planned/change',
- type: 'POST',
- data: data
- }).fail(function() {
- // Undo the UI changes we made.
- if ('shortlist-down' === whatUserWants) {
- $item.insertBefore( $item.prev() );
- } else if ('shortlist-up' === whatUserWants) {
- $item.insertAfter( $item.next() );
- } else if ('shortlist-remove' === whatUserWants) {
- toggle_shortlist($submitButton, 'remove', report_id);
- } else if ('shortlist-add' === whatUserWants) {
- toggle_shortlist($submitButton, 'add', report_id);
- }
- fixmystreet.update_list_item_buttons($list);
- }).complete(function() {
- if ($hiddenInput) {
- $hiddenInput.remove();
- }
- });
- });
- },
-
- contribute_as: function() {
- $('.content').on('change', '.js-contribute-as', function(){
- var opt = this.options[this.selectedIndex],
- val = opt.value,
- txt = opt.text;
- var $emailInput = $('input[name=email]').add('input[name=rznvy]');
- var $nameInput = $('input[name=name]');
- var $showNameCheckbox = $('input[name=may_show_name]');
- var $addAlertCheckbox = $('#form_add_alert');
- if (val === 'myself') {
- $emailInput.val($emailInput.prop('defaultValue')).prop('disabled', true);
- $nameInput.val($nameInput.prop('defaultValue')).prop('disabled', false);
- $showNameCheckbox.prop('checked', false).prop('disabled', false);
- $addAlertCheckbox.prop('checked', true).prop('disabled', false);
- } else if (val === 'another_user') {
- $emailInput.val('').prop('disabled', false);
- $nameInput.val('').prop('disabled', false);
- $showNameCheckbox.prop('checked', false).prop('disabled', true);
- $addAlertCheckbox.prop('checked', true).prop('disabled', false);
- } else if (val === 'body') {
- $emailInput.val('-').prop('disabled', true);
- $nameInput.val(txt).prop('disabled', true);
- $showNameCheckbox.prop('checked', true).prop('disabled', true);
- $addAlertCheckbox.prop('checked', false).prop('disabled', true);
- }
- });
- $('.js-contribute-as').change();
- },
-
on_resize: function() {
var last_type;
$(window).on('resize', function() {
@@ -738,86 +553,6 @@ $.extend(fixmystreet.set_up, {
make_multi('filter_categories');
},
- report_page_inspect: function() {
- if (!$('form#report_inspect_form').length) {
- return;
- }
-
- // Focus on form
- $('html,body').scrollTop($('#report_inspect_form').offset().top);
-
- // On the manage/inspect report form, we already have all the extra inputs
- // in the DOM, we just need to hide/show them as appropriate.
- $('form#report_inspect_form [name=category]').change(function() {
- var category = $(this).val(),
- selector = "[data-category='" + category + "']";
- $("form#report_inspect_form [data-category]:not(" + selector + ")").addClass("hidden");
- $("form#report_inspect_form " + selector).removeClass("hidden");
- // And update the associated priority list
- var priorities = $("form#report_inspect_form " + selector).data('priorities');
- var $select = $('#problem_priority'),
- curr_pri = $select.val();
- $select.find('option:gt(0)').remove();
- $.each(priorities.split('&'), function(i, kv) {
- if (!kv) {
- return;
- }
- kv = kv.split('=', 2);
- $select.append($('<option>', { value: kv[0], text: decodeURIComponent(kv[1]) }));
- });
- $select.val(curr_pri);
- });
-
- // The inspect form submit button can change depending on the selected state
- $("#report_inspect_form [name=state]").change(function(){
- var state = $(this).val();
- var $inspect_form = $("#report_inspect_form");
- var $submit = $inspect_form.find("input[type=submit]");
- var value = $submit.attr('data-value-'+state);
- if (value !== undefined) {
- $submit.val(value);
- } else {
- $submit.val($submit.data('valueOriginal'));
- }
-
- // We might also have a response template to preselect for the new state
- var $select = $inspect_form.find("select.js-template-name");
- var $option = $select.find("option[data-problem-state='"+state+"']").first();
- if ($option.length) {
- $select.val($option.val()).change();
- }
- }).change();
-
- $('.js-toggle-public-update').each(function() {
- var $checkbox = $(this);
- var toggle_public_update = function() {
- if ($checkbox.prop('checked')) {
- $('#public_update').parents('p').show();
- } else {
- $('#public_update').parents('p').hide();
- }
- };
- $checkbox.on('change', function() {
- toggle_public_update();
- });
- toggle_public_update();
- });
-
- if (geo_position_js.init()) {
- fixmystreet.geolocate.setup(function(pos) {
- var latlon = new OpenLayers.LonLat(pos.coords.longitude, pos.coords.latitude);
- var bng = latlon.clone().transform(
- new OpenLayers.Projection("EPSG:4326"),
- new OpenLayers.Projection("EPSG:27700") // TODO: Handle other projections
- );
- $("#problem_northing").text(bng.lat.toFixed(1));
- $("#problem_easting").text(bng.lon.toFixed(1));
- $("form#report_inspect_form input[name=latitude]").val(latlon.lat);
- $("form#report_inspect_form input[name=longitude]").val(latlon.lon);
- });
- }
- },
-
mobile_ui_tweaks: function() {
//move 'skip this step' link on mobile
$('.mobile #skip-this-step').addClass('chevron').wrap('<li>').parent().appendTo('#key-tools');
@@ -1065,75 +800,8 @@ $.extend(fixmystreet.set_up, {
}
});
});
- },
-
- moderation: function() {
- function toggle_original ($input, revert) {
- $input.prop('disabled', revert);
- if (revert) {
- $input.data('currentValue', $input.val());
- }
- $input.val($input.data(revert ? 'originalValue' : 'currentValue'));
- }
-
- function add_handlers (elem, word) {
- elem.each( function () {
- var $elem = $(this);
- $elem.find('.js-moderate').on('click', function () {
- $elem.find('.moderate-display').hide();
- $elem.find('.moderate-edit').show();
- });
-
- $elem.find('.revert-title').change( function () {
- toggle_original($elem.find('input[name=problem_title]'), $(this).prop('checked'));
- });
-
- $elem.find('.revert-textarea').change( function () {
- toggle_original($elem.find('textarea'), $(this).prop('checked'));
- });
-
- var hide_document = $elem.find('.hide-document');
- hide_document.change( function () {
- $elem.find('input[name=problem_title]').prop('disabled', $(this).prop('checked'));
- $elem.find('textarea').prop('disabled', $(this).prop('checked'));
- $elem.find('input[type=checkbox]').prop('disabled', $(this).prop('checked'));
- $(this).prop('disabled', false); // in case disabled above
- });
-
- $elem.find('.cancel').click( function () {
- $elem.find('.moderate-display').show();
- $elem.find('.moderate-edit').hide();
- });
-
- $elem.find('form').submit( function () {
- if (hide_document.prop('checked')) {
- return confirm('This will hide the ' + word + ' completely! (You will not be able to undo this without contacting support.)');
- }
- return true;
- });
- });
- }
- add_handlers( $('.problem-header'), 'problem' );
- add_handlers( $('.item-list__item--updates'), 'update' );
- },
-
- response_templates: function() {
- // If the user has manually edited the contents of an update field,
- // mark it as dirty so it doesn't get clobbered if we select another
- // response template. If the field is empty, it's not considered dirty.
- $('.js-template-name').each(function() {
- var $input = $('#' + $(this).data('for'));
- $input.change(function() { $(this).data('dirty', !/^\s*$/.test($(this).val())); });
- });
-
- $('.js-template-name').change(function() {
- var $this = $(this);
- var $input = $('#' + $this.data('for'));
- if (!$input.data('dirty')) {
- $input.val($this.val());
- }
- });
}
+
});
// The new location will be saved to a history state unless
@@ -1315,7 +983,7 @@ fixmystreet.display = {
},
report: function(reportPageUrl, reportId, callback) {
- $.ajax(reportPageUrl).done(function(html, textStatus, jqXHR) {
+ $.ajax(reportPageUrl, { cache: false }).done(function(html, textStatus, jqXHR) {
var $reportPage = $(html),
$twoColReport = $reportPage.find('.two_column_sidebar'),
$sideReport = $reportPage.find('#side-report');
@@ -1329,8 +997,8 @@ fixmystreet.display = {
if ($twoColReport.length) {
$twoColReport.appendTo('#map_sidebar');
$('body').addClass('with-actions');
- fixmystreet.set_up.report_page_inspect();
- fixmystreet.set_up.manage_duplicates();
+ fixmystreet.run(fixmystreet.set_up.report_page_inspect);
+ fixmystreet.run(fixmystreet.set_up.manage_duplicates);
} else {
$sideReport.appendTo('#map_sidebar');
}
@@ -1372,8 +1040,8 @@ fixmystreet.display = {
fixmystreet.set_up.fancybox_images();
fixmystreet.set_up.dropzone($sideReport);
fixmystreet.set_up.form_focus_triggers();
- fixmystreet.set_up.moderation();
- fixmystreet.set_up.response_templates();
+ fixmystreet.run(fixmystreet.set_up.moderation);
+ fixmystreet.run(fixmystreet.set_up.response_templates);
window.selected_problem_id = reportId;
var marker = fixmystreet.maps.get_marker_by_id(reportId);
diff --git a/web/cobrands/fixmystreet/staff.js b/web/cobrands/fixmystreet/staff.js
new file mode 100644
index 000000000..9825a37ea
--- /dev/null
+++ b/web/cobrands/fixmystreet/staff.js
@@ -0,0 +1,342 @@
+$.extend(fixmystreet.set_up, {
+ manage_duplicates: function() {
+ // Deal with changes to report state by inspector/other staff, specifically
+ // displaying nearby reports if it's changed to 'duplicate'.
+ function refresh_duplicate_list() {
+ var report_id = $("#report_inspect_form .js-report-id").text();
+ var args = {
+ filter_category: $("#report_inspect_form select#category").val(),
+ latitude: $('input[name="latitude"]').val(),
+ longitude: $('input[name="longitude"]').val()
+ };
+ $("#js-duplicate-reports ul").html("<li>Loading...</li>");
+ var nearby_url = '/report/'+report_id+'/nearby.json';
+ $.getJSON(nearby_url, args, function(data) {
+ var duplicate_of = $("#report_inspect_form [name=duplicate_of]").val();
+ var $reports = $(data.current)
+ .filter("li")
+ .not("[data-report-id="+report_id+"]")
+ .slice(0, 5);
+ $reports.filter("[data-report-id="+duplicate_of+"]").addClass("item-list--reports__item--selected");
+
+ (function() {
+ var timeout;
+ $reports.on('mouseenter', function(){
+ clearTimeout(timeout);
+ fixmystreet.maps.markers_highlight(parseInt($(this).data('reportId'), 10));
+ }).on('mouseleave', function(){
+ timeout = setTimeout(fixmystreet.maps.markers_highlight, 50);
+ });
+ })();
+
+ $("#js-duplicate-reports ul").empty().prepend($reports);
+
+ $reports.find("a").click(function() {
+ var report_id = $(this).closest("li").data('reportId');
+ $("#report_inspect_form [name=duplicate_of]").val(report_id);
+ $("#js-duplicate-reports ul li").removeClass("item-list--reports__item--selected");
+ $(this).closest("li").addClass("item-list--reports__item--selected");
+ return false;
+ });
+
+ show_nearby_pins(data, report_id);
+ });
+ }
+
+ function show_nearby_pins(data, report_id) {
+ var markers = fixmystreet.maps.markers_list( data.pins, true );
+ // We're replacing all the features in the markers layer with the
+ // possible duplicates, but the list of pins from the server doesn't
+ // include the current report. So we need to extract the feature for
+ // the current report and include it in the list of features we're
+ // showing on the layer.
+ var report_marker = fixmystreet.maps.get_marker_by_id(parseInt(report_id, 10));
+ if (report_marker) {
+ markers.unshift(report_marker);
+ }
+ fixmystreet.markers.removeAllFeatures();
+ fixmystreet.markers.addFeatures( markers );
+ }
+
+ function state_change() {
+ // The duplicate report list only makes sense when state is 'duplicate'
+ if ($(this).val() !== "duplicate") {
+ $("#js-duplicate-reports").addClass("hidden");
+ return;
+ } else {
+ $("#js-duplicate-reports").removeClass("hidden");
+ }
+ // If this report is already marked as a duplicate of another, then
+ // there's no need to refresh the list of duplicate reports
+ var duplicate_of = $("#report_inspect_form [name=duplicate_of]").val();
+ if (!!duplicate_of) {
+ return;
+ }
+
+ refresh_duplicate_list();
+ }
+
+ $("#report_inspect_form").on("change.state", "select#state", state_change);
+ $("#js-change-duplicate-report").click(refresh_duplicate_list);
+ },
+
+ list_item_actions: function() {
+ function toggle_shortlist(btn, sw, id) {
+ btn.attr('class', 'item-list__item__shortlist-' + sw);
+ btn.attr('title', btn.data('label-' + sw));
+ if (id) {
+ sw += '-' + id;
+ }
+ btn.attr('name', 'shortlist-' + sw);
+ }
+
+ $('.item-list--reports').on('click', ':submit', function(e) {
+ e.preventDefault();
+
+ var $submitButton = $(this);
+ var whatUserWants = $submitButton.prop('name');
+ var data;
+ var $item;
+ var $list;
+ var $hiddenInput;
+ var report_id;
+ if (fixmystreet.page === 'around') {
+ // Deal differently because one big form
+ var parts = whatUserWants.split('-');
+ whatUserWants = parts[0] + '-' + parts[1];
+ report_id = parts[2];
+ var token = $('[name=token]').val();
+ data = whatUserWants + '=1&token=' + token + '&id=' + report_id;
+ } else {
+ var $form = $(this).parents('form');
+ $item = $form.parent('.item-list__item');
+ $list = $item.parent('.item-list');
+
+ // The server expects to be told which button/input triggered the form
+ // submission. But $form.serialize() doesn't know that. So we inject a
+ // hidden input into the form, that can pass the name and value of the
+ // submit button to the server, as it expects.
+ $hiddenInput = $('<input>').attr({
+ type: 'hidden',
+ name: whatUserWants,
+ value: $submitButton.prop('value')
+ }).appendTo($form);
+ data = $form.serialize() + '&ajax=1';
+ }
+
+ // Update UI while the ajax request is sent in the background.
+ if ('shortlist-down' === whatUserWants) {
+ $item.insertAfter( $item.next() );
+ } else if ('shortlist-up' === whatUserWants) {
+ $item.insertBefore( $item.prev() );
+ } else if ('shortlist-remove' === whatUserWants) {
+ toggle_shortlist($submitButton, 'add', report_id);
+ } else if ('shortlist-add' === whatUserWants) {
+ toggle_shortlist($submitButton, 'remove', report_id);
+ }
+
+ // Items have moved around. We need to make sure the "up" button on the
+ // first item, and the "down" button on the last item, are disabled.
+ fixmystreet.update_list_item_buttons($list);
+
+ $.ajax({
+ url: '/my/planned/change',
+ type: 'POST',
+ data: data
+ }).fail(function() {
+ // Undo the UI changes we made.
+ if ('shortlist-down' === whatUserWants) {
+ $item.insertBefore( $item.prev() );
+ } else if ('shortlist-up' === whatUserWants) {
+ $item.insertAfter( $item.next() );
+ } else if ('shortlist-remove' === whatUserWants) {
+ toggle_shortlist($submitButton, 'remove', report_id);
+ } else if ('shortlist-add' === whatUserWants) {
+ toggle_shortlist($submitButton, 'add', report_id);
+ }
+ fixmystreet.update_list_item_buttons($list);
+ }).complete(function() {
+ if ($hiddenInput) {
+ $hiddenInput.remove();
+ }
+ });
+ });
+ },
+
+ contribute_as: function() {
+ $('.content').on('change', '.js-contribute-as', function(){
+ var opt = this.options[this.selectedIndex],
+ val = opt.value,
+ txt = opt.text;
+ var $emailInput = $('input[name=email]').add('input[name=rznvy]');
+ var $nameInput = $('input[name=name]');
+ var $showNameCheckbox = $('input[name=may_show_name]');
+ var $addAlertCheckbox = $('#form_add_alert');
+ if (val === 'myself') {
+ $emailInput.val($emailInput.prop('defaultValue')).prop('disabled', true);
+ $nameInput.val($nameInput.prop('defaultValue')).prop('disabled', false);
+ $showNameCheckbox.prop('checked', false).prop('disabled', false);
+ $addAlertCheckbox.prop('checked', true).prop('disabled', false);
+ } else if (val === 'another_user') {
+ $emailInput.val('').prop('disabled', false);
+ $nameInput.val('').prop('disabled', false);
+ $showNameCheckbox.prop('checked', false).prop('disabled', true);
+ $addAlertCheckbox.prop('checked', true).prop('disabled', false);
+ } else if (val === 'body') {
+ $emailInput.val('-').prop('disabled', true);
+ $nameInput.val(txt).prop('disabled', true);
+ $showNameCheckbox.prop('checked', true).prop('disabled', true);
+ $addAlertCheckbox.prop('checked', false).prop('disabled', true);
+ }
+ });
+ $('.js-contribute-as').change();
+ },
+
+ report_page_inspect: function() {
+ if (!$('form#report_inspect_form').length) {
+ return;
+ }
+
+ // Focus on form
+ $('html,body').scrollTop($('#report_inspect_form').offset().top);
+
+ // On the manage/inspect report form, we already have all the extra inputs
+ // in the DOM, we just need to hide/show them as appropriate.
+ $('form#report_inspect_form [name=category]').change(function() {
+ var category = $(this).val(),
+ selector = "[data-category='" + category + "']";
+ $("form#report_inspect_form [data-category]:not(" + selector + ")").addClass("hidden");
+ $("form#report_inspect_form " + selector).removeClass("hidden");
+ // And update the associated priority list
+ var priorities = $("form#report_inspect_form " + selector).data('priorities');
+ var $select = $('#problem_priority'),
+ curr_pri = $select.val();
+ $select.find('option:gt(0)').remove();
+ $.each(priorities.split('&'), function(i, kv) {
+ if (!kv) {
+ return;
+ }
+ kv = kv.split('=', 2);
+ $select.append($('<option>', { value: kv[0], text: decodeURIComponent(kv[1]) }));
+ });
+ $select.val(curr_pri);
+ });
+
+ // The inspect form submit button can change depending on the selected state
+ $("#report_inspect_form [name=state]").change(function(){
+ var state = $(this).val();
+ var $inspect_form = $("#report_inspect_form");
+ var $submit = $inspect_form.find("input[type=submit]");
+ var value = $submit.attr('data-value-'+state);
+ if (value !== undefined) {
+ $submit.val(value);
+ } else {
+ $submit.val($submit.data('valueOriginal'));
+ }
+
+ // We might also have a response template to preselect for the new state
+ var $select = $inspect_form.find("select.js-template-name");
+ var $option = $select.find("option[data-problem-state='"+state+"']").first();
+ if ($option.length) {
+ $select.val($option.val()).change();
+ }
+ }).change();
+
+ $('.js-toggle-public-update').each(function() {
+ var $checkbox = $(this);
+ var toggle_public_update = function() {
+ if ($checkbox.prop('checked')) {
+ $('#public_update').parents('p').show();
+ } else {
+ $('#public_update').parents('p').hide();
+ }
+ };
+ $checkbox.on('change', function() {
+ toggle_public_update();
+ });
+ toggle_public_update();
+ });
+
+ if (geo_position_js.init()) {
+ fixmystreet.geolocate.setup(function(pos) {
+ var latlon = new OpenLayers.LonLat(pos.coords.longitude, pos.coords.latitude);
+ var bng = latlon.clone().transform(
+ new OpenLayers.Projection("EPSG:4326"),
+ new OpenLayers.Projection("EPSG:27700") // TODO: Handle other projections
+ );
+ $("#problem_northing").text(bng.lat.toFixed(1));
+ $("#problem_easting").text(bng.lon.toFixed(1));
+ $("form#report_inspect_form input[name=latitude]").val(latlon.lat);
+ $("form#report_inspect_form input[name=longitude]").val(latlon.lon);
+ });
+ }
+ },
+
+ moderation: function() {
+ function toggle_original ($input, revert) {
+ $input.prop('disabled', revert);
+ if (revert) {
+ $input.data('currentValue', $input.val());
+ }
+ $input.val($input.data(revert ? 'originalValue' : 'currentValue'));
+ }
+
+ function add_handlers (elem, word) {
+ elem.each( function () {
+ var $elem = $(this);
+ $elem.find('.js-moderate').on('click', function () {
+ $elem.find('.moderate-display').hide();
+ $elem.find('.moderate-edit').show();
+ });
+
+ $elem.find('.revert-title').change( function () {
+ toggle_original($elem.find('input[name=problem_title]'), $(this).prop('checked'));
+ });
+
+ $elem.find('.revert-textarea').change( function () {
+ toggle_original($elem.find('textarea'), $(this).prop('checked'));
+ });
+
+ var hide_document = $elem.find('.hide-document');
+ hide_document.change( function () {
+ $elem.find('input[name=problem_title]').prop('disabled', $(this).prop('checked'));
+ $elem.find('textarea').prop('disabled', $(this).prop('checked'));
+ $elem.find('input[type=checkbox]').prop('disabled', $(this).prop('checked'));
+ $(this).prop('disabled', false); // in case disabled above
+ });
+
+ $elem.find('.cancel').click( function () {
+ $elem.find('.moderate-display').show();
+ $elem.find('.moderate-edit').hide();
+ });
+
+ $elem.find('form').submit( function () {
+ if (hide_document.prop('checked')) {
+ return confirm('This will hide the ' + word + ' completely! (You will not be able to undo this without contacting support.)');
+ }
+ return true;
+ });
+ });
+ }
+ add_handlers( $('.problem-header'), 'problem' );
+ add_handlers( $('.item-list__item--updates'), 'update' );
+ },
+
+ response_templates: function() {
+ // If the user has manually edited the contents of an update field,
+ // mark it as dirty so it doesn't get clobbered if we select another
+ // response template. If the field is empty, it's not considered dirty.
+ $('.js-template-name').each(function() {
+ var $input = $('#' + $(this).data('for'));
+ $input.change(function() { $(this).data('dirty', !/^\s*$/.test($(this).val())); });
+ });
+
+ $('.js-template-name').change(function() {
+ var $this = $(this);
+ var $input = $('#' + $this.data('for'));
+ if (!$input.data('dirty')) {
+ $input.val($this.val());
+ }
+ });
+ }
+});
diff --git a/web/cobrands/sass/_admin.scss b/web/cobrands/sass/_admin.scss
index a53f9f60a..58917a8ce 100644
--- a/web/cobrands/sass/_admin.scss
+++ b/web/cobrands/sass/_admin.scss
@@ -67,39 +67,51 @@ $button_bg_col: #a1a1a1; // also search bar (tables)
list-style: none;
}
}
- .admin-box { // for delimiting forms, etc
- border:1px solid #999;
- padding:0.5em 1em;
- margin:1.5em 0;
- h2 { // only really want on first-child
+}
+
+.admin-box { // for delimiting forms, etc
+ border:1px solid #999;
+ padding:0.5em 1em;
+ margin:1.5em 0;
+ h2 { // only really want on first-child
margin-top: 0;
- }
}
- .admin-offsite-link {
+}
+
+.admin-offsite-link {
display: inline;
- }
- .fms-admin-warning, .fms-admin-info {
+ padding-#{$right}: 12px;
+ background-image: url(../../i/external-link.png);
+ background-position: $right top;
+ background-repeat: no-repeat;
+}
+
+.fms-admin-warning, .fms-admin-info {
padding: 1em;
font-size: 90%;
border-style: solid;
border-width: 1px;
border-#{$left}-width: 1em;
margin-bottom: 1em;
- }
- .fms-admin-warning {
- border-color: #f99;
- background-color: #ffe1e1;
- }
- .fms-admin-info {
- border-color: #9f9;
- background-color: #e1ffe1;
- }
- .admin-open311-only {
+}
+
+.fms-admin-warning {
+ border-color: #f99;
+ background-color: #ffe1e1;
+}
+
+.fms-admin-info {
+ border-color: #9f9;
+ background-color: #e1ffe1;
+}
+
+.admin-open311-only {
border:1px solid #666;
padding:1em;
margin: 1em 0;
- }
- .admin-hint {
+}
+
+.admin-hint {
font-size: 80%; // little question marks are small
cursor: pointer;
display: block;
@@ -115,35 +127,39 @@ $button_bg_col: #a1a1a1; // also search bar (tables)
-webkit-border-radius: 0.333em;
border-radius: 0.333em;
p {
- display:none;
+ display:none;
}
&:before { content: "?" }
&.admin-hint-show {
- font-size: 90%;
- text-align: $left;
- display: block;
- float:none;
- margin:1em 0;
- &:before { content: "" }
- background-color: inherit !important;
- p {
- font-weight: normal;
+ font-size: 90%;
+ text-align: $left;
display: block;
- background-color: #ff9;
- color: #000;
- border-style: solid;
- border-width: 1px;
- border-#{$left}-width: 1em;
- border-color: #f93;
- padding:1em;
- margin: 0;
- }
+ float:none;
+ margin:1em 0;
+ &:before { content: "" }
+ background-color: inherit !important;
+ p {
+ font-weight: normal;
+ display: block;
+ background-color: #ff9;
+ color: #000;
+ border-style: solid;
+ border-width: 1px;
+ border-#{$left}-width: 1em;
+ border-color: #f93;
+ padding:1em;
+ margin: 0;
+ }
+ }
+}
+
+.admin-index-search {
+ width: 27em;
+ form {
+ clear: left;
+ }
+ select {
+ max-width: 65%;
+ float: left;
}
- }
- .admin-offsite-link {
- padding-#{$right}: 12px;
- background-image: url(../../i/external-link.png);
- background-position: $right top;
- background-repeat: no-repeat;
- }
}