diff options
-rwxr-xr-x | bin/open311-populate-service-list | 3 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin.pm | 40 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand.pm | 10 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Bromley.pm | 33 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Greenwich.pm | 9 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Oxfordshire.pm | 23 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/UK.pm | 10 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/WestBerkshire.pm | 16 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Body.pm | 15 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Problem.pm | 4 | ||||
-rw-r--r-- | perllib/FixMyStreet/SendReport/Open311.pm | 84 | ||||
-rw-r--r-- | t/app/sendreport/open311.t | 267 | ||||
-rw-r--r-- | templates/web/base/admin/index.html | 6 | ||||
-rw-r--r-- | templates/web/base/common_scripts.html | 8 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/admin.js (renamed from web/js/fixmystreet-admin.js) | 0 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/fixmystreet.js | 360 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/staff.js | 342 | ||||
-rw-r--r-- | web/cobrands/sass/_admin.scss | 110 |
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; - } } |