diff options
-rwxr-xr-x | bin/update-schema | 1 | ||||
-rw-r--r-- | db/downgrade_0042---0041.sql | 1 | ||||
-rw-r--r-- | db/schema.sql | 8 | ||||
-rw-r--r-- | db/schema_0042-planned_reports.sql | 11 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/My.pm | 72 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/AdminLog.pm | 6 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Problem.pm | 10 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/ResponseTemplate.pm | 5 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/User.pm | 46 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/UserPlannedReport.pm | 55 | ||||
-rw-r--r-- | perllib/FixMyStreet/TestMech.pm | 1 | ||||
-rw-r--r-- | t/app/controller/my.t | 15 | ||||
-rw-r--r-- | t/app/controller/my_planned.t | 52 | ||||
-rw-r--r-- | t/app/model/user_planned_report.t | 34 | ||||
-rw-r--r-- | templates/web/base/my/planned.html | 32 | ||||
-rw-r--r-- | templates/web/base/report/_main.html | 17 | ||||
-rw-r--r-- | web/cobrands/fixmystreet/fixmystreet.js | 21 |
17 files changed, 364 insertions, 23 deletions
diff --git a/bin/update-schema b/bin/update-schema index 99071f3ea..09155fa03 100755 --- a/bin/update-schema +++ b/bin/update-schema @@ -194,6 +194,7 @@ else { # By querying the database schema, we can see where we're currently at # (assuming schema change files are never half-applied, which should be the case) sub get_db_version { + return '0042' if table_exists('user_planned_reports'); return '0041' if column_exists('users', 'is_superuser') && ! constraint_exists('user_body_permissions_permission_type_check'); return '0040' if column_exists('users', 'is_superuser'); return '0039' if column_exists('users', 'facebook_id'); diff --git a/db/downgrade_0042---0041.sql b/db/downgrade_0042---0041.sql new file mode 100644 index 000000000..507ab22c4 --- /dev/null +++ b/db/downgrade_0042---0041.sql @@ -0,0 +1 @@ +drop table user_planned_reports; diff --git a/db/schema.sql b/db/schema.sql index f285922ac..48f0fd07e 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -458,6 +458,14 @@ create table user_body_permissions ( unique(user_id, body_id, permission_type) ); +create table user_planned_reports ( + id serial not null primary key, + user_id int references users(id) not null, + report_id int references problem(id) not null, + added timestamp not null default current_timestamp, + removed timestamp +); + create table response_templates ( id serial not null primary key, body_id int references body(id) not null, diff --git a/db/schema_0042-planned_reports.sql b/db/schema_0042-planned_reports.sql new file mode 100644 index 000000000..55ad1cc64 --- /dev/null +++ b/db/schema_0042-planned_reports.sql @@ -0,0 +1,11 @@ +begin; + +create table user_planned_reports ( + id serial not null primary key, + user_id int references users(id) not null, + report_id int references problem(id) not null, + added timestamp not null default current_timestamp, + removed timestamp +); + +commit; diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm index 72391fee2..b15750c98 100644 --- a/perllib/FixMyStreet/App/Controller/My.pm +++ b/perllib/FixMyStreet/App/Controller/My.pm @@ -2,6 +2,8 @@ package FixMyStreet::App::Controller::My; use Moose; use namespace::autoclean; +use JSON::MaybeXS; + BEGIN { extends 'Catalyst::Controller'; } =head1 NAME @@ -16,6 +18,11 @@ Catalyst Controller. =cut +sub begin : Private { + my ($self, $c) = @_; + $c->detach( '/auth/redirect' ) unless $c->user; +} + =head2 index =cut @@ -23,10 +30,28 @@ Catalyst Controller. sub my : Path : Args(0) { my ( $self, $c ) = @_; - $c->detach( '/auth/redirect' ) unless $c->user; + $c->stash->{problems_rs} = $c->cobrand->problems->search( + { user_id => $c->user->id }); + $c->forward('get_problems'); + $c->forward('get_updates'); + $c->forward('setup_page_data'); +} + +sub planned : Local : Args(0) { + my ( $self, $c ) = @_; + + $c->detach('/page_error_403_access_denied', []) + unless $c->user->from_body && $c->user->has_permission_to('planned_reports', $c->user->from_body->id); + + $c->stash->{problems_rs} = $c->user->active_planned_reports; + $c->forward('get_problems'); + $c->forward('setup_page_data'); +} + +sub get_problems : Private { + my ($self, $c) = @_; my $p_page = $c->get_param('p') || 1; - my $u_page = $c->get_param('u') || 1; $c->forward( '/reports/stash_report_filter_status' ); @@ -36,7 +61,6 @@ sub my : Path : Args(0) { my $states = $c->stash->{filter_problem_states}; my $params = { state => [ keys %$states ], - user_id => $c->user->id, }; my $category = $c->get_param('filter_category'); @@ -45,7 +69,7 @@ sub my : Path : Args(0) { $c->stash->{filter_category} = $category; } - my $rs = $c->cobrand->problems->search( $params, { + my $rs = $c->stash->{problems_rs}->search( $params, { order_by => { -desc => 'confirmed' }, rows => 50 } )->page( $p_page ); @@ -57,8 +81,14 @@ sub my : Path : Args(0) { } $c->stash->{problems_pager} = $rs->pager; $c->stash->{problems} = $problems; + $c->stash->{pins} = $pins; +} + +sub get_updates : Private { + my ($self, $c) = @_; - $rs = $c->user->comments->search( + my $u_page = $c->get_param('u') || 1; + my $rs = $c->user->comments->search( { state => 'confirmed' }, { order_by => { -desc => 'confirmed' }, @@ -69,8 +99,12 @@ sub my : Path : Args(0) { $c->stash->{has_content} += scalar @updates; $c->stash->{updates} = \@updates; $c->stash->{updates_pager} = $rs->pager; +} - my @categories = $c->cobrand->problems->search( { user_id => $c->user->id }, { +sub setup_page_data : Private { + my ($self, $c) = @_; + + my @categories = $c->stash->{problems_rs}->search({}, { columns => [ 'category' ], distinct => 1, order_by => [ 'category' ], @@ -79,6 +113,7 @@ sub my : Path : Args(0) { $c->stash->{filter_categories} = \@categories; $c->stash->{page} = 'my'; + my $pins = $c->stash->{pins}; FixMyStreet::Map::display_map( $c, latitude => $pins->[0]{latitude}, @@ -89,6 +124,31 @@ sub my : Path : Args(0) { if @$pins; } +sub planned_change : Path('planned/change') { + my ($self, $c) = @_; + $c->forward('/auth/check_csrf_token'); + + my $id = $c->get_param('id'); + $c->forward( '/report/load_problem_or_display_error', [ $id ] ); + + my $change = $c->get_param('change'); + $c->detach('/page_error_403_access_denied', []) + unless $change && $change =~ /add|remove/; + + if ($change eq 'add') { + $c->user->add_to_planned_reports($c->stash->{problem}); + } elsif ($change eq 'remove') { + $c->user->remove_from_planned_reports($c->stash->{problem}); + } + + if ($c->get_param('ajax')) { + $c->res->content_type('application/json; charset=utf-8'); + $c->res->body(encode_json({ outcome => $change })); + } else { + $c->res->redirect( $c->uri_for_action('report/display', $id) ); + } +} + __PACKAGE__->meta->make_immutable; 1; diff --git a/perllib/FixMyStreet/DB/Result/AdminLog.pm b/perllib/FixMyStreet/DB/Result/AdminLog.pm index d60915cfc..1c9bd3a63 100644 --- a/perllib/FixMyStreet/DB/Result/AdminLog.pm +++ b/perllib/FixMyStreet/DB/Result/AdminLog.pm @@ -38,7 +38,7 @@ __PACKAGE__->add_columns( "reason", { data_type => "text", default_value => "", is_nullable => 0 }, "time_spent", - { data_type => "integer", default_value => "0", is_nullable => 0 }, + { data_type => "integer", default_value => 0, is_nullable => 0 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->belongs_to( @@ -54,7 +54,7 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:RCi1FEwb9T2MZ2X+QOTTUA +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-20 14:38:36 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:y2xZ4BDv7H+f4vbIZyNflw 1; diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index 010b7755b..308fba71f 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -132,10 +132,16 @@ __PACKAGE__->belongs_to( { id => "user_id" }, { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, ); +__PACKAGE__->has_many( + "user_planned_reports", + "FixMyStreet::DB::Result::UserPlannedReport", + { "foreign.report_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Go+T9oFRfwQ1Ag89qPpF/g +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-20 15:00:41 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PMOhd1uloLTAYovW/fxgSg # Add fake relationship to stored procedure table __PACKAGE__->has_one( diff --git a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm index 48a1ab3ae..d189bf3ec 100644 --- a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm +++ b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm @@ -29,6 +29,7 @@ __PACKAGE__->add_columns( data_type => "timestamp", default_value => \"current_timestamp", is_nullable => 0, + original => { default_value => \"now()" }, }, ); __PACKAGE__->set_primary_key("id"); @@ -41,8 +42,8 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-02-19 16:13:43 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xzhmxtu0taAnBMZN0HBocw +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-20 14:38:36 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ECFQLMxOFGwv7cwfHLlszw # You can replace this text with custom code or comments, and it will be preserved on regeneration diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm index 7d1785c4b..cc8e050da 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -90,10 +90,21 @@ __PACKAGE__->has_many( { "foreign.user_id" => "self.id" }, { cascade_copy => 0, cascade_delete => 0 }, ); +__PACKAGE__->has_many( + "user_planned_reports", + "FixMyStreet::DB::Result::UserPlannedReport", + { "foreign.user_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-11 12:49:31 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SG86iN6Fr4/JIq7U2zYkug +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-20 15:00:41 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+pEOZ8GM14D4gqkp+fr+ZA + +use Moo; +use mySociety::EmailUtil; + +__PACKAGE__->many_to_many( planned_reports => 'user_planned_reports', 'report' ); __PACKAGE__->add_columns( "password" => { @@ -104,8 +115,6 @@ __PACKAGE__->add_columns( }, ); -use mySociety::EmailUtil; - sub latest_anonymity { my $self = shift; my $p = $self->problems->search(undef, { order_by => { -desc => 'id' } } )->first; @@ -277,4 +286,33 @@ sub adopt { $other->delete; } +# Planned reports + +# Override the default auto-created function as we only want one live entry per user +around add_to_planned_reports => sub { + my ( $orig, $self ) = ( shift, shift ); + my ( $report_col ) = @_; + my $existing = $self->user_planned_reports->search_rs({ report_id => $report_col->{id}, removed => undef })->first; + return $existing if $existing; + return $self->$orig(@_); +}; + +# Override the default auto-created function as we don't want to ever delete anything +around remove_from_planned_reports => sub { + my ($orig, $self, $report) = @_; + $self->user_planned_reports + ->search_rs({ report_id => $report->id, removed => undef }) + ->update({ removed => \'current_timestamp' }); +}; + +sub active_planned_reports { + my $self = shift; + $self->planned_reports->search({ removed => undef }); +} + +sub is_planned_report { + my ($self, $problem) = @_; + return $self->active_planned_reports->find({ id => $problem->id }); +} + 1; diff --git a/perllib/FixMyStreet/DB/Result/UserPlannedReport.pm b/perllib/FixMyStreet/DB/Result/UserPlannedReport.pm new file mode 100644 index 000000000..1e893c7a9 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/UserPlannedReport.pm @@ -0,0 +1,55 @@ +use utf8; +package FixMyStreet::DB::Result::UserPlannedReport; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; +__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn"); +__PACKAGE__->table("user_planned_reports"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "user_planned_reports_id_seq", + }, + "user_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "report_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "added", + { + data_type => "timestamp", + default_value => \"current_timestamp", + is_nullable => 0, + original => { default_value => \"now()" }, + }, + "removed", + { data_type => "timestamp", is_nullable => 1 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->belongs_to( + "report", + "FixMyStreet::DB::Result::Problem", + { id => "report_id" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); +__PACKAGE__->belongs_to( + "user", + "FixMyStreet::DB::Result::User", + { id => "user_id" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-07-20 15:03:08 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:mv7koDhvZSBW/4aQivtpAQ + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm index 45f32f46a..c3583bb3e 100644 --- a/perllib/FixMyStreet/TestMech.pm +++ b/perllib/FixMyStreet/TestMech.pm @@ -157,6 +157,7 @@ sub delete_user { for my $p ( $user->problems ) { $p->comments->delete; $p->questionnaires->delete; + $p->user_planned_reports->delete; $p->delete; } for my $a ( $user->alerts ) { diff --git a/t/app/controller/my.t b/t/app/controller/my.t index d24a66c8e..00070ed81 100644 --- a/t/app/controller/my.t +++ b/t/app/controller/my.t @@ -10,16 +10,19 @@ $mech->get_ok('/my'); is $mech->uri->path, '/auth', "got sent to the sign in page"; $mech->create_problems_for_body(1, 1234, 'Test Title'); +my $other_user = FixMyStreet::DB->resultset('User')->find_or_create({ email => 'another@example.com' }); +$mech->create_problems_for_body(1, 1234, 'Another Title', { user => $other_user }); -# sign in my $user = $mech->log_in_ok( 'test@example.com' ); $mech->get_ok('/my'); -is $mech->uri->path, '/my', "stayed on '/my/' page"; +is $mech->uri->path, '/my', "stayed on '/my' page"; -# Report listed $mech->content_contains('Test Title'); +$mech->content_lacks('Another Title'); -# cleanup -$mech->delete_user( $user ); -$mech->delete_problems_for_body(1234); done_testing(); + +END { + $mech->delete_user($user); + $mech->delete_user($other_user); +} diff --git a/t/app/controller/my_planned.t b/t/app/controller/my_planned.t new file mode 100644 index 000000000..25f82224e --- /dev/null +++ b/t/app/controller/my_planned.t @@ -0,0 +1,52 @@ +use strict; +use warnings; + +use Test::More; + +use FixMyStreet::TestMech; +my $mech = FixMyStreet::TestMech->new; + +$mech->get_ok('/my/planned'); +is $mech->uri->path, '/auth', "got sent to the sign in page"; + +my $body = $mech->create_body_ok(2237, 'Oxfordshire'); +my ($problem) = $mech->create_problems_for_body(1, $body->id, 'Test Title'); + +$mech->get_ok($problem->url); +$mech->content_lacks('Add to planned reports'); +$mech->content_lacks('Remove from planned reports'); + +my $user = $mech->log_in_ok( 'test@example.com' ); +$user->update({ from_body => $body }); +$user->user_body_permissions->find_or_create({ + body => $body, + permission_type => 'planned_reports', +}); + +$mech->get_ok('/my/planned'); +$mech->content_lacks('Test Title'); + +$user->add_to_planned_reports($problem); +$mech->get_ok('/my/planned'); +$mech->content_contains('Test Title'); + +$user->remove_from_planned_reports($problem); +$mech->get_ok('/my/planned'); +$mech->content_lacks('Test Title'); + +$user->add_to_planned_reports($problem); +$mech->get_ok('/my/planned'); +$mech->content_contains('Test Title'); + +$mech->get_ok($problem->url); +$mech->content_contains('Remove from planned reports'); +$mech->submit_form_ok({ with_fields => { change => 'remove' } }); +$mech->content_contains('Add to planned reports'); +$mech->submit_form_ok({ with_fields => { change => 'add' } }); +$mech->content_contains('Remove from planned reports'); + +done_testing(); + +END { + $mech->delete_user($user); +} diff --git a/t/app/model/user_planned_report.t b/t/app/model/user_planned_report.t new file mode 100644 index 000000000..6c0823044 --- /dev/null +++ b/t/app/model/user_planned_report.t @@ -0,0 +1,34 @@ +use strict; +use warnings; + +use Test::More; + +use FixMyStreet::TestMech; +use FixMyStreet::DB; + +my $mech = FixMyStreet::TestMech->new(); + +my @problems = $mech->create_problems_for_body(1, 2237, 'Title'); +my $problem = $problems[0]; +my $user = $problem->user; + +is $user->active_planned_reports, 0; +is $user->planned_reports, 0; + +$user->add_to_planned_reports($problem); +is $user->active_planned_reports, 1; +is $user->planned_reports, 1; + +$user->remove_from_planned_reports($problem); +is $user->active_planned_reports, 0; +is $user->planned_reports, 1; + +$user->add_to_planned_reports($problem); +is $user->active_planned_reports, 1; +is $user->planned_reports, 2; + +done_testing(); + +END { + $mech->delete_user($user); +} diff --git a/templates/web/base/my/planned.html b/templates/web/base/my/planned.html new file mode 100644 index 000000000..2e852ea01 --- /dev/null +++ b/templates/web/base/my/planned.html @@ -0,0 +1,32 @@ +[% + SET bodyclass = 'mappage'; + PROCESS "maps/${map.type}.html" IF problems.size; + INCLUDE 'header.html', title = loc('Your planned reports') +%] + +[% IF problems.size %] + [% map_html %] + </div> + <div id="map_sidebar"> + <div id="side"> +[% ELSE %] + <div id="map_sidebar"> + <div id="skipped-map"> +[% END %] + +<h1>[% loc('Your planned reports') %]</h1> + +[% IF ! has_content %] +[% loc('You haven’t planned any reports yet.') %] +[% END %] + +<section class="full-width"> +[% INCLUDE "reports/_list-filters.html", use_form_wrapper = 1 %] +[% INCLUDE 'pagination.html', pager = problems_pager, param = 'p' %] +[% INCLUDE 'my/_problem-list.html' %] +</section> + + </div> + </div> + +[% INCLUDE 'footer.html' %] diff --git a/templates/web/base/report/_main.html b/templates/web/base/report/_main.html index dfcfb8ce4..ffda48074 100644 --- a/templates/web/base/report/_main.html +++ b/templates/web/base/report/_main.html @@ -5,6 +5,23 @@ <div class="problem-header clearfix" problem-id="[% problem.id %]"> +[% IF c.user.has_permission_to('planned_reports', problem.bodies_str) %] +<form method="post" action="/my/planned/change" id="planned_form"> + <input type="hidden" name="id" value="[% problem.id %]"> + <input type="hidden" name="token" value="[% csrf_token %]"> + <input type="hidden" name="change" value="[% IF c.user.is_planned_report(problem) %]remove[% ELSE %]add[% END %]"> + <p><input type="submit" + data-remove="[% loc('Remove from planned reports') %]" data-add="[% loc('Add to planned reports') %]" + value=" + [%~ IF c.user.is_planned_report(problem) ~%] + [% loc('Remove from planned reports') %] + [%~ ELSE ~%] + [% loc('Add to planned reports') %] + [%~ END ~%] + "></p> +</form> +[% END %] + [% IF moderating %] [% original = problem_original %] <form method="post" action="/moderate/report/[% problem.id %]"> diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js index 8935712cc..b3f6f5e60 100644 --- a/web/cobrands/fixmystreet/fixmystreet.js +++ b/web/cobrands/fixmystreet/fixmystreet.js @@ -301,6 +301,27 @@ $.extend(fixmystreet.set_up, { $('#form_rznvy').removeClass(); $('#email').removeClass(); }); + + $('#planned_form').submit(function(e) { + if (e.metaKey || e.ctrlKey) { + return; + } + e.preventDefault(); + var $form = $(this), + $change = $form.find("input[name='change']" ), + $submit = $form.find("input[type='submit']" ), + data = $form.serialize() + '&ajax=1'; + + $.post(this.action, data, function(data) { + if (data.outcome == 'add') { + $change.val('remove'); + $submit.val($submit.data('remove')); + } else if (data.outcome == 'remove') { + $change.val('add'); + $submit.val($submit.data('add')); + } + }); + }); }, geolocation: function() { |