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; -  }  }  | 
