diff options
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report.pm | 54 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Problem.pm | 25 | ||||
-rw-r--r-- | t/app/controller/report_display.t | 50 | ||||
-rw-r--r-- | t/app/controller/report_inspect.t | 57 | ||||
-rw-r--r-- | t/app/controller/report_updates.t | 2 | ||||
-rw-r--r-- | t/app/model/problem.t | 11 | ||||
-rw-r--r-- | templates/web/base/report/_inspect.html | 49 | ||||
-rw-r--r-- | templates/web/base/report/_item.html | 2 | ||||
-rw-r--r-- | templates/web/base/report/display.html | 9 | ||||
-rw-r--r-- | templates/web/base/report/duplicate-no-updates.html | 7 | ||||
-rw-r--r-- | templates/web/base/report/updates.html | 2 | ||||
-rw-r--r-- | templates/web/bromley/report/display.html | 148 | ||||
-rw-r--r-- | templates/web/bromley/report/update-form.html | 137 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/fixmystreet.js | 94 | ||||
-rw-r--r-- | web/cobrands/sass/_report_list_pins.scss | 8 | ||||
-rw-r--r-- | web/js/map-OpenLayers.js | 86 | ||||
-rw-r--r-- | web/js/map-google.js | 3 |
17 files changed, 519 insertions, 225 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm index 13967601b..f7ccddc70 100644 --- a/perllib/FixMyStreet/App/Controller/Report.pm +++ b/perllib/FixMyStreet/App/Controller/Report.pm @@ -231,13 +231,8 @@ sub generate_map_tags : Private { latitude => $problem->latitude, longitude => $problem->longitude, pins => $problem->used_map - ? [ { - latitude => $problem->latitude, - longitude => $problem->longitude, - colour => $c->cobrand->pin_colour($problem, 'report'), - type => 'big', - } ] - : [], + ? [ $problem->pin_data($c, 'report', type => 'big') ] + : [], ); return 1; @@ -300,6 +295,7 @@ sub action_router : Path('') : Args(2) { my ( $self, $c, $id, $action ) = @_; $c->go( 'map', [ $id ] ) if $action eq 'map'; + $c->go( 'nearby_json', [ $id ] ) if $action eq 'nearby.json'; $c->detach( '/page_error_404_not_found', [] ); } @@ -318,9 +314,10 @@ sub inspect : Private { my $valid = 1; my $update_text; my $reputation_change = 0; + my %update_params = (); if ($permissions->{report_inspect}) { - foreach (qw/detailed_information traffic_information/) { + foreach (qw/detailed_information traffic_information duplicate_of/) { $problem->set_extra_metadata( $_ => $c->get_param($_) ); } @@ -345,6 +342,14 @@ sub inspect : Private { if ( $problem->state eq 'hidden' ) { $problem->get_photoset->delete_cached; } + if ( $problem->state eq 'duplicate' && $old_state ne 'duplicate' ) { + # If the report is being closed as duplicate, make sure the + # update records this. + $update_params{problem_state} = "duplicate"; + } + if ( $problem->state ne 'duplicate' ) { + $problem->unset_extra_metadata('duplicate_of'); + } if ( $problem->state ne $old_state ) { $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'state_change' ] ); } @@ -389,6 +394,7 @@ sub inspect : Private { state => 'confirmed', mark_fixed => 0, anonymous => 0, + %update_params, } ); } # This problem might no longer be visible on the current cobrand, @@ -417,6 +423,38 @@ sub map : Private { } +sub nearby_json : Private { + my ( $self, $c, $id ) = @_; + + $c->forward( 'load_problem_or_display_error', [ $id ] ); + my $p = $c->stash->{problem}; + my $dist = 1000; + + my $nearby = $c->model('DB::Nearby')->nearby( + $c, $dist, [ $p->id ], 5, $p->latitude, $p->longitude, undef, [ $p->category ], undef + ); + my @pins = map { + my $p = $_->problem; + my $colour = $c->cobrand->pin_colour( $p, 'around' ); + [ $p->latitude, $p->longitude, + $colour, + $p->id, $p->title_safe, 'small', JSON->false + ] + } @$nearby; + + my $on_map_list_html = $c->render_fragment( + 'around/on_map_list_items.html', + { on_map => [], around_map => $nearby } + ); + + my $json = { pins => \@pins }; + $json->{current} = $on_map_list_html if $on_map_list_html; + my $body = encode_json($json); + $c->res->content_type('application/json; charset=utf-8'); + $c->res->body($body); +} + + =head2 check_has_permission_to Ensure the currently logged-in user has any of the provided permissions applied diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index ab6f20050..ec1534fe9 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -181,6 +181,7 @@ use namespace::clean -except => [ 'meta' ]; use Utils; use FixMyStreet::Map::FMS; use LWP::Simple qw($ua); +use RABX; my $IM = eval { require Image::Magick; @@ -941,6 +942,7 @@ sub pin_data { id => $self->id, title => $opts{private} ? $self->title : $self->title_safe, problem => $self, + type => $opts{type}, } }; @@ -1032,4 +1034,27 @@ has shortlisted_user => ( }, ); +has duplicate_of => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + return unless $self->state eq 'duplicate'; + my $duplicate_of = int($self->get_extra_metadata("duplicate_of") || 0); + return unless $duplicate_of; + return $self->result_source->schema->resultset('Problem')->search({ id => $duplicate_of })->first; + }, +); + +has duplicates => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + my $rabx_id = RABX::serialise( $self->id ); + my @duplicates = $self->result_source->schema->resultset('Problem')->search({ extra => { like => "\%duplicate_of,$rabx_id%" } })->all; + return \@duplicates; + }, +); + 1; diff --git a/t/app/controller/report_display.t b/t/app/controller/report_display.t index fb532ddc4..fad8b2bbb 100644 --- a/t/app/controller/report_display.t +++ b/t/app/controller/report_display.t @@ -25,31 +25,16 @@ my $dt = DateTime->new( second => 23 ); -my $report = FixMyStreet::App->model('DB::Problem')->find_or_create( - { - postcode => 'SW1A 1AA', - bodies_str => '2504', - areas => ',105255,11806,11828,2247,2504,', - category => 'Other', - title => 'Test 2', - detail => 'Test 2 Detail', - used_map => 't', - name => 'Test User', - anonymous => 'f', - state => 'confirmed', - confirmed => $dt->ymd . ' ' . $dt->hms, - lang => 'en-gb', - service => '', - cobrand => 'default', - cobrand_data => '', - send_questionnaire => 't', - latitude => '51.5016605453401', - longitude => '-0.142497580865087', - user_id => $user->id, - } -); +my $westminster = $mech->create_body_ok(2504, 'Westminster City Council'); +my ($report, $report2) = $mech->create_problems_for_body(2, $westminster->id, "Example", { + user => $user, + confirmed => $dt->ymd . ' ' . $dt->hms, +}); +$report->update({ + title => 'Test 2', + detail => 'Test 2 Detail' +}); my $report_id = $report->id; -ok $report, "created test report - $report_id"; subtest "check that no id redirects to homepage" => sub { $mech->get_ok('/report'); @@ -125,6 +110,22 @@ subtest "check owner of report can view non public reports" => sub { ok $report->update( { non_public => 0 } ), 'make report public'; }; +subtest "duplicate reports are signposted correctly" => sub { + $report2->set_extra_metadata(duplicate_of => $report->id); + $report2->state('duplicate'); + $report2->update; + + my $report2_id = $report2->id; + ok $mech->get("/report/$report2_id"), "get '/report/$report2_id'"; + $mech->content_contains('This report is a duplicate'); + $mech->content_contains($report->title); + $mech->log_out_ok; + + $report2->unset_extra_metadata('duplicate_of'); + $report2->state('confirmed'); + $report2->update; +}; + subtest "test a good report" => sub { $mech->get_ok("/report/$report_id"); is $mech->uri->path, "/report/$report_id", "at /report/$report_id"; @@ -532,5 +533,6 @@ subtest "Zurich banners are displayed correctly" => sub { END { $mech->delete_user('test@example.com'); + $mech->delete_body($westminster); done_testing(); } diff --git a/t/app/controller/report_inspect.t b/t/app/controller/report_inspect.t index efb32d116..56e6e957f 100644 --- a/t/app/controller/report_inspect.t +++ b/t/app/controller/report_inspect.t @@ -21,12 +21,14 @@ my $wodc = $mech->create_body_ok(2420, 'West Oxfordshire District Council', id = $mech->create_contact_ok( body_id => $wodc->id, category => 'Horses', email => 'horses@example.net' ); -my ($report) = $mech->create_problems_for_body(1, $oxon->id, 'Test', { +my ($report, $report2) = $mech->create_problems_for_body(2, $oxon->id, 'Test', { category => 'Cows', cobrand => 'fixmystreet', areas => ',2237,2420', whensent => \'current_timestamp', latitude => 51.847693, longitude => -1.355908, }); my $report_id = $report->id; +my $report2_id = $report2->id; + my $user = $mech->log_in_ok('test@example.com'); $user->update( { from_body => $oxon } ); @@ -93,6 +95,59 @@ FixMyStreet::override_config { $mech->content_lacks('Invalid location'); }; + subtest "test duplicate reports are shown" => sub { + my $old_state = $report->state; + $report->set_extra_metadata('duplicate_of' => $report2->id); + $report->state('duplicate'); + $report->update; + + $mech->get_ok("/report/$report_id"); + $mech->content_contains($report2->title); + + $mech->get_ok("/report/$report2_id"); + $mech->content_contains($report->title); + + $report->unset_extra_metadata('duplicate_of'); + $report->state($old_state); + $report->update; + }; + + subtest "marking a report as a duplicate with update correctly sets update status" => sub { + my $old_state = $report->state; + $report->comments->delete_all; + + $mech->get_ok("/report/$report_id"); + $mech->submit_form_ok({ button => 'save', with_fields => { state => 'Duplicate', duplicate_of => $report2->id, public_update => "This is a duplicate.", save_inspected => "1" } }); + $report->discard_changes; + + is $report->state, 'duplicate', 'report marked as duplicate'; + is $report->comments->search({ problem_state => 'duplicate' })->count, 1, 'update marking report as duplicate was left'; + + $report->update({ state => $old_state }); + }; + + subtest "marking a report as a duplicate doesn't clobber user-provided update" => sub { + my $old_state = $report->state; + $report->comments->delete_all; + + $mech->get_ok("/report/$report_id"); + my $update_text = "This text was entered as an update by the user."; + $mech->submit_form_ok({ button => 'save', with_fields => { + state => 'Duplicate', + duplicate_of => $report2->id, + public_update => $update_text, + save_inspected => "1", + }}); + $report->discard_changes; + + is $report->state, 'duplicate', 'report marked as duplicate'; + is $report->comments->search({ problem_state => 'duplicate' })->count, 1, 'update marked report as duplicate'; + $mech->content_contains($update_text); + $mech->content_lacks("Thank you for your report. This problem has already been reported."); + + $report->update({ state => $old_state }); + }; + foreach my $test ( { type => 'report_edit_priority', priority => 1 }, { type => 'report_edit_category', category => 1 }, diff --git a/t/app/controller/report_updates.t b/t/app/controller/report_updates.t index e1cd0da71..5a88097fa 100644 --- a/t/app/controller/report_updates.t +++ b/t/app/controller/report_updates.t @@ -685,6 +685,8 @@ for my $test ( my $meta_state = $test->{meta} || $test->{fields}->{state}; if ( $test->{reopened} ) { like $update_meta->[0], qr/reopened$/, 'update meta says reopened'; + } elsif ( $test->{state} eq 'duplicate' ) { + like $update_meta->[0], qr/closed as $meta_state$/, 'update meta includes state change'; } else { like $update_meta->[0], qr/marked as $meta_state$/, 'update meta includes state change'; } diff --git a/t/app/model/problem.t b/t/app/model/problem.t index bd7d0e55c..1130078c0 100644 --- a/t/app/model/problem.t +++ b/t/app/model/problem.t @@ -763,6 +763,17 @@ subtest 'check response templates' => sub { is $problem->response_templates, 2, 'Global and pothole templates returned'; }; +subtest 'check duplicate reports' => sub { + my ($problem1, $problem2) = $mech->create_problems_for_body(2, $body_ids{2651}, 'TITLE'); + $problem1->set_extra_metadata(duplicate_of => $problem2->id); + $problem1->state('duplicate'); + $problem1->update; + + is $problem1->duplicate_of->title, $problem2->title, 'problem1 returns correct problem from duplicate_of'; + is scalar @{ $problem2->duplicates }, 1, 'problem2 has correct number of duplicates'; + is $problem2->duplicates->[0]->title, $problem1->title, 'problem2 includes problem1 in duplicates'; +}; + END { $problem->comments->delete if $problem; $problem->delete if $problem; diff --git a/templates/web/base/report/_inspect.html b/templates/web/base/report/_inspect.html index 012411b7e..ccaa756c5 100644 --- a/templates/web/base/report/_inspect.html +++ b/templates/web/base/report/_inspect.html @@ -9,7 +9,7 @@ <div class="inspect-section"> <p> <strong>[% loc('Report ID:') %]</strong> - [% problem.id %] + <span class="js-report-id">[% problem.id %]</span> </p> <p> [% SET local_coords = problem.local_coords; %] @@ -63,19 +63,38 @@ [% END %] [% IF permissions.report_inspect %] - <p> - <label for="state">[% loc('State') %]</label> - [%# XXX this is duplicated from admin/report_edit.html, should be refactored %] - <select name="state" id="state" class="form-control"> - [% FOREACH group IN state_groups %] - <optgroup label="[% group.0 %]"> - [% FOREACH state IN group.1 %] - <option [% 'selected ' IF state == problem.state %] value="[% state %]">[% state_pretty.$state %]</option> - [% END %] - </optgroup> - [% END %] - </select> - </p> + <p> + <label for="state">[% loc('State') %]</label> + [%# XXX this is duplicated from admin/report_edit.html, should be refactored %] + <select name="state" id="state" class="form-control"> + [% FOREACH group IN state_groups %] + <optgroup label="[% group.0 %]"> + [% FOREACH state IN group.1 %] + <option [% 'selected ' IF state == problem.state %] value="[% state %]">[% state_pretty.$state %]</option> + [% END %] + </optgroup> + [% END %] + </select> + </p> + <div id="js-duplicate-reports" class="[% "hidden" UNLESS problem.duplicate_of %]"> + <input type="hidden" name="duplicate_of" value="[% problem.duplicate_of.id %]"> + <p class="[% "hidden" UNLESS problem.duplicate_of %]"><strong>[% loc('Duplicate of') %]</strong></p> + <p class="[% "hidden" IF problem.duplicate_of %]">[% loc('Which report is it a duplicate of?') %]</p> + <ul class="item-list"> + [% IF problem.duplicate_of %] + [% INCLUDE 'report/_item.html' item_extra_class = 'item-list--reports__item--selected' problem = problem.duplicate_of %] + <li class="item-list__item"><a class="btn" href="#" id="js-change-duplicate-report">[% loc('Choose another') %]</a></li> + [% END %] + </ul> + </div> + [% IF problem.duplicates.size %] + <p><strong>[% loc('Duplicates') %]</strong></p> + <ul class="item-list"> + [% FOR duplicate IN problem.duplicates %] + [% INCLUDE 'report/_item.html' problem = duplicate %] + [% END %] + </ul> + [% END %] [% END %] </div> @@ -139,7 +158,7 @@ <p> <input type="hidden" name="token" value="[% csrf_token %]"> <a class="btn" href="[% c.uri_for( '/report', problem.id ) %]">[% loc('Cancel') %]</a> - <input class="btn btn-primary" type="submit" value="[% loc('Save changes') %]" name="save" /> + <input class="btn btn-primary" type="submit" value="[% loc('Save changes') %]" data-value-original="[% loc('Save changes') %]" data-value-duplicate="[% loc('Save + close as duplicate') %]" name="save" /> </p> </div> diff --git a/templates/web/base/report/_item.html b/templates/web/base/report/_item.html index 5894c5d81..0f42b00ce 100644 --- a/templates/web/base/report/_item.html +++ b/templates/web/base/report/_item.html @@ -1,4 +1,4 @@ -<li class="item-list__item item-list--reports__item [% item_extra_class %]"> +<li class="item-list__item item-list--reports__item [% item_extra_class %]" data-report-id="[% problem.id | html %]"> <a href="[% c.cobrand.base_url_for_report( problem ) %][% problem.url %]"> [% IF problem.photo %] <img class="img" height="60" width="90" src="[% problem.photos.first.url_fp %]" alt=""> diff --git a/templates/web/base/report/display.html b/templates/web/base/report/display.html index 1ee5c4636..7c26c4938 100644 --- a/templates/web/base/report/display.html +++ b/templates/web/base/report/display.html @@ -40,12 +40,19 @@ [% INCLUDE 'report/banner.html' %] [% INCLUDE 'report/_main.html' %] + +[% IF problem.duplicate_of %] + [% INCLUDE 'report/duplicate-no-updates.html' hide_header = 1 %] +[% END %] + [% TRY %][% INCLUDE 'report/_message_manager.html' %][% CATCH file %][% END %] [% INCLUDE 'report/display_tools.html' %] [% TRY %][% INCLUDE 'report/sharing.html' %][% CATCH file %][% END %] [% INCLUDE 'report/updates.html' %] -[% IF NOT shown_form %] +[% IF problem.duplicate_of %] + [% INCLUDE 'report/duplicate-no-updates.html' %] +[% ELSIF NOT shown_form %] [% INCLUDE 'report/update-form.html' %] [% END %] diff --git a/templates/web/base/report/duplicate-no-updates.html b/templates/web/base/report/duplicate-no-updates.html new file mode 100644 index 000000000..eb9ded728 --- /dev/null +++ b/templates/web/base/report/duplicate-no-updates.html @@ -0,0 +1,7 @@ +<div> + [% UNLESS hide_header %]<h2>[% loc( 'Provide an update') %]</h2>[% END %] + <p>[% loc("This report is a duplicate. Please leave updates on the original report:") %]</p> + <ul class="item-list"> + [% INCLUDE 'report/_item.html' item_extra_class = 'item-list__item--with-pin item-list--reports__item--selected' problem = problem.duplicate_of %] + </ul> +</div> diff --git a/templates/web/base/report/updates.html b/templates/web/base/report/updates.html index fc2ac6c78..ff48ecbca 100644 --- a/templates/web/base/report/updates.html +++ b/templates/web/base/report/updates.html @@ -54,7 +54,7 @@ [%- ELSIF state == 'not responsible' %] [%- update_state = loc( "marked as not the council's responsibility" ) %] [%- ELSIF state == 'duplicate' %] - [%- update_state = loc( 'marked as a duplicate report' ) %] + [%- update_state = loc( 'closed as a duplicate report' ) %] [%- ELSIF state == 'internal referral' %] [%- update_state = loc( 'marked as an internal referral' ) %] [%- END %] diff --git a/templates/web/bromley/report/display.html b/templates/web/bromley/report/display.html index 9f2089d28..4c1a69bca 100644 --- a/templates/web/bromley/report/display.html +++ b/templates/web/bromley/report/display.html @@ -21,147 +21,19 @@ [% INCLUDE 'report/banner.html' %] [% INCLUDE 'report/_main.html' %] -[% INCLUDE 'report/display_tools.html' %] -[% INCLUDE 'report/updates.html' %] - -<div id="update_form"> - <h2>[% loc( 'Provide an update') %]</h2> - - [% INCLUDE 'errors.html' %] - - <form method="post" action="[% c.uri_for( '/report/update' ) %]" name="updateForm" class="validate"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]> - <input type="hidden" name="token" value="[% csrf_token %]"> - <fieldset> - <input type="hidden" name="submit_update" value="1"> - <input type="hidden" name="id" value="[% problem.id | html %]"> - - [% IF c.cobrand.allow_photo_upload %] - <input type="hidden" name="upload_fileid" value="[% upload_fileid %]"> - <label for="form_photo"> - <span data-singular="[% loc('Photo') %]" data-plural="[% loc('Photos') %]">[% loc('Photo') %]</span> - </label> - - [% IF field_errors.photo %] - <p class='form-error'>[% field_errors.photo %]</p> - [% END %] - - <div id="form_photos"> - [% IF upload_fileid %] - <p>[% loc('You have already attached photos to this update. Note that you can attach a maximum of 3 to this update (if you try to upload more, the oldest will be removed).') %]</p> - [% FOREACH id IN upload_fileid.split(',') %] - <img align="right" src="/photo/temp.[% id %]" alt=""> - [% END %] - [% END %] - <input type="file" name="photo1" id="form_photo"> - <label for="form_photo2">[% loc('Photo') %]</label> - <input type="file" name="photo2" id="form_photo2"> - <label for="form_photo3">[% loc('Photo') %]</label> - <input type="file" name="photo3" id="form_photo3"> - </div> - [% END %] - - <label for="form_update">[% loc( 'Update' ) %]</label> - [% IF field_errors.update %] - <div class='form-error'>[% field_errors.update %]</div> - [% END %] - <textarea class="form-control" rows="7" cols="30" name="update" id="form_update" placeholder="[% loc('Please write your update here') %]" required>[% update.text | html %]</textarea> - - <div class="general-notes"> - <p>Please note this comments box can only be used for this report. - <br><a href="http://www.bromley.gov.uk/report">Report a different issue</a> - </div> - - [% IF c.user && c.user.belongs_to_body( problem.bodies_str ) %] - <label for="form_state">[% loc( 'State' ) %]</label> - <select name="state" id="form_state" class="form-control"> - [% FOREACH state IN [ ['confirmed', loc('Open')], ['investigating', - loc('Investigating')], ['action scheduled', loc('Action Scheduled')], - ['in progress', loc('In Progress')], ['duplicate', loc('Duplicate')], - ['unable to fix', loc('Unable to fix')], ['not responsible', loc('Not Responsible')], - ['fixed', loc('Fixed')] ] %] - <option [% 'selected ' IF state.0 == problem.state_display %] value="[% state.0 %]">[% state.1 %]</option> - [% END %] - </select> - [% ELSE %] - [% IF problem.is_fixed AND c.user_exists AND c.user.id == problem.user_id %] - - <input type="checkbox" name="reopen" id="form_reopen" value="1"[% ' checked' IF update.mark_open %]> - <label class="inline" for="form_reopen">[% loc('This problem has not been fixed') %]</label> - - [% ELSIF !problem.is_fixed %] - - <div class="checkbox-group"> - <input type="checkbox" name="fixed" id="form_fixed" value="1"[% ' checked' IF update.mark_fixed %]> - <label class="inline" for="form_fixed">[% loc('This problem has been fixed') %]</label> - </div> - - [% END %] - [% END %] - - [% IF c.user_exists %] - - [% INCLUDE name %] - - <input class="final-submit green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"> +[% IF problem.duplicate_of %] + [% INCLUDE 'report/duplicate-no-updates.html' hide_header = 1 %] +[% END %] - [% ELSE %] - - <label for="form_rznvy">[% loc('Email' ) %] - <span class="muted">([% loc('We never show your email') %])</span> - </label> - - [% IF field_errors.email %] - <p class='form-error'>[% field_errors.email %]</p> - [% END %] - <input class="form-control" type="email" name="rznvy" id="form_rznvy" value="[% update.user.email | html %]" placeholder="[% loc('Your email address' ) %]" required> - - <div id="form_sign_in"> - <p>To submit your update you now need to confirm it either by email or by using a FixMyStreet password.</p> - - <div id="form_sign_in_no" class="form-box"> - <h5>Confirm my report by email</h5> - - [% INCLUDE name %] - - <label for="password_register">[% loc('Password (optional)') %]</label> - - <div class="general-notes"> - <p>[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]</p> - </div> - - <div class="form-txt-submit-box"> - <input type="password" class="form-control" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]"> - <input class="green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"> - </div> - </div> - <div id="form_sign_in_yes" class="form-box"> - <h5>Confirm my report with my FixMyStreet password</h5> - - <label class="hidden-js n" for="password_sign_in">[% loc('Yes I have a password') %]</label> - [% IF field_errors.password %] - <p class='form-error'>[% field_errors.password %]</p> - [% END %] - <div class="form-txt-submit-box"> - <input type="password" class="form-control" name="password_sign_in" id="password_sign_in" value="" placeholder="[% loc('Your password') %]"> - <input class="green-btn js-submit_sign_in" type="submit" name="submit_sign_in" value="[% loc('Post') %]"> - </div> - - <div class="checkbox-group"> - <input type="checkbox" id="remember_me" name="remember_me" value='1'[% ' checked' IF remember_me %]> - <label class="inline n" for="remember_me">[% loc('Keep me signed in on this computer') %]</label> - </div> - </div> - </div> - - [% END %] - - <p>Your information will only be used in accordance with our <a href="/faq#privacy">privacy policy</a>.</p> - - </fieldset> - </form> -</div> +[% INCLUDE 'report/display_tools.html' %] +[% INCLUDE 'report/updates.html' %] +[% IF problem.duplicate_of %] + [% INCLUDE 'report/duplicate-no-updates.html' %] +[% ELSE %] + [% INCLUDE 'report/update-form.html' %] +[% END %] </div> [% INCLUDE 'footer.html' %] diff --git a/templates/web/bromley/report/update-form.html b/templates/web/bromley/report/update-form.html new file mode 100644 index 000000000..112cb2960 --- /dev/null +++ b/templates/web/bromley/report/update-form.html @@ -0,0 +1,137 @@ +<div id="update_form"> + <h2>[% loc( 'Provide an update') %]</h2> + + [% INCLUDE 'errors.html' %] + + <form method="post" action="[% c.uri_for( '/report/update' ) %]" name="updateForm" class="validate"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]> + <input type="hidden" name="token" value="[% csrf_token %]"> + <fieldset> + <input type="hidden" name="submit_update" value="1"> + <input type="hidden" name="id" value="[% problem.id | html %]"> + + [% IF c.cobrand.allow_photo_upload %] + <input type="hidden" name="upload_fileid" value="[% upload_fileid %]"> + <label for="form_photo"> + <span data-singular="[% loc('Photo') %]" data-plural="[% loc('Photos') %]">[% loc('Photo') %]</span> + </label> + + [% IF field_errors.photo %] + <p class='form-error'>[% field_errors.photo %]</p> + [% END %] + + <div id="form_photos"> + [% IF upload_fileid %] + <p>[% loc('You have already attached photos to this update. Note that you can attach a maximum of 3 to this update (if you try to upload more, the oldest will be removed).') %]</p> + [% FOREACH id IN upload_fileid.split(',') %] + <img align="right" src="/photo/temp.[% id %]" alt=""> + [% END %] + [% END %] + <input type="file" name="photo1" id="form_photo"> + <label for="form_photo2">[% loc('Photo') %]</label> + <input type="file" name="photo2" id="form_photo2"> + <label for="form_photo3">[% loc('Photo') %]</label> + <input type="file" name="photo3" id="form_photo3"> + </div> + [% END %] + + <label for="form_update">[% loc( 'Update' ) %]</label> + [% IF field_errors.update %] + <div class='form-error'>[% field_errors.update %]</div> + [% END %] + <textarea class="form-control" rows="7" cols="30" name="update" id="form_update" placeholder="[% loc('Please write your update here') %]" required>[% update.text | html %]</textarea> + + <div class="general-notes"> + <p>Please note this comments box can only be used for this report. + <br><a href="http://www.bromley.gov.uk/report">Report a different issue</a> + </div> + + [% IF c.user && c.user.belongs_to_body( problem.bodies_str ) %] + <label for="form_state">[% loc( 'State' ) %]</label> + <select name="state" id="form_state" class="form-control"> + [% FOREACH state IN [ ['confirmed', loc('Open')], ['investigating', + loc('Investigating')], ['action scheduled', loc('Action Scheduled')], + ['in progress', loc('In Progress')], ['duplicate', loc('Duplicate')], + ['unable to fix', loc('Unable to fix')], ['not responsible', loc('Not Responsible')], + ['fixed', loc('Fixed')] ] %] + <option [% 'selected ' IF state.0 == problem.state_display %] value="[% state.0 %]">[% state.1 %]</option> + [% END %] + </select> + [% ELSE %] + [% IF problem.is_fixed AND c.user_exists AND c.user.id == problem.user_id %] + + <input type="checkbox" name="reopen" id="form_reopen" value="1"[% ' checked' IF update.mark_open %]> + <label class="inline" for="form_reopen">[% loc('This problem has not been fixed') %]</label> + + [% ELSIF !problem.is_fixed %] + + <div class="checkbox-group"> + <input type="checkbox" name="fixed" id="form_fixed" value="1"[% ' checked' IF update.mark_fixed %]> + <label class="inline" for="form_fixed">[% loc('This problem has been fixed') %]</label> + </div> + + [% END %] + [% END %] + + [% IF c.user_exists %] + + [% INCLUDE name %] + + <input class="final-submit green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"> + + + [% ELSE %] + + <label for="form_rznvy">[% loc('Email' ) %] + <span class="muted">([% loc('We never show your email') %])</span> + </label> + + [% IF field_errors.email %] + <p class='form-error'>[% field_errors.email %]</p> + [% END %] + <input class="form-control" type="email" name="rznvy" id="form_rznvy" value="[% update.user.email | html %]" placeholder="[% loc('Your email address' ) %]" required> + + <div id="form_sign_in"> + <p>To submit your update you now need to confirm it either by email or by using a FixMyStreet password.</p> + + <div id="form_sign_in_no" class="form-box"> + <h5>Confirm my report by email</h5> + + [% INCLUDE name %] + + <label for="password_register">[% loc('Password (optional)') %]</label> + + <div class="general-notes"> + <p>[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]</p> + </div> + + <div class="form-txt-submit-box"> + <input type="password" class="form-control" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]"> + <input class="green-btn js-submit_register" type="submit" name="submit_register" value="[% loc('Post') %]"> + </div> + </div> + <div id="form_sign_in_yes" class="form-box"> + <h5>Confirm my report with my FixMyStreet password</h5> + + <label class="hidden-js n" for="password_sign_in">[% loc('Yes I have a password') %]</label> + [% IF field_errors.password %] + <p class='form-error'>[% field_errors.password %]</p> + [% END %] + <div class="form-txt-submit-box"> + <input type="password" class="form-control" name="password_sign_in" id="password_sign_in" value="" placeholder="[% loc('Your password') %]"> + <input class="green-btn js-submit_sign_in" type="submit" name="submit_sign_in" value="[% loc('Post') %]"> + </div> + + <div class="checkbox-group"> + <input type="checkbox" id="remember_me" name="remember_me" value='1'[% ' checked' IF remember_me %]> + <label class="inline n" for="remember_me">[% loc('Keep me signed in on this computer') %]</label> + </div> + </div> + </div> + + [% END %] + + <p>Your information will only be used in accordance with our <a href="/faq#privacy">privacy policy</a>.</p> + + </fieldset> + </form> +</div> diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js index 1701c5cd0..cd3b127d6 100644 --- a/web/cobrands/fixmystreet/fixmystreet.js +++ b/web/cobrands/fixmystreet/fixmystreet.js @@ -390,6 +390,87 @@ $.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); + }, + contribute_as: function() { $('.content').on('change', '.js-contribute-as', function(){ @@ -575,6 +656,18 @@ $.extend(fixmystreet.set_up, { $("form#report_inspect_form " + selector).removeClass("hidden"); }); + // The inspect form submit button can change depending on the selected state + $("#report_inspect_form [name=state]").change(function(){ + var state = $(this).val(); + var $submit = $("#report_inspect_form input[type=submit]"); + var value = $submit.attr('data-value-'+state); + if (value !== undefined) { + $submit.val(value); + } else { + $submit.val($submit.data('valueOriginal')); + } + }).change(); + $('.js-toggle-public-update').each(function() { var $checkbox = $(this); var toggle_public_update = function() { @@ -1093,6 +1186,7 @@ fixmystreet.display = { $twoColReport.appendTo('#map_sidebar'); $('body').addClass('with-actions'); fixmystreet.set_up.report_page_inspect(); + fixmystreet.set_up.manage_duplicates(); } else { $sideReport.appendTo('#map_sidebar'); } diff --git a/web/cobrands/sass/_report_list_pins.scss b/web/cobrands/sass/_report_list_pins.scss index 74f0a5f90..eaeefbc10 100644 --- a/web/cobrands/sass/_report_list_pins.scss +++ b/web/cobrands/sass/_report_list_pins.scss @@ -50,6 +50,14 @@ color: #777; } } +.item-list--reports__item--selected { + background: $base_bg; + + a, a:hover, a:focus { + background-color: transparent; + } +} + .item-list__item--empty { background: none; diff --git a/web/js/map-OpenLayers.js b/web/js/map-OpenLayers.js index a3cefa7da..49801911b 100644 --- a/web/js/map-OpenLayers.js +++ b/web/js/map-OpenLayers.js @@ -90,7 +90,8 @@ var fixmystreet = fixmystreet || {}; size: pin[5] || marker_size, faded: 0, id: pin[3], - title: pin[4] || '' + title: pin[4] || '', + draggable: pin[6] === false ? false : true }); markers.push( marker ); } @@ -144,7 +145,7 @@ var fixmystreet = fixmystreet || {}; admin_drag: function(pin_moved_callback, confirm_change) { confirm_change = confirm_change || false; var original_lonlat; - var drag = new OpenLayers.Control.DragFeature( fixmystreet.markers, { + var drag = new OpenLayers.Control.DragFeatureFMS( fixmystreet.markers, { onStart: function(feature, e) { // Keep track of where the feature started, so we can put it // back if the user cancels the operation. @@ -167,12 +168,41 @@ var fixmystreet = fixmystreet || {}; } ); fixmystreet.map.addControl( drag ); drag.activate(); + }, + + // `markers.redraw()` in markers_highlight will trigger an + // `overFeature` event if the mouse cursor is still over the same + // marker on the map, which would then run markers_highlight + // again, causing an infinite flicker while the cursor remains over + // the same marker. We really only want to redraw the markers when + // the cursor moves from one marker to another (ie: when there is an + // overFeature followed by an outFeature followed by an overFeature). + // Therefore, we keep track of the previous event in + // fixmystreet.latest_map_hover_event and only call markers_highlight + // if we know the previous event was different to the current one. + // (See the `overFeature` and `outFeature` callbacks inside of + // fixmystreet.select_feature). + + markers_highlight: function(problem_id) { + for (var i = 0; i < fixmystreet.markers.features.length; i++) { + if (typeof problem_id == 'undefined') { + // There is no highlighted marker, so unfade this marker + fixmystreet.markers.features[i].attributes.faded = 0; + } else if (problem_id == fixmystreet.markers.features[i].attributes.id) { + // This is the highlighted marker, unfade it + fixmystreet.markers.features[i].attributes.faded = 0; + } else { + // This is not the hightlighted marker, fade it + fixmystreet.markers.features[i].attributes.faded = 1; + } + } + fixmystreet.markers.redraw(); } }; var drag = { activate: function() { - this._drag = new OpenLayers.Control.DragFeature( fixmystreet.markers, { + this._drag = new OpenLayers.Control.DragFeatureFMS( fixmystreet.markers, { onComplete: function(feature, e) { fixmystreet.update_pin( feature.geometry ); } @@ -195,35 +225,6 @@ var fixmystreet = fixmystreet || {}; fixmystreet.map.setCenter(center, z); } - // `markers.redraw()` in markers_highlight will trigger an - // `overFeature` event if the mouse cursor is still over the same - // marker on the map, which would then run markers_highlight - // again, causing an infinite flicker while the cursor remains over - // the same marker. We really only want to redraw the markers when - // the cursor moves from one marker to another (ie: when there is an - // overFeature followed by an outFeature followed by an overFeature). - // Therefore, we keep track of the previous event in - // fixmystreet.latest_map_hover_event and only call markers_highlight - // if we know the previous event was different to the current one. - // (See the `overFeature` and `outFeature` callbacks inside of - // fixmystreet.select_feature). - - function markers_highlight(problem_id) { - for (var i = 0; i < fixmystreet.markers.features.length; i++) { - if (typeof problem_id == 'undefined') { - // There is no highlighted marker, so unfade this marker - fixmystreet.markers.features[i].attributes.faded = 0; - } else if (problem_id == fixmystreet.markers.features[i].attributes.id) { - // This is the highlighted marker, unfade it - fixmystreet.markers.features[i].attributes.faded = 0; - } else { - // This is not the hightlighted marker, fade it - fixmystreet.markers.features[i].attributes.faded = 1; - } - } - fixmystreet.markers.redraw(); - } - function sidebar_highlight(problem_id) { if (typeof problem_id !== 'undefined') { var $a = $('.item-list--reports a[href$="/' + problem_id + '"]'); @@ -505,7 +506,7 @@ var fixmystreet = fixmystreet || {}; overFeature: function (feature) { if (fixmystreet.latest_map_hover_event != 'overFeature') { document.getElementById('map').style.cursor = 'pointer'; - markers_highlight(feature.attributes.id); + fixmystreet.maps.markers_highlight(feature.attributes.id); sidebar_highlight(feature.attributes.id); fixmystreet.latest_map_hover_event = 'overFeature'; } @@ -513,7 +514,7 @@ var fixmystreet = fixmystreet || {}; outFeature: function (feature) { if (fixmystreet.latest_map_hover_event != 'outFeature') { document.getElementById('map').style.cursor = ''; - markers_highlight(); + fixmystreet.maps.markers_highlight(); sidebar_highlight(); fixmystreet.latest_map_hover_event = 'outFeature'; } @@ -666,9 +667,9 @@ var fixmystreet = fixmystreet || {}; var href = $('a', this).attr('href'); var id = parseInt(href.replace(/^.*[/]([0-9]+)$/, '$1')); clearTimeout(timeout); - markers_highlight(id); + fixmystreet.maps.markers_highlight(id); }).on('mouseleave', '.item-list--reports__item', function(){ - timeout = setTimeout(markers_highlight, 50); + timeout = setTimeout(fixmystreet.maps.markers_highlight, 50); }); })(); }); @@ -888,6 +889,19 @@ OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, { } }); +/* Drag handler that allows individual features to disable dragging */ +OpenLayers.Control.DragFeatureFMS = OpenLayers.Class(OpenLayers.Control.DragFeature, { + CLASS_NAME: "OpenLayers.Control.DragFeatureFMS", + + overFeature: function(feature) { + if (feature.attributes.draggable) { + return OpenLayers.Control.DragFeature.prototype.overFeature.call(this, feature); + } else { + return false; + } + } +}) + OpenLayers.Renderer.SVGBig = OpenLayers.Class(OpenLayers.Renderer.SVG, { MAX_PIXEL: 15E7, CLASS_NAME: "OpenLayers.Renderer.SVGBig" diff --git a/web/js/map-google.js b/web/js/map-google.js index 4c3f6188e..be2df8502 100644 --- a/web/js/map-google.js +++ b/web/js/map-google.js @@ -56,6 +56,9 @@ fixmystreet.maps = {}; google.maps.event.trigger(fixmystreet.map, 'idle'); }; + // This is a noop on Google Maps right now. + fixmystreet.maps.markers_highlight = function() {}; + function PaddingControl(div) { div.style.padding = '40px'; } |