From 8002200321139053ed4787074ea1c2b80db6a8a4 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 22 Dec 2016 09:27:24 +0000 Subject: [PATCH 001/311] Render status text from ruby. --- app/views/widgets/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/widgets/show.html.erb b/app/views/widgets/show.html.erb index a3c8b72173..91f82a2f17 100644 --- a/app/views/widgets/show.html.erb +++ b/app/views/widgets/show.html.erb @@ -20,7 +20,7 @@ <%= link_to @info_request.title, request_path(@info_request), :target => "_top" %>
-

_("Status")

+

<%=_("Status") %>

<%= status_description(@info_request, @status) %>
From 21a03aa91e716d78a26e3084beb2434b031f1a6c Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 23 Dec 2016 10:58:39 +0000 Subject: [PATCH 002/311] Remove deprecated stub syntax. --- spec/controllers/user_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb index 67a34eb4d5..a5601fb7f0 100644 --- a/spec/controllers/user_controller_spec.rb +++ b/spec/controllers/user_controller_spec.rb @@ -755,7 +755,7 @@ def do_signin(email, password) limiter = double allow(limiter).to receive(:record) allow(limiter).to receive(:limit?).and_return(true) - controller.stub(:ip_rate_limiter).and_return(limiter) + allow(controller).to receive(:ip_rate_limiter).and_return(limiter) end it 'blocks the signup' do From 56e553696dd8e0628b8fc6eb9326b99565bb3f8f Mon Sep 17 00:00:00 2001 From: Gareth Rees Date: Wed, 11 Jan 2017 10:02:53 +0000 Subject: [PATCH 003/311] Clarify admin info message 9e215bd added a note to address the problem where we can't fall back to the default locale's request_email attribute. What I didn't understand at the time was that it _does_ fall back when there are _no_ translations. This is because when there are no translations for a locale, there's no PublicBody::Translation record. When there are translations, it just finds an empty field from the record and returns that. Ticket https://github.com/mysociety/alaveteli/issues/3134. --- app/views/admin_public_body/edit.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/admin_public_body/edit.html.erb b/app/views/admin_public_body/edit.html.erb index 921204919f..11a573820f 100644 --- a/app/views/admin_public_body/edit.html.erb +++ b/app/views/admin_public_body/edit.html.erb @@ -31,7 +31,8 @@ <% if @public_body.ordered_translations.many? %>
- Make sure you add the request email for all locales. + Make sure you add the request email for all locales that have a + translation.
<% end %> From aa317212a616557f4cc59ecc0d184007cb4f7776 Mon Sep 17 00:00:00 2001 From: Gareth Rees Date: Thu, 19 Jan 2017 13:40:09 +0000 Subject: [PATCH 004/311] Prevent body name overflowing on stats axis labels For the axis labels prefer the body short name (full name appears in the tooltip, and the HTML table), and truncate it if its still too long. Fixes https://github.com/mysociety/alaveteli/issues/1485 Fixes https://github.com/mysociety/asktheeu-theme/issues/60 --- app/models/statistics.rb | 2 +- doc/CHANGES.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/statistics.rb b/app/models/statistics.rb index e278a01195..a8ce2a9cc7 100644 --- a/app/models/statistics.rb +++ b/app/models/statistics.rb @@ -89,7 +89,7 @@ def simplify_stats_for_graphs(data, # tooltips, and so on: data['public_bodies'].each_with_index do |pb, i| result['x_values'] << i - result['x_ticks'] << [i, pb.name] + result['x_ticks'] << [i, pb.short_or_long_name.truncate(30)] result['tooltips'] << "#{pb.name} (#{result['totals'][i]})" result['public_bodies'] << { 'name' => pb.name, diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 005cb72a62..bf08e4073b 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -2,6 +2,8 @@ ## Highlighted Features +* Prevent long authority names overflowing on statistics page (Gareth Rees) + ## Upgrade Notes ### Changed Templates From 8247253e3aa4be394c076d98979a5e15a2d00702 Mon Sep 17 00:00:00 2001 From: lizconlan Date: Mon, 23 Jan 2017 10:41:22 +0000 Subject: [PATCH 005/311] Force visually-hidden elements to act as block-level elements --- app/assets/stylesheets/responsive/_utils.scss | 1 + doc/CHANGES.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/assets/stylesheets/responsive/_utils.scss b/app/assets/stylesheets/responsive/_utils.scss index 80d95b1aec..5011dacbd9 100644 --- a/app/assets/stylesheets/responsive/_utils.scss +++ b/app/assets/stylesheets/responsive/_utils.scss @@ -64,6 +64,7 @@ $lte-ie7: false !default; height: 1px !important; width: 1px !important; overflow: hidden; + display: block; } body:hover .visually-hidden a, body:hover .visually-hidden input, body:hover .visually-hidden button { display: none !important; } diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 8d21b559c5..1b98c103be 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -4,6 +4,8 @@ ## Highlighted Features * Prevent long authority names overflowing on statistics page (Gareth Rees) +* Fix css bug which allowed some "visually-hidden" elements to affect page + length (Liz Conlan) ## Upgrade Notes From 5a679aa60074b600cf0ccf21f4616f20b5f66588 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 6 Feb 2017 15:17:59 +0000 Subject: [PATCH 006/311] Add translators' note for string which can't contain double quotes This was added to app.po in 63055c520c48ec271b0e8d57352e683902824464, but that is erased when updating translation files from Transifex. This should persist. --- app/models/info_request.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 27209399f8..013046f351 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -740,6 +740,8 @@ def recipient_email_valid_for_followup? def recipient_name_and_email MailHandler.address_from_name_and_email( + # TRANSLATORS: Please don't use double quotes (") in this translation + # or it will break the site's ability to send emails to authorities! _("{{law_used}} requests at {{public_body}}", :law_used => law_used_human(:short), :public_body => public_body.short_or_long_name), From f9f0d30a94defb49353bf6bc0bbba5fd92dc760f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 6 Feb 2017 14:31:27 +0000 Subject: [PATCH 007/311] Update newrelic_rpm. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index f5ab771a2f..856072b324 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -215,7 +215,7 @@ GEM net-ssh (2.6.7) net-ssh-gateway (1.2.0) net-ssh (>= 2.6.5) - newrelic_rpm (3.17.1.326) + newrelic_rpm (3.18.0.329) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) From af672d19851df126ea101f899b5ab544070c6508 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 14 Oct 2016 14:49:37 +0100 Subject: [PATCH 008/311] Add some specs for InfoRequest#make_zip_cache_path --- spec/models/info_request_spec.rb | 138 +++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/spec/models/info_request_spec.rb b/spec/models/info_request_spec.rb index d103964455..5b9aabbaa8 100644 --- a/spec/models/info_request_spec.rb +++ b/spec/models/info_request_spec.rb @@ -2616,6 +2616,143 @@ def apply_filters(filters) end + describe "making a zip cache path for a user" do + let(:non_owner) { FactoryGirl.create(:user) } + let(:owner) { request.user } + let(:admin) { FactoryGirl.create(:admin_user) } + + let(:base_path) do + File.join(Rails.root, "cache", "zips", "test", "download", "123", + "123456", "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3") + end + let(:path) { File.join(base_path, "test.zip") } + let(:hidden_path) { File.join(base_path, "test_hidden.zip") } + let(:requester_only_path) { File.join(base_path, "test_requester_only.zip") } + + # Slightly confusing - this runs *after* the let(:request) in each context + # below, so it's ok + before do + # Digest::SHA1.hexdigest("test") + test_hash = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" + allow(request).to receive(:last_update_hash).and_return(test_hash) + end + + shared_examples_for "a situation when everything is public" do + it "doesn't add a suffix for anyone" do + expect(request.make_zip_cache_path(nil)).to eq (path) + expect(request.make_zip_cache_path(non_owner)).to eq (path) + expect(request.make_zip_cache_path(admin)).to eq (path) + expect(request.make_zip_cache_path(owner)).to eq (path) + end + end + + shared_examples_for "a situation when anything is not public" do + it "doesn't add a suffix for anonymous users" do + expect(request.make_zip_cache_path(nil)).to eq (path) + end + + it "doesn't add a suffix for non owner users" do + expect(request.make_zip_cache_path(non_owner)).to eq (path) + end + + it "adds a _hidden suffix for admin users" do + expect(request.make_zip_cache_path(admin)).to eq (hidden_path) + end + + it "adds a requester_only suffix for owner users" do + expect(request.make_zip_cache_path(owner)).to eq (requester_only_path) + end + end + + shared_examples_for "a request when any correspondence is not public" do + context "when an incoming message is hidden" do + before do + incoming = request.incoming_messages.first + incoming.prominence = "hidden" + incoming.save! + end + + it_behaves_like "a situation when anything is not public" + end + + context "when an incoming message is requester_only" do + before do + incoming = request.incoming_messages.first + incoming.prominence = "requester_only" + incoming.save! + end + + it_behaves_like "a situation when anything is not public" + end + + context "when an outgoing message is hidden" do + before do + outgoing = request.outgoing_messages.first + outgoing.prominence = "hidden" + outgoing.save! + end + + it_behaves_like "a situation when anything is not public" + end + + context "when an outgoing message is requester_only" do + before do + outgoing = request.outgoing_messages.first + outgoing.prominence = "requester_only" + outgoing.save! + end + + it_behaves_like "a situation when anything is not public" + end + end + + shared_examples_for "a request when anything is not public" do + context "when the request is not public but the correspondence is" do + it_behaves_like "a situation when anything is not public" + end + + context "when the request is not public and neither is the correspondence" do + it_behaves_like "a request when any correspondence is not public" + end + end + + context "when the request is public" do + let(:request) do + FactoryGirl.create(:info_request_with_incoming, id: 123456, + title: "test") + end + + context "when all correspondence is public" do + it_behaves_like "a situation when everything is public" + end + + it_behaves_like "a request when any correspondence is not public" + end + + context "when the request is hidden" do + let(:request) do + FactoryGirl.create(:info_request_with_incoming, id: 123456, + title: "test", + prominence: "hidden") + end + + it_behaves_like "a request when anything is not public" + end + + context "when the request is requester_only" do + let(:request) do + FactoryGirl.create( + :info_request_with_incoming, + id: 123456, + title: "test", + prominence: "requester_only" + ) + end + + it_behaves_like "a request when anything is not public" + end + end + def email_and_raw_email(opts = {}) raw_email = opts[:raw_email] || <<-EOF.strip_heredoc From: EMAIL_FROM @@ -2634,4 +2771,5 @@ def email_and_raw_email(opts = {}) [email, raw_email] end + end From ab9734ce6ee48a3d725bee3a72ef189335dcef80 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 14 Oct 2016 18:00:35 +0100 Subject: [PATCH 009/311] Update request controller specs for authorization Switching session ids during a single spec breaks CanCanCan, but these are better as single-expectation specs anyway, so I've separated them --- spec/controllers/request_controller_spec.rb | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index dec1a6b758..422c55093c 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -828,21 +828,25 @@ def expect_hidden(hidden_template) expect_hidden('hidden') end - it "should show request to requester and admin if logged in" do + it "should not show request if logged in but not the requester" do session[:user_id] = FactoryGirl.create(:user).id get :show, :url_title => @info_request.url_title expect_hidden('hidden') + end + it "should show request to requester" do session[:user_id] = @info_request.user.id get :show, :url_title => @info_request.url_title expect(response).to render_template('show') + end + it "shouild show request to admin" do session[:user_id] = FactoryGirl.create(:admin_user).id get :show, :url_title => @info_request.url_title expect(response).to render_template('show') end - it 'should not cache an attachment when showing an attachment to the requester or admin' do + it 'should not cache an attachment when showing an attachment to the requester' do session[:user_id] = @info_request.user.id incoming_message = @info_request.incoming_messages.first expect(@controller).not_to receive(:foi_fragment_cache_write) @@ -851,6 +855,16 @@ def expect_hidden(hidden_template) :part => 2, :file_name => 'interesting.pdf' end + + it 'should not cache an attachment when showing an attachment to the admin' do + session[:user_id] = FactoryGirl.create(:admin_user).id + incoming_message = @info_request.incoming_messages.first + expect(@controller).not_to receive(:foi_fragment_cache_write) + get :get_attachment, :incoming_message_id => incoming_message.id, + :id => @info_request.id, + :part => 2, + :file_name => 'interesting.pdf' + end end context 'when the incoming message has prominence hidden' do From ce5588139a779ef9b9ce9a5203f4492dde06e1ed Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 17 Oct 2016 16:38:06 +0100 Subject: [PATCH 010/311] Improve comments in request download spec --- spec/integration/download_request_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/integration/download_request_spec.rb b/spec/integration/download_request_spec.rb index b075e0cd0d..fdffcd30da 100644 --- a/spec/integration/download_request_spec.rb +++ b/spec/integration/download_request_spec.rb @@ -343,7 +343,7 @@ def sleep_and_receive_mail(name, info_request) it 'should not include the outgoing message in a download of the entire request by a non-request owner but should retain them for owner and admin' do - # Non-owner can download zip with incoming and attachments + # Non-owner can download zip with original message initially non_owner = login(FactoryGirl.create(:user)) info_request = FactoryGirl.create(:info_request) @@ -352,7 +352,7 @@ def sleep_and_receive_mail(name, info_request) expect(zip.read('correspondence.txt')).to match('Some information please') end - # Admin makes the incoming message requester only + # Admin makes the outgoing message requester only admin = login(FactoryGirl.create(:admin_user)) using_session(admin) do From 68d85fc8f4880630fcbdf83d54a7891cecb989a9 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 17 Oct 2016 17:02:42 +0100 Subject: [PATCH 011/311] Fix bug in old_unclassified_request factory The request this created wouldn't actually pass the is_old_unclassified_request? test because the incoming message isn't old enough. --- spec/factories/info_requests.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/spec/factories/info_requests.rb b/spec/factories/info_requests.rb index c961624e5d..7c06af4184 100644 --- a/spec/factories/info_requests.rb +++ b/spec/factories/info_requests.rb @@ -73,8 +73,20 @@ factory :old_unclassified_request do after(:create) do |info_request, evaluator| - incoming_message = FactoryGirl.create(:incoming_message, :info_request => info_request) - info_request.log_event("response", {:incoming_message_id => incoming_message.id}) + incoming_message = FactoryGirl.create( + :incoming_message, + :info_request => info_request, + :created_at => Time.now - 31.days + ) + info_request.info_request_events = [ + FactoryGirl.create( + :info_request_event, + :info_request => info_request, + :event_type => "response", + :incoming_message_id => incoming_message.id, + :created_at => Time.now - 31.days + ) + ] info_request.last_public_response_at = Time.now - 31.days info_request.awaiting_description = true info_request.save! From fa65048da5cde2cb863f53ed415abb7038c91c72 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 12 Oct 2016 08:45:09 +0100 Subject: [PATCH 012/311] Add CanCanCan gem an current_user method for it --- Gemfile | 1 + Gemfile.lock | 2 ++ app/controllers/application_controller.rb | 3 +++ 3 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index e6d0706ec0..c72580e06a 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'pg', '~> 0.18.4' # New gem releases aren't being done. master is newer and supports Rails > 3.0 gem 'acts_as_versioned', :git => 'https://github.com/technoweenie/acts_as_versioned.git', :ref => '63b1fc8529d028' gem 'active_model_otp', :git => 'https://github.com/heapsource/active_model_otp.git', :ref => 'c342283fe564bf' +gem 'cancancan', '1.12' # Pinned because 1.13 onwards don't support Ruby 1.9 gem 'charlock_holmes', '~> 0.7.3' gem 'dynamic_form', '~> 1.1.4' # 4.1.0 has a bug in it which is fixed in a later version which does not have Ruby 1.9.3 support diff --git a/Gemfile.lock b/Gemfile.lock index 856072b324..b4347e933c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,6 +83,7 @@ GEM bullet (5.1.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) + cancancan (1.12.0) capistrano (2.15.4) highline net-scp (>= 1.0.0) @@ -373,6 +374,7 @@ DEPENDENCIES annotate (~> 2.7.1) bootstrap-sass (~> 2.3.2.2) bullet (~> 5.1.0) + cancancan (= 1.12) capistrano (~> 2.15.4) capybara (~> 2.7.0) charlock_holmes (~> 0.7.3) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 40974b3068..401f5241c8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -294,6 +294,9 @@ def authenticated_user end end + # For CanCanCan and other libs which need a Devise-like current_user method + alias_method :current_user, :authenticated_user + # Do a POST redirect. This is a nasty hack - we store the posted values in # the session, and when the GET redirect with "?post_redirect=1" happens, # load them in. From 7ae22470de386f8171e35b6fc764ad72b09e2fc2 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 17 Oct 2016 16:45:03 +0100 Subject: [PATCH 013/311] Refactor lib/ability.rb into a CanCanCan Ability This turns all uses of the Ability class' methods into instances of can? or cannot? from CanCanCan. In addition, a couple of specs had to be modified because CanCanCan needs to see what class an object is (so it doesn't work with simple doubles fro RSpec). --- app/controllers/followups_controller.rb | 2 +- .../delivery_statuses_controller.rb | 4 +- app/controllers/request_controller.rb | 18 +- app/models/ability.rb | 66 ++++++++ app/models/info_request.rb | 36 ++-- .../request/_incoming_correspondence.html.erb | 4 +- .../request/_incoming_correspondence.text.erb | 2 +- .../request/_outgoing_correspondence.html.erb | 2 +- .../request/_outgoing_correspondence.text.erb | 2 +- lib/ability.rb | 17 -- lib/message_prominence.rb | 4 - .../delivery_statuses_controller_spec.rb | 44 ++--- spec/controllers/request_controller_spec.rb | 6 +- spec/lib/ability_spec.rb | 51 ------ spec/models/ability_spec.rb | 156 ++++++++++++++++++ spec/models/incoming_message_spec.rb | 57 ------- spec/models/outgoing_message_spec.rb | 52 ------ 17 files changed, 287 insertions(+), 236 deletions(-) create mode 100644 app/models/ability.rb delete mode 100644 lib/ability.rb delete mode 100644 spec/lib/ability_spec.rb create mode 100644 spec/models/ability_spec.rb diff --git a/app/controllers/followups_controller.rb b/app/controllers/followups_controller.rb index fb1011a51f..8bcfc3ab0d 100644 --- a/app/controllers/followups_controller.rb +++ b/app/controllers/followups_controller.rb @@ -91,7 +91,7 @@ def check_user_credentials render :template => 'user/banned' return end - if authenticated_user && !@info_request.user_can_view?(authenticated_user) + if authenticated_user && cannot?(:read, @info_request) return render_hidden end end diff --git a/app/controllers/outgoing_messages/delivery_statuses_controller.rb b/app/controllers/outgoing_messages/delivery_statuses_controller.rb index 14c32f91be..91be638bc2 100644 --- a/app/controllers/outgoing_messages/delivery_statuses_controller.rb +++ b/app/controllers/outgoing_messages/delivery_statuses_controller.rb @@ -26,8 +26,8 @@ def set_outgoing_message end def check_prominence - unless @outgoing_message.user_can_view?(@user) && - @outgoing_message.info_request.user_can_view?(@user) + unless can?(:read, @outgoing_message) && \ + can?(:read, @outgoing_message.info_request) return render_hidden('request/_hidden_correspondence', :locals => { :message => @outgoing_message }) end diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 4fc24e80f0..5e3edf441e 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -90,7 +90,7 @@ def show @info_request = InfoRequest.find_by_url_title!(params[:url_title]) # Test for whole request being hidden - if !@info_request.user_can_view?(authenticated_user) + if cannot?(:read, @info_request) return render_hidden end @@ -139,7 +139,7 @@ def show def details long_cache @info_request = InfoRequest.find_by_url_title!(params[:url_title]) - if !@info_request.user_can_view?(authenticated_user) + if cannot?(:read, @info_request) return render_hidden end @columns = ['id', 'event_type', 'created_at', 'described_state', 'last_described_at', 'calculated_state' ] @@ -158,7 +158,7 @@ def similar @info_request = InfoRequest.find_by_url_title!(params[:url_title]) raise ActiveRecord::RecordNotFound.new("Request not found") if @info_request.nil? - if !@info_request.user_can_view?(authenticated_user) + if cannot?(:read, @info_request) return render_hidden end @xapian_object = ActsAsXapian::Similar.new([InfoRequestEvent], @info_request.info_request_events, @@ -447,7 +447,7 @@ def describe_state end # Check authenticated, and parameters set. - unless Ability::can_update_request_state?(authenticated_user, info_request) + unless can?(:update_request_state, info_request) authenticated_as_user?( info_request.user, :web => _("To classify the response to this FOI request"), @@ -573,11 +573,11 @@ def authenticate_attachment # Test for hidden incoming_message = IncomingMessage.find(params[:incoming_message_id]) raise ActiveRecord::RecordNotFound.new("Message not found") if incoming_message.nil? - if !incoming_message.info_request.user_can_view?(authenticated_user) + if cannot?(:read, incoming_message.info_request) @info_request = incoming_message.info_request # used by view return render_hidden end - if !incoming_message.user_can_view?(authenticated_user) + if cannot?(:read, incoming_message) @incoming_message = incoming_message # used by view return render_hidden('request/hidden_correspondence') end @@ -696,7 +696,7 @@ def get_attachment_internal(html_conversion) end # check permissions - raise "internal error, pre-auth filter should have caught this" if !@info_request.user_can_view?(authenticated_user) + raise "internal error, pre-auth filter should have caught this" if cannot?(:read, @info_request) @attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename(@incoming_message.get_attachments_for_display, @part_number, @original_filename) # If we can't find the right attachment, redirect to the incoming message: unless @attachment @@ -805,7 +805,7 @@ def download_entire_request :info_request_title=>@info_request.title) ) # Test for whole request being hidden or requester-only - if !@info_request.user_can_view?(@user) + if cannot?(:read, @info_request) return render_hidden end cache_file_path = @info_request.make_zip_cache_path(@user) @@ -854,7 +854,7 @@ def make_request_zip(info_request, file_path) zipfile.get_output_stream(file_info[:filename]) { |f| f.puts(file_info[:data]) } message_index = 0 info_request.incoming_messages.each do |message| - next unless message.user_can_view?(authenticated_user) + next unless can?(:read, message) message_index += 1 message.get_attachments_for_display.each do |attachment| filename = "#{message_index}_#{attachment.url_part_number}_#{attachment.display_filename}" diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 0000000000..79350423c4 --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,66 @@ +class Ability + include CanCan::Ability + + def initialize(user) + # Define abilities for the passed in user here. For example: + # + # user ||= User.new # guest user (not logged in) + # if user.admin? + # can :manage, :all + # else + # can :read, :all + # end + # + # The first argument to `can` is the action you are giving the user + # permission to do. + # If you pass :manage it will apply to every action. Other common actions + # here are :read, :create, :update and :destroy. + # + # The second argument is the resource the user can perform the action on. + # If you pass :all it will apply to every resource. Otherwise pass a Ruby + # class of the resource. + # + # The third argument is an optional hash of conditions to further filter the + # objects. + # For example, here the user can only update published articles. + # + # can :update, Article, :published => true + # + # See the wiki for details: + # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities + + # Updating request status + can :update_request_state, InfoRequest do |request| + self.class.can_update_request_state?(user, request) + end + + # Viewing messages with prominence + can :read, [IncomingMessage, OutgoingMessage] do |msg| + self.class.can_view_with_prominence?(msg.prominence, + msg.info_request, + user) + end + + # Viewing requests with prominence + can :read, InfoRequest do |request| + self.class.can_view_with_prominence?(request.prominence, request, user) + end + end + + private + + def self.can_update_request_state?(user, request) + (user && request.is_old_unclassified?) || request.is_owning_user?(user) + end + + def self.can_view_with_prominence?(prominence, info_request, user) + if prominence == 'hidden' + return User.view_hidden?(user) + end + if prominence == 'requester_only' + return info_request.is_owning_user?(user) + end + return true + end + +end diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 013046f351..0b768ec489 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -1018,22 +1018,36 @@ def download_zip_dir end def make_zip_cache_path(user) + # The zip file varies depending on user because it can include different + # messages depending on whether the user can access hidden or + # requester_only messages. We name it appropriately, so that every user + # with the right permissions gets a file with only the right things in. cache_file_dir = File.join(InfoRequest.download_zip_dir, "download", request_dirs, last_update_hash) - cache_file_suffix = if all_can_view_all_correspondence? - "" - elsif Ability.can_view_with_prominence?('hidden', self, user) - "_hidden" - elsif Ability.can_view_with_prominence?('requester_only', self, user) - "_requester_only" - else - "" - end + cache_file_suffix = zip_cache_file_suffix(user) File.join(cache_file_dir, "#{url_title}#{cache_file_suffix}.zip") end + def zip_cache_file_suffix(user) + # Simple short circuit for requests where everything is public + if all_can_view_all_correspondence? + "" + # If the user can view hidden things, they can view anything, so no need + # to go any further + elsif User.view_hidden?(user) + "_hidden" + # If the user can't view hidden things, but owns the request, they can + # see more than the public, so they get requester_only + elsif is_owning_user?(user) + "_requester_only" + # Everyone else can only see public stuff, which is the default case + else + "" + end + end + def is_old_unclassified? !is_external? && awaiting_description && url_title != 'holding_pen' && get_last_public_response_event && Time.now > get_last_public_response_event.created_at + OLD_AGE_IN_DAYS @@ -1120,10 +1134,6 @@ def is_actual_owning_user?(user) user.id == user_id end - def user_can_view?(user) - Ability.can_view_with_prominence?(prominence, self, user) - end - # Is this request visible to everyone? def all_can_view? %w(normal backpage).include?(prominence) diff --git a/app/views/request/_incoming_correspondence.html.erb b/app/views/request/_incoming_correspondence.html.erb index cfafb484fd..d993e0ad0d 100644 --- a/app/views/request/_incoming_correspondence.html.erb +++ b/app/views/request/_incoming_correspondence.html.erb @@ -15,7 +15,7 @@ - <%- if not incoming_message.user_can_view?(@user) %> + <%- if cannot?(:read, incoming_message) %> <%= render :partial => 'request/hidden_correspondence', :locals => { :message => incoming_message } %> <%- else %> <%= render :partial => 'request/restricted_correspondence', :locals => {:message => incoming_message } %> @@ -36,6 +36,6 @@ - + <%- end %> diff --git a/app/views/request/_incoming_correspondence.text.erb b/app/views/request/_incoming_correspondence.text.erb index 0d85f9246c..5ec57af7eb 100644 --- a/app/views/request/_incoming_correspondence.text.erb +++ b/app/views/request/_incoming_correspondence.text.erb @@ -1,4 +1,4 @@ -<%- if not incoming_message.user_can_view?(@user) %> +<%- if cannot?(:read, incoming_message) %> <%= render :partial => 'request/hidden_correspondence', :formats => 'text', :locals => { :message => incoming_message }%> <%- else %> <%= _('From:') %><% if incoming_message.specific_from_name? %> <%= incoming_message.safe_mail_from %><% end %><% if incoming_message.from_public_body? %>, <%= @info_request.public_body.name %><% end %> diff --git a/app/views/request/_outgoing_correspondence.html.erb b/app/views/request/_outgoing_correspondence.html.erb index 087418d461..3ca1823e0f 100644 --- a/app/views/request/_outgoing_correspondence.html.erb +++ b/app/views/request/_outgoing_correspondence.html.erb @@ -1,5 +1,5 @@
- <%- if not outgoing_message.user_can_view?(@user) %> + <%- if cannot?(:read, outgoing_message) %> <%= render :partial => 'request/hidden_correspondence', :locals => { :message => outgoing_message } %> <%- else %> <%= render :partial => 'request/restricted_correspondence', :locals => {:message => outgoing_message } %> diff --git a/app/views/request/_outgoing_correspondence.text.erb b/app/views/request/_outgoing_correspondence.text.erb index 221e359e2d..1a41db311d 100644 --- a/app/views/request/_outgoing_correspondence.text.erb +++ b/app/views/request/_outgoing_correspondence.text.erb @@ -1,4 +1,4 @@ -<%- if not outgoing_message.user_can_view?(@user) %> +<%- if cannot?(:read, outgoing_message) %> <%= render :partial => 'request/hidden_correspondence', :formats => 'text', :locals => { :message => outgoing_message }%> <%- else %> <%= _('From:') %> <% if @info_request.user_name %><%= @info_request.user_name %><% else %><%= "[#{_('An anonymous user')}]"%><% end %> diff --git a/lib/ability.rb b/lib/ability.rb deleted file mode 100644 index c58950c0da..0000000000 --- a/lib/ability.rb +++ /dev/null @@ -1,17 +0,0 @@ -# -*- encoding : utf-8 -*- -module Ability - def self.can_update_request_state?(user, request) - (user && request.is_old_unclassified?) || request.is_owning_user?(user) - end - - def self.can_view_with_prominence?(prominence, info_request, user) - if prominence == 'hidden' - return User.view_hidden?(user) - end - if prominence == 'requester_only' - return info_request.is_owning_user?(user) - end - return true - end - -end diff --git a/lib/message_prominence.rb b/lib/message_prominence.rb index af7f5a48ec..84b7934709 100644 --- a/lib/message_prominence.rb +++ b/lib/message_prominence.rb @@ -10,10 +10,6 @@ def has_prominence module InstanceMethods - def user_can_view?(user) - Ability.can_view_with_prominence?(self.prominence, self.info_request, user) - end - def indexed_by_search? self.prominence == 'normal' end diff --git a/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb b/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb index e0f4c7b68d..f50daf849e 100644 --- a/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb +++ b/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb @@ -21,8 +21,8 @@ it 'assigns the outgoing message' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -36,8 +36,8 @@ it 'renders hidden when the message cannot be viewed' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => false, - :info_request => double(:user_can_view? => true), + :prominence => 'hidden', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -51,8 +51,8 @@ it 'renders hidden when the request cannot be viewed' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => false), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'hidden'}), :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -66,8 +66,8 @@ it 'sets the title' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -87,8 +87,8 @@ session[:user_id] = FactoryGirl.create(:admin_user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -102,8 +102,8 @@ it 'sets show_mail_server_logs to true if the user is an owner' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -117,8 +117,8 @@ it 'sets show_mail_server_logs to false if the user is not an owner' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -137,8 +137,8 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -157,8 +157,8 @@ session[:user_id] = FactoryGirl.create(:admin_user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -171,8 +171,8 @@ it 'does not assign mail server logs for a regular user' do attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -186,8 +186,8 @@ it 'renders the show template' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', - :user_can_view? => true, - :info_request => double(:user_can_view? => true), + :prominence => 'normal', + :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 422c55093c..679bd307e8 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2305,15 +2305,15 @@ def expect_redirect(status, redirect_path) describe RequestController, "when caching fragments" do it "should not fail with long filenames" do long_name = "blahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblah.txt" - info_request = double(InfoRequest, :user_can_view? => true, - :all_can_view? => true) + info_request = double(InfoRequest, :prominence => 'normal', + :all_can_view? => true) incoming_message = double(IncomingMessage, :info_request => info_request, :parse_raw_email! => true, :info_request_id => 132, :id => 44, :get_attachments_for_display => nil, :apply_masks => nil, - :user_can_view? => true, + :prominence => 'normal', :all_can_view? => true) attachment = FactoryGirl.build(:body_text, :filename => long_name) allow(IncomingMessage).to receive(:find).with("44").and_return(incoming_message) diff --git a/spec/lib/ability_spec.rb b/spec/lib/ability_spec.rb deleted file mode 100644 index 86337cc96d..0000000000 --- a/spec/lib/ability_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# -*- encoding : utf-8 -*- -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - -describe Ability do - describe ".can_update_request_state?" do - context "old and unclassified request" do - let(:request) { mock_model(InfoRequest, :is_old_unclassified? => true) } - - context "logged out" do - let(:user) { nil } - before(:each) { allow(request).to receive(:is_owning_user?).and_return(false) } - it { expect(Ability::can_update_request_state?(user, request)).to be false } - end - - context "logged in but not owner of request" do - let(:user) { mock_model(User) } - before(:each) { allow(request).to receive(:is_owning_user?).and_return(false) } - - it { expect(Ability::can_update_request_state?(user, request)).to be true } - end - end - - context "new request" do - let(:request) { mock_model(InfoRequest, :is_old_unclassified? => false) } - - context "logged out" do - let(:user) { nil } - before(:each) { allow(request).to receive(:is_owning_user?).and_return(false) } - - it { expect(Ability::can_update_request_state?(user, request)).to be false } - end - - context "logged in" do - let(:user) { mock_model(User) } - - # An owner of a request can also be someone with admin powers - context "as owner of request" do - before(:each) { allow(request).to receive(:is_owning_user?).and_return(true) } - - it { expect(Ability::can_update_request_state?(user, request)).to be true } - end - - context "but not owner of request" do - before(:each) { allow(request).to receive(:is_owning_user?).and_return(false) } - - it { expect(Ability::can_update_request_state?(user, request)).to be false } - end - end - end - end -end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb new file mode 100644 index 0000000000..ead2eef5b8 --- /dev/null +++ b/spec/models/ability_spec.rb @@ -0,0 +1,156 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require "cancan/matchers" + +shared_examples_for "a class with message prominence" do + let(:admin_ability) { Ability.new(FactoryGirl.create(:admin_user)) } + let(:other_user_ability) { Ability.new(FactoryGirl.create(:user)) } + + context 'if the prominence is hidden' do + before do + resource.prominence = 'hidden' + end + + it 'should return true for an admin user' do + expect(admin_ability).to be_able_to(:read, resource) + end + + it 'should return false for a non-admin user' do + expect(other_user_ability).not_to be_able_to(:read, resource) + end + + it 'should return false for the owner' do + expect(owner_ability).not_to be_able_to(:read, resource) + end + end + + context 'if the prominence is requester_only' do + before do + resource.prominence = 'requester_only' + end + + it 'should return true if the user owns the right resource' do + expect(owner_ability).to be_able_to(:read, resource) + end + + it 'should return true for an admin user' do + expect(admin_ability).to be_able_to(:read, resource) + end + + it 'should return false if the user does not own the right resource' do + expect(other_user_ability).not_to be_able_to(:read, resource) + end + end + + context 'if the prominence is normal' do + before do + resource.prominence = 'normal' + end + + it 'should return true for a non-admin user' do + expect(other_user_ability).to be_able_to(:read, resource) + end + + it 'should return true for an admin user' do + expect(admin_ability).to be_able_to(:read, resource) + end + + it 'should return true if the user owns the right resource' do + expect(owner_ability).to be_able_to(:read, resource) + end + end +end + +describe Ability do + describe "reading IncomingMessages" do + let(:info_request) { FactoryGirl.create(:info_request_with_incoming) } + let!(:resource) { info_request.incoming_messages.first } + let!(:owner_ability) { Ability.new(info_request.user) } + + it_behaves_like "a class with message prominence" + end + + describe "reading OutgoingMessages" do + let(:info_request) { FactoryGirl.create(:info_request) } + let!(:resource) { info_request.outgoing_messages.first } + let!(:owner_ability) { Ability.new(info_request.user) } + + it_behaves_like "a class with message prominence" + end + + describe "reading InfoRequests" do + let!(:resource) { FactoryGirl.create(:info_request) } + let!(:owner_ability) { Ability.new(resource.user) } + + it_behaves_like "a class with message prominence" + end + + describe "updating request state of InfoRequests" do + context "given an old and unclassified request" do + let(:request) { FactoryGirl.create(:old_unclassified_request) } + + context "when logged out" do + let(:ability) { Ability.new(nil) } + + it "should return false" do + expect(ability).not_to be_able_to(:update_request_state, request) + end + end + + context "when logged in but not owner of request" do + let(:user) { FactoryGirl.create(:user) } + let(:ability) { Ability.new(user) } + + it "should return true" do + expect(ability).to be_able_to(:update_request_state, request) + end + end + + context "when owner of request" do + let(:user) { request.user } + let(:ability) { Ability.new(user) } + + it "should return true" do + expect(ability).to be_able_to(:update_request_state, request) + end + end + end + + context "given a new request" do + let(:request) { FactoryGirl.create(:info_request) } + + context "when logged out" do + let(:ability) { Ability.new(nil) } + + it "should return false" do + expect(ability).not_to be_able_to(:update_request_state, request) + end + end + + context "when logged in" do + context "as owner of request" do + let(:ability) { Ability.new(request.user) } + + it "should return true" do + expect(ability).to be_able_to(:update_request_state, request) + end + end + + context "as an admin" do + let(:ability) { Ability.new(FactoryGirl.create(:admin_user)) } + + it "should return true" do + expect(ability).to be_able_to(:update_request_state, request) + end + end + + context "but not owner of request" do + let(:ability) { Ability.new(FactoryGirl.create(:user)) } + + it "should return false" do + expect(ability).not_to be_able_to(:update_request_state, request) + end + end + end + end + end +end diff --git a/spec/models/incoming_message_spec.rb b/spec/models/incoming_message_spec.rb index eea1db2b7e..95b9d3fe0a 100644 --- a/spec/models/incoming_message_spec.rb +++ b/spec/models/incoming_message_spec.rb @@ -400,63 +400,6 @@ end -describe IncomingMessage, 'when asked if a user can view it' do - - before do - @user = mock_model(User) - @info_request = mock_model(InfoRequest) - @incoming_message = IncomingMessage.new(:info_request => @info_request) - end - - context 'if the prominence is hidden' do - - before do - @incoming_message.prominence = 'hidden' - end - - it 'should return true if the user can view hidden things' do - allow(User).to receive(:view_hidden?).with(@user).and_return(true) - expect(@incoming_message.user_can_view?(@user)).to be true - end - - it 'should return false if the user cannot view hidden things' do - allow(User).to receive(:view_hidden?).with(@user).and_return(false) - expect(@incoming_message.user_can_view?(@user)).to be false - end - - end - - context 'if the prominence is requester_only' do - - before do - @incoming_message.prominence = 'requester_only' - end - - it 'should return true if the user owns the associated request' do - allow(@info_request).to receive(:is_owning_user?).with(@user).and_return(true) - expect(@incoming_message.user_can_view?(@user)).to be true - end - - it 'should return false if the user does not own the associated request' do - allow(@info_request).to receive(:is_owning_user?).with(@user).and_return(false) - expect(@incoming_message.user_can_view?(@user)).to be false - end - end - - context 'if the prominence is normal' do - - before do - @incoming_message.prominence = 'normal' - end - - it 'should return true' do - expect(@incoming_message.user_can_view?(@user)).to be true - end - - end - -end - describe 'when destroying a message' do let(:incoming_message) { FactoryGirl.create(:plain_incoming_message) } diff --git a/spec/models/outgoing_message_spec.rb b/spec/models/outgoing_message_spec.rb index a528fff1d1..f8f9e580dc 100644 --- a/spec/models/outgoing_message_spec.rb +++ b/spec/models/outgoing_message_spec.rb @@ -842,58 +842,6 @@ end - describe '#user_can_view?' do - - before do - @info_request = FactoryGirl.create(:info_request) - @outgoing_message = @info_request.outgoing_messages.first - end - - context 'if the prominence is hidden' do - - before do - @outgoing_message.prominence = 'hidden' - end - - it 'should return true for an admin user' do - expect(@outgoing_message.user_can_view?(FactoryGirl.create(:admin_user))).to be true - end - - it 'should return false for a non-admin user' do - expect(@outgoing_message.user_can_view?(FactoryGirl.create(:user))).to be false - end - - end - - context 'if the prominence is requester_only' do - - before do - @outgoing_message.prominence = 'requester_only' - end - - it 'should return true if the user owns the associated request' do - expect(@outgoing_message.user_can_view?(@info_request.user)).to be true - end - - it 'should return false if the user does not own the associated request' do - expect(@outgoing_message.user_can_view?(FactoryGirl.create(:user))).to be false - end - end - - context 'if the prominence is normal' do - - before do - @outgoing_message.prominence = 'normal' - end - - it 'should return true for a non-admin user' do - expect(@outgoing_message.user_can_view?(FactoryGirl.create(:user))).to be true - end - - end - - end - describe '#smtp_message_ids' do context 'a sent message' do From e0518c4f63bda8f02531bbcb6c23503fb023dc38 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 7 Oct 2016 09:31:40 +0100 Subject: [PATCH 014/311] Add an alaveteli_features gem This adds a library module which provides access to a Flipper instance for controlling feature flags, and a helper method feature_enabled? which allows you to check if a feature is enabled in views and controllers. --- gems/alaveteli_features/.gitignore | 17 +++++ gems/alaveteli_features/.rspec | 2 + gems/alaveteli_features/Gemfile | 4 ++ gems/alaveteli_features/LICENSE.txt | 22 +++++++ gems/alaveteli_features/README.md | 62 +++++++++++++++++++ gems/alaveteli_features/Rakefile | 6 ++ .../alaveteli_features.gemspec | 32 ++++++++++ .../lib/alaveteli_features.rb | 28 +++++++++ .../install/install_generator.rb | 44 +++++++++++++ .../initializers/alaveteli_features.rb.erb | 8 +++ .../lib/alaveteli_features/helpers.rb | 7 +++ .../lib/alaveteli_features/railtie.rb | 17 +++++ .../lib/alaveteli_features/version.rb | 3 + .../spec/alaveteli_features_spec.rb | 18 ++++++ .../spec/helpers/feature_enabled_spec.rb | 62 +++++++++++++++++++ gems/alaveteli_features/spec/spec_helper.rb | 2 + 16 files changed, 334 insertions(+) create mode 100644 gems/alaveteli_features/.gitignore create mode 100644 gems/alaveteli_features/.rspec create mode 100644 gems/alaveteli_features/Gemfile create mode 100644 gems/alaveteli_features/LICENSE.txt create mode 100644 gems/alaveteli_features/README.md create mode 100644 gems/alaveteli_features/Rakefile create mode 100644 gems/alaveteli_features/alaveteli_features.gemspec create mode 100644 gems/alaveteli_features/lib/alaveteli_features.rb create mode 100644 gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/install_generator.rb create mode 100644 gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/templates/config/initializers/alaveteli_features.rb.erb create mode 100644 gems/alaveteli_features/lib/alaveteli_features/helpers.rb create mode 100644 gems/alaveteli_features/lib/alaveteli_features/railtie.rb create mode 100644 gems/alaveteli_features/lib/alaveteli_features/version.rb create mode 100644 gems/alaveteli_features/spec/alaveteli_features_spec.rb create mode 100644 gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb create mode 100644 gems/alaveteli_features/spec/spec_helper.rb diff --git a/gems/alaveteli_features/.gitignore b/gems/alaveteli_features/.gitignore new file mode 100644 index 0000000000..d87d4be66f --- /dev/null +++ b/gems/alaveteli_features/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/gems/alaveteli_features/.rspec b/gems/alaveteli_features/.rspec new file mode 100644 index 0000000000..8c18f1abdd --- /dev/null +++ b/gems/alaveteli_features/.rspec @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/gems/alaveteli_features/Gemfile b/gems/alaveteli_features/Gemfile new file mode 100644 index 0000000000..9d6f7f43f9 --- /dev/null +++ b/gems/alaveteli_features/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in alaveteli_features.gemspec +gemspec diff --git a/gems/alaveteli_features/LICENSE.txt b/gems/alaveteli_features/LICENSE.txt new file mode 100644 index 0000000000..63ec041349 --- /dev/null +++ b/gems/alaveteli_features/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2016 TODO: Write your name + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/gems/alaveteli_features/README.md b/gems/alaveteli_features/README.md new file mode 100644 index 0000000000..770aa6d165 --- /dev/null +++ b/gems/alaveteli_features/README.md @@ -0,0 +1,62 @@ +# AlaveteliFeatures + +This is a small gem for Alaveteli sites which adds a feature flipping library. + +## Installation + +Add this line to your application's Gemfile: + + gem 'alaveteli_features' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install alaveteli_features + +Then run: + + $ rails g alaveteli_features:install + +to install the necessary migrations and an example initializer file. + +## Usage + +Configure your features in `config/initializers/alaveteli_features.rb` with +something like: + + AlaveteliFeatures.backend.enable(:feature_name) + +`AlaveteliFeatures.backend` is an instance of [Flipper](https://github.com/jnunemaker/flipper/) +so you can use [any of the DSL methods](https://github.com/jnunemaker/flipper/blob/master/docs/Gates.md) +it provides. + +By default, AlaveteliFeatures uses the active record backend, you can also +swap the backend for a different one: + + require 'flipper/adapters/memory' + + memory_backend = Flipper.new(Flipper::Adapters::Memory.new) + AlaveteliFeatures.backend = memory_backend + + +To check for enabled features in your backend, use the `feature_enabled?` +helper. This is lazily-included in `ActionController::Base` and +`ActionView::Base` so you can use it in your controllers and views +automatically. To use it elsewhere just include the `Helpers` module in any +class that needs it: + + class SomeClass + include AlaveteliFeatures::Helpers + end + + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/gems/alaveteli_features/Rakefile b/gems/alaveteli_features/Rakefile new file mode 100644 index 0000000000..b7e9ed549b --- /dev/null +++ b/gems/alaveteli_features/Rakefile @@ -0,0 +1,6 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/gems/alaveteli_features/alaveteli_features.gemspec b/gems/alaveteli_features/alaveteli_features.gemspec new file mode 100644 index 0000000000..30ebbb30ee --- /dev/null +++ b/gems/alaveteli_features/alaveteli_features.gemspec @@ -0,0 +1,32 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'alaveteli_features/version' + +Gem::Specification.new do |spec| + spec.name = "alaveteli_features" + spec.version = AlaveteliFeatures::VERSION + spec.authors = ["mySociety"] + spec.email = ["alaveteli@mysociety.org"] + spec.description = "Feature flags for Alaveteli" + spec.summary = "Feature flags for Alaveteli" + spec.homepage = "https://alaveteli.org" + spec.license = "MIT" + + spec.files = `git ls-files`.split($/) + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_dependency "rails", "~> 3.2.22" + spec.add_dependency "flipper" + spec.add_dependency "flipper-active_record" + # Mime types 3 needs Ruby 2.0.0 or greater, but we need to support 1.9.3 so + # force a lower version + spec.add_dependency "mime-types", "< 3.0.0" + + spec.add_development_dependency "bundler", "~> 1.3" + spec.add_development_dependency "rake" + spec.add_development_dependency "rspec" + spec.add_development_dependency "rspec-rails" +end diff --git a/gems/alaveteli_features/lib/alaveteli_features.rb b/gems/alaveteli_features/lib/alaveteli_features.rb new file mode 100644 index 0000000000..89793fc865 --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features.rb @@ -0,0 +1,28 @@ +require "alaveteli_features/version" +require "alaveteli_features/helpers" +require "alaveteli_features/railtie" if defined?(Rails) +require "flipper" +require "flipper-active_record" + +module AlaveteliFeatures + def self.backend + return @backend if @backend + if ActiveRecord::Base.connection.table_exists? :flipper_features + @backend = Flipper.new(Flipper::Adapters::ActiveRecord.new) + else + Rails.logger.warn "No database tables found for feature flags, you " \ + "might need to set a backend explicitly if you " \ + "don't want them stored in a database, or run" \ + "rake db:migrate to create the table." + Rails.logger.warn "Using memory-based feature storage instead." + require 'flipper/adapters/memory' + @backend = Flipper.new(Flipper::Adapters::Memory.new) + end + @backend + end + + # for overriding with memory adapter in tests + def self.backend=(backend) + @backend = backend + end +end diff --git a/gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/install_generator.rb b/gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/install_generator.rb new file mode 100644 index 0000000000..9c5db8ac0d --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/install_generator.rb @@ -0,0 +1,44 @@ +require 'rails/generators' + +module AlaveteliFeatures + class InstallGenerator < Rails::Generators::Base + class_option :migrate, type: :boolean, default: true, banner: 'Run AlaveteliFeatures migrations' + + def self.source_paths + paths = self.superclass.source_paths + paths << File.expand_path('../templates', "../../#{__FILE__}") + paths << File.expand_path('../templates', "../#{__FILE__}") + paths << File.expand_path('../templates', __FILE__) + paths.flatten + end + + def prepare_options + @run_migrations = options[:migrate] + end + + def install_migrations + say_status :copying, "migrations" + generate 'flipper:active_record' + end + + def run_migrations + if @run_migrations + say_status :running, "migrations" + rake 'db:migrate' + else + say_status :skipping, "migrations (don't forget to run rake db:migrate)" + end + end + + def add_files + template 'config/initializers/alaveteli_features.rb.erb', 'config/initializers/alaveteli_features.rb' + end + + def complete + puts "*" * 50 + puts "AlaveteliFeatures has been installed successfully. You're all ready to go!" + puts " " + puts "Enjoy!" + end + end +end \ No newline at end of file diff --git a/gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/templates/config/initializers/alaveteli_features.rb.erb b/gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/templates/config/initializers/alaveteli_features.rb.erb new file mode 100644 index 0000000000..31e85b0241 --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features/generators/alaveteli_features/install/templates/config/initializers/alaveteli_features.rb.erb @@ -0,0 +1,8 @@ +# Set up our available features and (optionally) get some defaults for +# them from the config/general.yml configuration. + +# See Flipper's documentation for further examples of how you can enable +# and disable features, noting that (depending on the adapter used) there +# might well be settings stored in other places (the db, caches, etc) that +# you need to respect. +# https://github.com/jnunemaker/flipper/blob/master/lib/flipper/dsl.rb diff --git a/gems/alaveteli_features/lib/alaveteli_features/helpers.rb b/gems/alaveteli_features/lib/alaveteli_features/helpers.rb new file mode 100644 index 0000000000..6e9f769c7d --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features/helpers.rb @@ -0,0 +1,7 @@ +module AlaveteliFeatures + module Helpers + def feature_enabled?(feature, *args) + AlaveteliFeatures.backend.enabled?(feature, *args) + end + end +end \ No newline at end of file diff --git a/gems/alaveteli_features/lib/alaveteli_features/railtie.rb b/gems/alaveteli_features/lib/alaveteli_features/railtie.rb new file mode 100644 index 0000000000..138bedadee --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features/railtie.rb @@ -0,0 +1,17 @@ +require 'alaveteli_features/helpers' + +module AlaveteliFeatures + class Railtie < Rails::Railtie + initializer "alaveteli_features.helpers" do + ActiveSupport.on_load :action_view do + include AlaveteliFeatures::Helpers + end + ActiveSupport.on_load :action_controller do + include AlaveteliFeatures::Helpers + end + end + generators do + require 'alaveteli_features/generators/alaveteli_features/install/install_generator' + end + end +end \ No newline at end of file diff --git a/gems/alaveteli_features/lib/alaveteli_features/version.rb b/gems/alaveteli_features/lib/alaveteli_features/version.rb new file mode 100644 index 0000000000..83540981c1 --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features/version.rb @@ -0,0 +1,3 @@ +module AlaveteliFeatures + VERSION = "0.0.1" +end diff --git a/gems/alaveteli_features/spec/alaveteli_features_spec.rb b/gems/alaveteli_features/spec/alaveteli_features_spec.rb new file mode 100644 index 0000000000..c20de94e52 --- /dev/null +++ b/gems/alaveteli_features/spec/alaveteli_features_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require 'flipper/adapters/memory' + +describe AlaveteliFeatures do + it 'should have a version number' do + expect(AlaveteliFeatures::VERSION).not_to be_nil + end + + it 'should allow you to access the backend' do + expect(AlaveteliFeatures.backend).not_to be_nil + end + + it 'should allow you to set the backend' do + test_backend = Flipper.new(Flipper::Adapters::Memory.new) + AlaveteliFeatures.backend = test_backend + expect(AlaveteliFeatures.backend).to be test_backend + end +end diff --git a/gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb b/gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb new file mode 100644 index 0000000000..234de2b96f --- /dev/null +++ b/gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' +require 'flipper/adapters/memory' +require 'alaveteli_features/helpers' + +describe AlaveteliFeatures::Helpers do + let(:instance) { Class.new { include AlaveteliFeatures::Helpers }.new } + let(:test_backend) { Flipper.new(Flipper::Adapters::Memory.new) } + let(:user_class) do + # A test class to let us test the actor-based feature flipping + class User + attr_reader :id + + def initialize(id, admin) + @id = id + @admin = admin + end + + def admin? + @admin + end + + # Must respond to flipper_id + alias_method :flipper_id, :id + end + end + + before do + AlaveteliFeatures.backend = test_backend + # Seems to be the only way to make sure we don't register a group twice + begin + AlaveteliFeatures.backend.group(:admins) + rescue Flipper::GroupNotRegistered + Flipper.register :admins do |actor| + actor.respond_to?(:admin?) && actor.admin? + end + end + end + + describe "#feature_enabled?" do + it "should respond true when a feature is enabled" do + AlaveteliFeatures.backend.enable(:test_feature) + expect(instance.feature_enabled?(:test_feature)).to eq true + end + + it "should respond false when a feature is disabled" do + AlaveteliFeatures.backend.disable(:test_feature) + expect(instance.feature_enabled?(:test_feature)).to eq false + end + + it "should pass on other arguments to the backend" do + user1 = user_class.new(1, true) + + mock_backend = double("backend") + AlaveteliFeatures.backend = mock_backend + + expect(mock_backend).to( + receive(:enabled?).with(:test_feature, user1) + ) + instance.feature_enabled?(:test_feature, user1) + end + end +end \ No newline at end of file diff --git a/gems/alaveteli_features/spec/spec_helper.rb b/gems/alaveteli_features/spec/spec_helper.rb new file mode 100644 index 0000000000..05f00bc912 --- /dev/null +++ b/gems/alaveteli_features/spec/spec_helper.rb @@ -0,0 +1,2 @@ +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'alaveteli_features' From 4c5f0a3ece8886e79d994b8f4cb815c8bf333c2e Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 7 Oct 2016 10:08:23 +0100 Subject: [PATCH 015/311] Install and use AlaveteliFeatures for enabling annotations --- Gemfile | 3 +++ Gemfile.lock | 14 ++++++++++++ app/controllers/application_controller.rb | 2 +- app/controllers/comment_controller.rb | 2 +- app/views/followups/new.html.erb | 2 +- .../general/_advanced_search_tips.html.erb | 4 ++-- app/views/general/search.html.erb | 2 +- app/views/request/_after_actions.html.erb | 2 +- app/views/request/_request_sent.html.erb | 2 +- app/views/request/_wall_listing.html.erb | 2 +- .../describe_notices/_successful.html.erb | 2 +- app/views/user/_user_listing_single.html.erb | 2 +- app/views/user/banned.html.erb | 2 +- app/views/user/show.html.erb | 4 ++-- config/initializers/alaveteli_features.rb | 16 ++++++++++++++ .../20161006142352_create_flipper_tables.rb | 22 +++++++++++++++++++ spec/controllers/comment_controller_spec.rb | 2 +- spec/controllers/request_controller_spec.rb | 2 +- .../request/_after_actions.html.erb_spec.rb | 2 +- 19 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 config/initializers/alaveteli_features.rb create mode 100644 db/migrate/20161006142352_create_flipper_tables.rb diff --git a/Gemfile b/Gemfile index c72580e06a..a8b4d94e17 100644 --- a/Gemfile +++ b/Gemfile @@ -67,6 +67,9 @@ group :assets do gem 'therubyracer', '~> 0.12.2' end +# Feature flags +gem 'alaveteli_features', :path => 'gems/alaveteli_features' + group :test do gem 'fakeweb', '~> 1.3.0' gem 'coveralls', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index b4347e933c..5c1b2b83a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,6 +42,15 @@ GIT acts_as_versioned (0.6.0) activerecord (>= 3.0.9) +PATH + remote: gems/alaveteli_features + specs: + alaveteli_features (0.0.1) + flipper + flipper-active_record + mime-types (< 3.0.0) + rails (~> 4.0.13) + GEM remote: https://rubygems.org/ specs: @@ -159,6 +168,10 @@ GEM railties (>= 3.1.0) fast_gettext (1.1.0) ffi (1.9.10) + flipper (0.9.2) + flipper-active_record (0.9.2) + activerecord (>= 3.2, < 6) + flipper (~> 0.9.2) foundation-rails (5.5.3.2) railties (>= 3.1.0) sass (>= 3.3.0, < 3.5) @@ -371,6 +384,7 @@ PLATFORMS DEPENDENCIES active_model_otp! acts_as_versioned! + alaveteli_features! annotate (~> 2.7.1) bootstrap-sass (~> 2.3.2.2) bullet (~> 5.1.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 401f5241c8..a60667f604 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -346,7 +346,7 @@ def authentication_check # def check_read_only if !AlaveteliConfiguration::read_only.empty? - if AlaveteliConfiguration::enable_annotations + if feature_enabled?(:annotations) flash[:notice] = _("

{{site_name}} is currently in maintenance. You can only view existing requests. You cannot make new ones, add followups or annotations, or otherwise change the database.

{{read_only}}

", :site_name => site_name, :read_only => AlaveteliConfiguration::read_only) diff --git a/app/controllers/comment_controller.rb b/app/controllers/comment_controller.rb index c4b16f9fbf..759524e8fb 100644 --- a/app/controllers/comment_controller.rb +++ b/app/controllers/comment_controller.rb @@ -108,7 +108,7 @@ def create_track_thing # not usually hit this unless they are explicitly attempting to avoid the # comment block. def reject_unless_comments_allowed - unless AlaveteliConfiguration.enable_annotations && @info_request.comments_allowed? + unless feature_enabled?(:annotations) && @info_request.comments_allowed? redirect_to request_url(@info_request), :notice => "Comments are not allowed on this request" end end diff --git a/app/views/followups/new.html.erb b/app/views/followups/new.html.erb index fd94479027..0c40eefc83 100644 --- a/app/views/followups/new.html.erb +++ b/app/views/followups/new.html.erb @@ -46,7 +46,7 @@ <%= _('When you receive the paper response, please help others find ' \ 'out what it says:') %>
    - <% if AlaveteliConfiguration.enable_annotations %> + <% if feature_enabled?(:annotations) %>
  • <%= _('Add an annotation to your request with choice quotes, ' \ 'or a summary of the response.') %>
  • <% end %> diff --git a/app/views/general/_advanced_search_tips.html.erb b/app/views/general/_advanced_search_tips.html.erb index 8a30a07a8d..70a9a497b2 100644 --- a/app/views/general/_advanced_search_tips.html.erb +++ b/app/views/general/_advanced_search_tips.html.erb @@ -9,7 +9,7 @@
  • <%= _('variety: to select type of thing to search for, see the table of varieties below.', :varieties_url => "#varieties") %>
  • <%= _('requested_from:home_office to search requests from the Home Office, typing the name as in the URL.')%>
  • <%= _('requested_by:julian_todd to search requests made by Julian Todd, typing the name as in the URL.') %>
  • - <% if AlaveteliConfiguration.enable_annotations %> + <% if feature_enabled?(:annotations) %>
  • <%= _('commented_by:tony_bowden to search annotations made by Tony Bowden, typing the name as in the URL.')%>
  • <% end %>
  • <%= _('request: to restrict to a specific request, typing the title as in the URL.')%> @@ -54,7 +54,7 @@ # the initial request _('Follow up message sent by requester') %> <%=search_link('variety:response')%><%= _('Response from a public authority') %> - <% if AlaveteliConfiguration.enable_annotations %> + <% if feature_enabled?(:annotations) %> <%=search_link('variety:comment')%><%= _('Annotation added to request') %> <% end %> <%=search_link('variety:authority')%><%= _('A public authority') %> diff --git a/app/views/general/search.html.erb b/app/views/general/search.html.erb index e064ce1868..4b033f0927 100644 --- a/app/views/general/search.html.erb +++ b/app/views/general/search.html.erb @@ -93,7 +93,7 @@

    <%= _("Search in") %>

    - <% filters = if AlaveteliConfiguration.enable_annotations + <% filters = if feature_enabled?(:annotations) [["sent", _("messages from users")], ["response", _("messages from authorities")], ["comment", _("comments")]] diff --git a/app/views/request/_after_actions.html.erb b/app/views/request/_after_actions.html.erb index 10361ba465..79425145ae 100644 --- a/app/views/request/_after_actions.html.erb +++ b/app/views/request/_after_actions.html.erb @@ -65,7 +65,7 @@
  • <% end %> - <% if AlaveteliConfiguration.enable_annotations && @info_request.comments_allowed? %> + <% if if feature_enabled?(:annotations) && @info_request.comments_allowed? %>
  • <%= link_to _('Add an annotation'), new_comment_path(:url_title => @info_request.url_title) %>
  • diff --git a/app/views/request/_request_sent.html.erb b/app/views/request/_request_sent.html.erb index 52ef2c7132..013ece430d 100644 --- a/app/views/request/_request_sent.html.erb +++ b/app/views/request/_request_sent.html.erb @@ -49,7 +49,7 @@

    <%= _("Keep your request up to date") %>

    - <% if AlaveteliConfiguration.enable_annotations %> + <% if feature_enabled?(:annotations) %>

    <%= _('If you write about this request ' \ '(for example in a forum or a blog) ' \ diff --git a/app/views/request/_wall_listing.html.erb b/app/views/request/_wall_listing.html.erb index ff7f1df83b..161f713ce8 100644 --- a/app/views/request/_wall_listing.html.erb +++ b/app/views/request/_wall_listing.html.erb @@ -11,7 +11,7 @@ end %> <%= _('A follow up to {{request_title}} was sent to {{public_body_name}} by {{info_request_user}} on {{date}}.',:public_body_name=>public_body_link_absolute(info_request.public_body),:info_request_user=>request_user_link_absolute(info_request),:date=>simple_date(event.created_at),:request_url=>outgoing_message_path(event.outgoing_message),:request_title=>info_request.title) %> <% elsif event.event_type == 'response' %> <%= _('A response to {{request_title}} was sent by {{public_body_name}} to {{info_request_user}} on {{date}}. The request status is: {{request_status}}',:public_body_name=>public_body_link_absolute(info_request.public_body),:info_request_user=>request_user_link_absolute(info_request),:date=>simple_date(event.created_at),:request_url=>incoming_message_path(event.incoming_message_selective_columns("incoming_messages.id")),:request_title=>info_request.title,:request_status=>info_request.display_status) %> - <% elsif event.event_type == 'comment' && AlaveteliConfiguration::enable_annotations %> + <% elsif event.event_type == 'comment' && feature_enabled?(:annotations) %> <%= _('An annotation to {{request_title}} was made by {{event_comment_user}} on {{date}}',:public_body_name=>public_body_link_absolute(info_request.public_body),:info_request_user=>request_user_link_absolute(info_request),:event_comment_user=>user_link_absolute(event.comment.user),:date=>simple_date(event.created_at),:request_url=>comment_url(event.comment),:request_title=>info_request.title) %> <% else %> <%# Events of other types will not be indexed: see InfoRequestEvent#indexed_by_search? diff --git a/app/views/request/describe_notices/_successful.html.erb b/app/views/request/describe_notices/_successful.html.erb index 8e660a0bb8..4c7befefc1 100644 --- a/app/views/request/describe_notices/_successful.html.erb +++ b/app/views/request/describe_notices/_successful.html.erb @@ -1,4 +1,4 @@ -<% if AlaveteliConfiguration.enable_annotations %> +<% if feature_enabled?(:annotations) %>

    <%= _("We're glad you got all the information that you wanted. If you " \ "write about or make use of the information, please come back and " \ diff --git a/app/views/user/_user_listing_single.html.erb b/app/views/user/_user_listing_single.html.erb index 302bcf4da3..2c065adc85 100644 --- a/app/views/user/_user_listing_single.html.erb +++ b/app/views/user/_user_listing_single.html.erb @@ -21,7 +21,7 @@ '{{count}} requests made.', request_count, :count => request_count) %> - <% if AlaveteliConfiguration::enable_annotations %> + <% if feature_enabled?(:annotations) %> <% annotation_count = display_user.comments.visible.size %> <%= n_('{{count}} annotation made.', '{{count}} annotations made.', diff --git a/app/views/user/banned.html.erb b/app/views/user/banned.html.erb index 25d68a5d5c..cd72bac415 100644 --- a/app/views/user/banned.html.erb +++ b/app/views/user/banned.html.erb @@ -6,7 +6,7 @@ <%= @details %>

    -<% if AlaveteliConfiguration::enable_annotations %> +<% if feature_enabled?(:annotations) %>

    <%= _('You will be unable to make new requests, send follow ups, add ' \ 'annotations or send messages to other users. You may continue ' \ diff --git a/app/views/user/show.html.erb b/app/views/user/show.html.erb index dc50738a77..5be4134276 100644 --- a/app/views/user/show.html.erb +++ b/app/views/user/show.html.erb @@ -51,7 +51,7 @@ <% if @xapian_requests %>

    <%= _('On this page') %>

    <%= _('FOI requests') %> - <% if AlaveteliConfiguration::enable_annotations %> + <% if feature_enabled?(:annotations) %>
    <%= _('Annotations') %> <% end %> <% end %> @@ -211,7 +211,7 @@ <% end %> <% end %> - <% if @xapian_comments && AlaveteliConfiguration::enable_annotations %> + <% if @xapian_comments && feature_enabled?(:annotations) %> <% if @xapian_comments.results.empty? %> <% if @page == 1 %>

    diff --git a/config/initializers/alaveteli_features.rb b/config/initializers/alaveteli_features.rb new file mode 100644 index 0000000000..44efe5c637 --- /dev/null +++ b/config/initializers/alaveteli_features.rb @@ -0,0 +1,16 @@ +# Set up our available features and (optionally) get some defaults for +# them from the config/general.yml configuration. + +# See Flipper's documentation for further examples of how you can enable +# and disable features, noting that (depending on the adapter used) there +# might well be settings stored in other places (the db, caches, etc) that +# you need to respect. +# https://github.com/jnunemaker/flipper/blob/master/lib/flipper/dsl.rb + +# Annotations +# We enable annotations globally based on the ENABLE_ANNOTATIONS config +if AlaveteliConfiguration.enable_annotations + AlaveteliFeatures.backend.enable(:annotations) +else + AlaveteliFeatures.backend.disable(:annotations) +end diff --git a/db/migrate/20161006142352_create_flipper_tables.rb b/db/migrate/20161006142352_create_flipper_tables.rb new file mode 100644 index 0000000000..5041e1f31e --- /dev/null +++ b/db/migrate/20161006142352_create_flipper_tables.rb @@ -0,0 +1,22 @@ +class CreateFlipperTables < ActiveRecord::Migration + def self.up + create_table :flipper_features do |t| + t.string :key, null: false + t.timestamps null: false + end + add_index :flipper_features, :key, unique: true + + create_table :flipper_gates do |t| + t.string :feature_key, null: false + t.string :key, null: false + t.string :value + t.timestamps null: false + end + add_index :flipper_gates, [:feature_key, :key, :value], unique: true + end + + def self.down + drop_table :flipper_gates + drop_table :flipper_features + end +end diff --git a/spec/controllers/comment_controller_spec.rb b/spec/controllers/comment_controller_spec.rb index d6618c9358..21552fbc93 100644 --- a/spec/controllers/comment_controller_spec.rb +++ b/spec/controllers/comment_controller_spec.rb @@ -71,7 +71,7 @@ end it "should not allow comments if comments are not allowed globally" do - allow(AlaveteliConfiguration).to receive(:enable_annotations).and_return(false) + allow(controller).to receive(:feature_enabled?).with(:annotations).and_return(false) session[:user_id] = users(:silly_name_user).id info_request = info_requests(:fancy_dog_request) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 679bd307e8..4291dacbb3 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2625,7 +2625,7 @@ def make_request context "when annotations are disabled" do before do - allow(AlaveteliConfiguration).to receive(:enable_annotations).and_return(false) + allow(controller).to receive(:feature_enabled?).with(:annotations).and_return(false) end it "doesn't mention annotations in the flash message" do diff --git a/spec/views/request/_after_actions.html.erb_spec.rb b/spec/views/request/_after_actions.html.erb_spec.rb index 2ade3a10d6..c09cedcdb3 100644 --- a/spec/views/request/_after_actions.html.erb_spec.rb +++ b/spec/views/request/_after_actions.html.erb_spec.rb @@ -99,7 +99,7 @@ end it "should not display a link to annotate the request if comments are disabled globally" do - allow(AlaveteliConfiguration).to receive(:enable_annotations).and_return(false) + allow(view).to receive(:feature_enabled?).with(:annotations).and_return(false) render :partial => 'request/after_actions' expect(response.body).to have_css('ul.anyone_actions') do |div| expect(div).not_to have_css('a', :text => 'Add an annotation (to help the requester or others)') From 1ec55a7c33afc91e938b07f2c9a94c7bcde0920c Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 31 Oct 2016 17:23:09 +0000 Subject: [PATCH 016/311] Build the AP branch. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 37ef771153..3c7aef1cc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ branches: only: - master - develop + - alaveteli-pro-develop rvm: - 1.9.3 - 2.0.0 From 46be9db26f429193ad772dc17df09429e3d05c19 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 1 Nov 2016 12:46:27 +0000 Subject: [PATCH 017/311] Add a ProAccount model --- app/models/pro_account.rb | 14 ++++++++++++++ db/migrate/20161101110656_create_pro_accounts.rb | 10 ++++++++++ spec/factories/pro_accounts.rb | 16 ++++++++++++++++ spec/models/pro_account_spec.rb | 14 ++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 app/models/pro_account.rb create mode 100644 db/migrate/20161101110656_create_pro_accounts.rb create mode 100644 spec/factories/pro_accounts.rb create mode 100644 spec/models/pro_account_spec.rb diff --git a/app/models/pro_account.rb b/app/models/pro_account.rb new file mode 100644 index 0000000000..f1e983c1b8 --- /dev/null +++ b/app/models/pro_account.rb @@ -0,0 +1,14 @@ +# == Schema Information +# +# Table name: pro_accounts +# +# id :integer not null, primary key +# user_id :integer not null +# default_embargo_duration :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# + +class ProAccount < ActiveRecord::Base + belongs_to :user +end diff --git a/db/migrate/20161101110656_create_pro_accounts.rb b/db/migrate/20161101110656_create_pro_accounts.rb new file mode 100644 index 0000000000..549bc995e1 --- /dev/null +++ b/db/migrate/20161101110656_create_pro_accounts.rb @@ -0,0 +1,10 @@ +class CreateProAccounts < ActiveRecord::Migration + def change + create_table :pro_accounts do |t| + t.column :user_id, :integer, null: false + t.column :default_embargo_duration, :string + + t.timestamps + end + end +end diff --git a/spec/factories/pro_accounts.rb b/spec/factories/pro_accounts.rb new file mode 100644 index 0000000000..e5e6421e11 --- /dev/null +++ b/spec/factories/pro_accounts.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: pro_accounts +# +# id :integer not null, primary key +# user_id :integer not null +# default_embargo_duration :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryGirl.define do + factory :pro_account do + user + end +end diff --git a/spec/models/pro_account_spec.rb b/spec/models/pro_account_spec.rb new file mode 100644 index 0000000000..ecf6bd52d5 --- /dev/null +++ b/spec/models/pro_account_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +RSpec.describe ProAccount, :type => :model do + let(:account) { FactoryGirl.create(:pro_account) } + + it "belongs to a user" do + expect(account.user).not_to be_nil + end + + it "has a default_embargo_duration field" do + account.default_embargo_duration = "3_months" + expect(account.default_embargo_duration).to eq "3_months" + end +end From 794bf3c9dc0d23b6f9fff032833c6980a9e249dc Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 1 Nov 2016 12:46:39 +0000 Subject: [PATCH 018/311] Add a pro_user factory --- spec/factories/users.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/factories/users.rb b/spec/factories/users.rb index b79e5dd878..34625f35fa 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -47,6 +47,12 @@ name 'Admin User' admin_level 'super' end + factory :pro_user do + name 'Pro User' + after(:create) do |user, evaluator| + create(:pro_account, :user => user) + end + end end end From 559b86f1805171ccd6e7ee6783276459fe6cfbfe Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 1 Nov 2016 12:47:05 +0000 Subject: [PATCH 019/311] Update Gemfile.lock to move alaveteli_features to rails 3 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5c1b2b83a7..6ecca79494 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,7 +49,7 @@ PATH flipper flipper-active_record mime-types (< 3.0.0) - rails (~> 4.0.13) + rails (~> 3.2.22) GEM remote: https://rubygems.org/ From ffe8d210b31b84fc2bd0bed09d0cc0f6d3e6146a Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 1 Nov 2016 12:51:50 +0000 Subject: [PATCH 020/311] Add a pro? method to User --- app/models/user.rb | 6 ++++++ spec/models/user_spec.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 68b2e6855d..6dd86fd681 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -70,6 +70,8 @@ class User < ActiveRecord::Base :dependent => :destroy has_many :request_classifications, :dependent => :destroy + has_one :pro_account, + :dependent => :destroy scope :not_banned, -> { where(ban_text: "") } @@ -543,6 +545,10 @@ def for_admin_column(complete = false) end end + def pro? + pro_account.present? + end + private def create_new_salt diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0f168ed216..22975d118b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -999,4 +999,16 @@ end + describe '#pro?' do + it 'returns true if the user has a pro account' do + user = FactoryGirl.create(:pro_user) + expect(user.pro?).to be true + end + + it 'returns false if the user doesnt have a pro account' do + user = FactoryGirl.create(:user) + expect(user.pro?).to be false + end + end + end From 41507e0e239d00665f45c4468ce361ddf78f3725 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 1 Nov 2016 13:16:21 +0000 Subject: [PATCH 021/311] Add a pro scope to User --- app/models/user.rb | 4 ++++ spec/models/user_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 6dd86fd681..1ef8770c23 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -109,6 +109,10 @@ class User < ActiveRecord::Base has_one_time_password :counter_based => true + def self.pro + includes(:pro_account).where("pro_accounts.id IS NOT NULL") + end + # Return user given login email, password and other form parameters (e.g. name) # # The specific_user_login parameter says that login as a particular user is diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 22975d118b..601bb836ef 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1011,4 +1011,13 @@ end end + describe 'pro scope' do + it "only includes pro user" do + pro_user = FactoryGirl.create(:pro_user) + user = FactoryGirl.create(:user) + expect(User.pro.include?(pro_user)).to be true + expect(User.pro.include?(user)).to be false + end + end + end From 4dbabe34843c0295ab41d87799c80a6d0686121e Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 7 Oct 2016 10:08:23 +0100 Subject: [PATCH 022/311] Install and use AlaveteliFeatures for enabling annotations --- app/views/request/_after_actions.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/request/_after_actions.html.erb b/app/views/request/_after_actions.html.erb index 79425145ae..c403653f5a 100644 --- a/app/views/request/_after_actions.html.erb +++ b/app/views/request/_after_actions.html.erb @@ -65,7 +65,7 @@ <% end %> - <% if if feature_enabled?(:annotations) && @info_request.comments_allowed? %> + <% if feature_enabled?(:annotations) && @info_request.comments_allowed? %>
  • <%= link_to _('Add an annotation'), new_comment_path(:url_title => @info_request.url_title) %>
  • From c3cf8bc0a8049b644f8d70d113867404bb7de8e2 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 1 Nov 2016 16:20:29 +0000 Subject: [PATCH 023/311] Add a model for storing embargo information. --- app/models/embargo.rb | 16 ++++++++++++ app/models/info_request.rb | 1 + config/initializers/inflections.rb | 6 +++-- db/migrate/20161101151318_create_embargoes.rb | 10 +++++++ spec/factories/embargos.rb | 18 +++++++++++++ spec/models/embargo_spec.rb | 26 +++++++++++++++++++ 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 app/models/embargo.rb create mode 100644 db/migrate/20161101151318_create_embargoes.rb create mode 100644 spec/factories/embargos.rb create mode 100644 spec/models/embargo_spec.rb diff --git a/app/models/embargo.rb b/app/models/embargo.rb new file mode 100644 index 0000000000..5f91c1d762 --- /dev/null +++ b/app/models/embargo.rb @@ -0,0 +1,16 @@ +# -*- encoding : utf-8 -*- +# == Schema Information +# +# Table name: embargos +# +# id :integer not null, primary key +# info_request_id :integer not null +# publish_at :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Embargo < ActiveRecord::Base + belongs_to :info_request + validates_presence_of :info_request +end diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 0b768ec489..184b9f3f83 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -76,6 +76,7 @@ class InfoRequest < ActiveRecord::Base has_many :comments, :order => 'created_at', :dependent => :destroy has_many :censor_rules, :order => 'created_at desc', :dependent => :destroy has_many :mail_server_logs, :order => 'mail_server_log_done_id, "order"', :dependent => :destroy + has_one :embargo attr_accessor :is_batch_request_template attr_reader :followup_bad_reason diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index e3c680d365..5cdf141a7a 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -3,9 +3,11 @@ # Add new inflection rules using the following format # (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| +ActiveSupport::Inflector.inflections do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) -# end + inflect.plural /^(embargo)$/i, '\1es' + inflect.singular /^(embargo)es/i, '\1' +end diff --git a/db/migrate/20161101151318_create_embargoes.rb b/db/migrate/20161101151318_create_embargoes.rb new file mode 100644 index 0000000000..4cd2e1e737 --- /dev/null +++ b/db/migrate/20161101151318_create_embargoes.rb @@ -0,0 +1,10 @@ +# -*- encoding : utf-8 -*- +class CreateEmbargoes < ActiveRecord::Migration + def change + create_table :embargoes do |t| + t.belongs_to :info_request, index: true + t.column :publish_at, :datetime, null: false + t.timestamps + end + end +end diff --git a/spec/factories/embargos.rb b/spec/factories/embargos.rb new file mode 100644 index 0000000000..e87b104f52 --- /dev/null +++ b/spec/factories/embargos.rb @@ -0,0 +1,18 @@ +# -*- encoding : utf-8 -*- +# == Schema Information +# +# Table name: embargos +# +# id :integer not null, primary key +# info_request_id :integer not null +# publish_at :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryGirl.define do + factory :embargo do + info_request + publish_at Time.now + 3.months + end +end diff --git a/spec/models/embargo_spec.rb b/spec/models/embargo_spec.rb new file mode 100644 index 0000000000..f0c44c4069 --- /dev/null +++ b/spec/models/embargo_spec.rb @@ -0,0 +1,26 @@ +# -*- encoding : utf-8 -*- +# == Schema Information +# +# Table name: embargos +# +# id :integer not null, primary key +# info_request_id :integer not null +# publish_at :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# + +require 'spec_helper' + +describe Embargo, :type => :model do + let(:embargo) { FactoryGirl.create(:embargo) } + + it 'belongs to an info_request' do + expect(embargo.info_request).not_to be_nil + end + + it 'has a publish_at field' do + expect(embargo.publish_at).to be_a(ActiveSupport::TimeWithZone) + end + +end From a366acbc6b8fc52a6f9b1c9d6d6e528c0b33ee43 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 1 Nov 2016 17:47:18 +0000 Subject: [PATCH 024/311] Make handling of prominence a bit more consistent and isolated. --- app/controllers/request_controller.rb | 3 +- app/controllers/widget_votes_controller.rb | 2 +- app/controllers/widgets_controller.rb | 2 +- app/models/concerns/message_prominence.rb | 24 ++++ app/models/incoming_message.rb | 4 +- app/models/info_request.rb | 59 +++++---- app/models/info_request/prominence.rb | 11 ++ .../info_request/prominence/calculator.rb | 41 ++++++ .../info_request/prominence/visible_query.rb | 14 ++ app/models/outgoing_message.rb | 3 +- app/views/admin_request/edit.html.erb | 2 +- app/views/admin_request/show.html.erb | 4 +- app/views/request/_bubble.html.erb | 2 +- app/views/request/_sidebar.html.erb | 2 +- app/views/request/hidden.html.erb | 2 +- app/views/request/show.html.erb | 6 +- config/initializers/alaveteli.rb | 1 - lib/message_prominence.rb | 22 ---- spec/controllers/request_controller_spec.rb | 4 +- .../prominence/calculator_spec.rb | 121 ++++++++++++++++++ .../prominence/visible_query_spec.rb | 16 +++ spec/models/info_request_spec.rb | 34 +++-- .../request/_after_actions.html.erb_spec.rb | 3 +- 23 files changed, 297 insertions(+), 85 deletions(-) create mode 100644 app/models/concerns/message_prominence.rb create mode 100644 app/models/info_request/prominence.rb create mode 100644 app/models/info_request/prominence/calculator.rb create mode 100644 app/models/info_request/prominence/visible_query.rb delete mode 100644 lib/message_prominence.rb create mode 100644 spec/models/info_request/prominence/calculator_spec.rb create mode 100644 spec/models/info_request/prominence/visible_query_spec.rb diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 5e3edf441e..5436fd1060 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -583,7 +583,8 @@ def authenticate_attachment end # Is this a completely public request that we can cache attachments for # to be served up without authentication? - if incoming_message.info_request.all_can_view? && incoming_message.all_can_view? + if incoming_message.info_request.prominence(:decorate => true).is_public? && + incoming_message.is_public? @files_can_be_cached = true end end diff --git a/app/controllers/widget_votes_controller.rb b/app/controllers/widget_votes_controller.rb index 089827f844..27ea4326c7 100644 --- a/app/controllers/widget_votes_controller.rb +++ b/app/controllers/widget_votes_controller.rb @@ -43,7 +43,7 @@ def find_info_request end def check_prominence - unless @info_request.prominence == 'normal' + unless @info_request.prominence(:decorate => true).is_searchable? render :nothing => true, :status => :forbidden end end diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 3ea14bd10a..e915b10815 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -50,7 +50,7 @@ def find_info_request end def check_prominence - unless @info_request.prominence == 'normal' + unless @info_request.prominence(:decorate => true).is_searchable? render :nothing => true, :status => :forbidden end end diff --git a/app/models/concerns/message_prominence.rb b/app/models/concerns/message_prominence.rb new file mode 100644 index 0000000000..3e59d69c26 --- /dev/null +++ b/app/models/concerns/message_prominence.rb @@ -0,0 +1,24 @@ +# -*- encoding : utf-8 -*- +module MessageProminence + + extend ActiveSupport::Concern + + included do + validates_inclusion_of :prominence, :in => self.prominence_states + end + + def indexed_by_search? + is_public? + end + + def is_public? + self.prominence == 'normal' + end + + module ClassMethods + def prominence_states + ['normal', 'hidden','requester_only'] + end + end + +end diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index bff989f822..feb993f2ad 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -38,7 +38,7 @@ class IncomingMessage < ActiveRecord::Base include AdminColumn - extend MessageProminence + include MessageProminence MAX_ATTACHMENT_TEXT_CLIPPED = 1000000 # 1Mb ish @@ -57,8 +57,6 @@ class IncomingMessage < ActiveRecord::Base belongs_to :raw_email, :dependent => :destroy - has_prominence - after_destroy :update_request after_update :update_request before_destroy :destroy_email_file diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 184b9f3f83..f5b262ceed 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -82,17 +82,11 @@ class InfoRequest < ActiveRecord::Base has_tag_string - scope :visible, -> { where(prominence: "normal") } + scope :visible, Prominence::VisibleQuery.new # user described state (also update in info_request_event, admin_request/edit.rhtml) validate :must_be_valid_state - - validates_inclusion_of :prominence, :in => [ - 'normal', - 'backpage', - 'hidden', - 'requester_only' - ] + validates_inclusion_of :prominence, :in => Prominence::VALUES validates_inclusion_of :law_used, :in => [ 'foi', # Freedom of Information Act @@ -170,6 +164,23 @@ def report_reasons _("Other")] end + + # Public: Overrides the ActiveRecord attribute accessor + # + # opts = Hash of options (default: {}) + # :decorate - Wrap the string in a ProminenceCalculator decorator that + # has methods indicating whether the InfoRequest is public, searchable + # etc. + # Returns a String or ProminenceCalculator + def prominence(opts = {}) + decorate = opts.fetch(:decorate, false) + if decorate + Prominence::Calculator.new(self) + else + read_attribute(:prominence) + end + end + def must_be_valid_state unless InfoRequest.enumerate_states.include?(described_state) errors.add(:described_state, "is not a valid state") @@ -180,6 +191,10 @@ def is_batch_request_template? is_batch_request_template == true end + def indexed_by_search? + prominence(:decorate => true).is_searchable? + end + # The request must either be internal, in which case it has # a foreign key reference to a User object and no external_url or external_user_name, # or else be external in which case it has no user_id but does have an external_url, @@ -779,7 +794,7 @@ def get_last_public_response end def public_outgoing_events - info_request_events.select{|e| e.outgoing? && e.outgoing_message.all_can_view? } + info_request_events.select{|e| e.outgoing? && e.outgoing_message.is_public? } end # The last public outgoing message @@ -792,7 +807,7 @@ def initial_request_text return '' if outgoing_messages.empty? body_opts = { :censor_rules => applicable_censor_rules } first_message = outgoing_messages.first - first_message.all_can_view? ? first_message.get_text_for_indexing(true, body_opts) : '' + first_message.is_public? ? first_message.get_text_for_indexing(true, body_opts) : '' end # Returns index of last event which is described or nil if none described. @@ -1033,7 +1048,7 @@ def make_zip_cache_path(user) def zip_cache_file_suffix(user) # Simple short circuit for requests where everything is public - if all_can_view_all_correspondence? + if all_correspondence_is_public? "" # If the user can view hidden things, they can view anything, so no need # to go any further @@ -1064,7 +1079,7 @@ def who_can_followup_to(skip_message = nil) end incoming_message.safe_mail_from - next if ! incoming_message.all_can_view? + next if ! incoming_message.is_public? email = OutgoingMailer.email_for_followup(self, incoming_message) name = OutgoingMailer.name_for_followup(self, incoming_message) @@ -1135,22 +1150,22 @@ def is_actual_owning_user?(user) user.id == user_id end - # Is this request visible to everyone? def all_can_view? - %w(normal backpage).include?(prominence) + warn %q([DEPRECATION] InfoRequest#all_can_view? will be removed in + 0.27. It has been replaced by InfoRequest.prominence#is_public?).squish + prominence(:decorate => true).is_public? end def all_can_view_all_correspondence? - all_can_view? && - incoming_messages.all?{ |message| message.all_can_view? } && - outgoing_messages.all?{ |message| message.all_can_view? } + warn %q([DEPRECATION] InfoRequest#all_can_view_all_correspondence? will be removed in + 0.27. It has been replaced by InfoRequest#all_correspondence_is_public?).squish + all_correspondence_is_public? end - def indexed_by_search? - if prominence == 'backpage' || prominence == 'hidden' || prominence == 'requester_only' - return false - end - true + def all_correspondence_is_public? + prominence(:decorate => true).is_public? && + incoming_messages.all?{ |message| message.is_public? } && + outgoing_messages.all?{ |message| message.is_public? } end def self.reject_incoming_at_mta(options) diff --git a/app/models/info_request/prominence.rb b/app/models/info_request/prominence.rb new file mode 100644 index 0000000000..a0b2de4c0c --- /dev/null +++ b/app/models/info_request/prominence.rb @@ -0,0 +1,11 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module Prominence + + VALUES = [ 'normal', + 'backpage', + 'hidden', + 'requester_only' ] + + end +end diff --git a/app/models/info_request/prominence/calculator.rb b/app/models/info_request/prominence/calculator.rb new file mode 100644 index 0000000000..0457861146 --- /dev/null +++ b/app/models/info_request/prominence/calculator.rb @@ -0,0 +1,41 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module Prominence + class Calculator + + def initialize(info_request) + @info_request = info_request + end + + # Is this request visible to everyone? + def is_public? + %w(normal backpage).include?(to_s) + end + + # Is this request findable via search? + def is_searchable? + to_s == 'normal' + end + + # Is this request hidden from some people? + def is_private? + %w(hidden requester_only).include?(to_s) + end + + # Is this request visible only to admins and the requester? + def is_requester_only? + to_s == 'requester_only' + end + + # Is this request visible only to admins? + def is_hidden? + to_s == 'hidden' + end + + def to_s + @info_request.read_attribute(:prominence) + end + + end + end +end diff --git a/app/models/info_request/prominence/visible_query.rb b/app/models/info_request/prominence/visible_query.rb new file mode 100644 index 0000000000..948def73ae --- /dev/null +++ b/app/models/info_request/prominence/visible_query.rb @@ -0,0 +1,14 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module Prominence + class VisibleQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.where(prominence: 'normal') + end + end + end +end diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb index 2655fd0721..9437203099 100644 --- a/app/models/outgoing_message.rb +++ b/app/models/outgoing_message.rb @@ -27,7 +27,7 @@ class OutgoingMessage < ActiveRecord::Base include AdminColumn - extend MessageProminence + include MessageProminence include Rails.application.routes.url_helpers include LinkToHelper @@ -62,7 +62,6 @@ class OutgoingMessage < ActiveRecord::Base after_update :xapian_reindex_after_update strip_attributes :allow_empty => true - has_prominence self.default_url_options[:host] = AlaveteliConfiguration.domain diff --git a/app/views/admin_request/edit.html.erb b/app/views/admin_request/edit.html.erb index c29d03cdef..51d344bfef 100644 --- a/app/views/admin_request/edit.html.erb +++ b/app/views/admin_request/edit.html.erb @@ -8,7 +8,7 @@ <%= text_field 'info_request', 'title', :size => 50 %>

    - <%= select( 'info_request', "prominence", [ "normal", "backpage", "requester_only", "hidden" ]) %> + <%= select( 'info_request', "prominence", InfoRequest::Prominence::VALUES) %> (backpage means hidden from lists/search; hidden means completely hidden; super users can see anything)

    diff --git a/app/views/admin_request/show.html.erb b/app/views/admin_request/show.html.erb index 636beb67e5..08608cc29c 100644 --- a/app/views/admin_request/show.html.erb +++ b/app/views/admin_request/show.html.erb @@ -137,7 +137,7 @@ <% end %>
    - <% if ['hidden', 'requester_only'].include? @info_request.prominence %> + <% if @info_request.prominence(:decorate => true).is_private? %>

    This request has already been hidden

    <% else %>

- <% if !['hidden', 'requester_only'].include? @info_request.prominence %> + <% if !@info_request.prominence(:decorate => true).is_private? %> <% if ! @info_request.is_external? %>
diff --git a/app/views/request/_bubble.html.erb b/app/views/request/_bubble.html.erb index 6b0312bde2..67d0c6be30 100644 --- a/app/views/request/_bubble.html.erb +++ b/app/views/request/_bubble.html.erb @@ -35,7 +35,7 @@

<%= a.display_size %> <%= link_to "Download", attachment_path %> - <% if a.has_body_as_html? && incoming_message.info_request.all_can_view? %> + <% if a.has_body_as_html? && incoming_message.info_request.prominence(:decorate => true).is_public? %> <%= link_to "View as HTML", attachment_as_html_path %> <% end %> diff --git a/app/views/request/_sidebar.html.erb b/app/views/request/_sidebar.html.erb index 7713054157..76abff5ce8 100644 --- a/app/views/request/_sidebar.html.erb +++ b/app/views/request/_sidebar.html.erb @@ -11,7 +11,7 @@ <%= _("This request has prominence 'hidden'. You can only see it " \ "because you are logged in as a super user.") %>

- <% elsif @info_request.prominence == 'requester_only' %> + <% elsif @info_request.prominence(:decorate => true).is_requester_only? %>

<%= _('Request hidden') %>

<%# The eccentric formatting of the following string is in order that it be identical to the corresponding string in request/show.html.erb %> diff --git a/app/views/request/hidden.html.erb b/app/views/request/hidden.html.erb index 643041d5fd..d06e188785 100644 --- a/app/views/request/hidden.html.erb +++ b/app/views/request/hidden.html.erb @@ -11,7 +11,7 @@ "any questions.", :url => help_contact_path.html_safe) %>

-<% if @info_request.prominence == 'requester_only' %> +<% if @info_request.prominence(:decorate => true).is_requester_only? %>

<%= _('If you are the requester, then you may sign in ' \ 'to view the request.', diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index b003b36e36..9226c13a49 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -35,7 +35,7 @@ <% end %> -<% if @info_request.all_can_view? %> +<% if @info_request.prominence(:decorate => true).is_public? %> <% content_for :open_graph_tags do %> @@ -54,14 +54,14 @@ <% end %> <% end %> -<% if @info_request.prominence == 'hidden' %> +<% if @info_request.prominence(:decorate => true).is_hidden? %>

<%= _("This request has prominence 'hidden'. You can only see it because " \ "you are logged in as a super user.") %>

<% end %> -<% if @info_request.prominence == 'requester_only' %> +<% if @info_request.prominence(:decorate => true).is_requester_only? %>

<%= _("This request is hidden, so that only you the requester can see it. " \ "Please contact us if you are not sure why.", diff --git a/config/initializers/alaveteli.rb b/config/initializers/alaveteli.rb index 12a1225465..3fb8532c7f 100644 --- a/config/initializers/alaveteli.rb +++ b/config/initializers/alaveteli.rb @@ -46,7 +46,6 @@ require 'normalize_string' require 'alaveteli_file_types' require 'alaveteli_localization' -require 'message_prominence' require 'theme' require 'xapian_queries' require 'date_quarter' diff --git a/lib/message_prominence.rb b/lib/message_prominence.rb deleted file mode 100644 index 84b7934709..0000000000 --- a/lib/message_prominence.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -*- encoding : utf-8 -*- -module MessageProminence - - def has_prominence - send :include, InstanceMethods - cattr_accessor :prominence_states - self.prominence_states = ['normal', 'hidden','requester_only'] - validates_inclusion_of :prominence, :in => self.prominence_states - end - - module InstanceMethods - - def indexed_by_search? - self.prominence == 'normal' - end - - def all_can_view? - self.prominence == 'normal' - end - - end -end diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 4291dacbb3..91ba4b6208 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2306,7 +2306,7 @@ def expect_redirect(status, redirect_path) it "should not fail with long filenames" do long_name = "blahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblah.txt" info_request = double(InfoRequest, :prominence => 'normal', - :all_can_view? => true) + :is_public? => true) incoming_message = double(IncomingMessage, :info_request => info_request, :parse_raw_email! => true, :info_request_id => 132, @@ -2314,7 +2314,7 @@ def expect_redirect(status, redirect_path) :get_attachments_for_display => nil, :apply_masks => nil, :prominence => 'normal', - :all_can_view? => true) + :is_public? => true) attachment = FactoryGirl.build(:body_text, :filename => long_name) allow(IncomingMessage).to receive(:find).with("44").and_return(incoming_message) allow(IncomingMessage).to receive(:get_attachment_by_url_part_number_and_filename).and_return(attachment) diff --git a/spec/models/info_request/prominence/calculator_spec.rb b/spec/models/info_request/prominence/calculator_spec.rb new file mode 100644 index 0000000000..349e0aa71e --- /dev/null +++ b/spec/models/info_request/prominence/calculator_spec.rb @@ -0,0 +1,121 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::Prominence::Calculator do + + let(:info_request){ FactoryGirl.build(:info_request) } + + def expect_value(prominence, method, value) + info_request.prominence = prominence + expect(described_class.new(info_request).send(method)).to eq(value) + end + + describe '#is_public?' do + + it 'returns true if its prominence is normal' do + expect_value('normal', :is_public?, true) + end + + it 'returns true if its prominence is backpage' do + expect_value('backpage', :is_public?, true) + end + + it 'returns false if its prominence is hidden' do + expect_value('hidden', :is_public?, false) + end + + it 'returns false if its prominence is requester_only' do + expect_value('requester_only', :is_public?, false) + end + + end + + describe '#is_searchable?' do + + it 'returns true if its prominence is normal' do + expect_value('normal', :is_searchable?, true) + end + + it 'returns false if its prominence is backpage' do + expect_value('backpage', :is_searchable?, false) + end + + it 'returns false if its prominence is hidden' do + expect_value('hidden', :is_searchable?, false) + end + + it 'returns false if its prominence is requester_only' do + expect_value('requester_only', :is_searchable?, false) + end + + end + + describe '#is_private?' do + + it 'returns false if its prominence is normal' do + expect_value('normal', :is_private?, false) + end + + it 'returns false if its prominence is backpage' do + expect_value('backpage', :is_private?, false) + end + + it 'returns true if its prominence is hidden' do + expect_value('hidden', :is_private?, true) + end + + it 'returns true if its prominence is requester_only' do + expect_value('requester_only', :is_private?, true) + end + + end + + describe '#is_requester_only?' do + + it 'returns false if its prominence is normal' do + expect_value('normal', :is_requester_only?, false) + end + + it 'returns false if its prominence is backpage' do + expect_value('backpage', :is_requester_only?, false) + end + + it 'returns false if its prominence is hidden' do + expect_value('hidden', :is_requester_only?, false) + end + + it 'returns true if its prominence is requester_only' do + expect_value('requester_only', :is_requester_only?, true) + end + + end + + describe '#is_hidden?' do + + it 'returns false if its prominence is normal' do + expect_value('normal', :is_hidden?, false) + end + + it 'returns false if its prominence is backpage' do + expect_value('backpage', :is_hidden?, false) + end + + it 'returns true if its prominence is hidden' do + expect_value('hidden', :is_hidden?, true) + end + + it 'returns false if its prominence is requester_only' do + expect_value('requester_only', :is_hidden?, false) + end + + end + + describe '#to_s' do + + it 'returns the prominence of the request' do + expect_value('normal', :to_s, 'normal') + end + + end + +end diff --git a/spec/models/info_request/prominence/visible_query_spec.rb b/spec/models/info_request/prominence/visible_query_spec.rb new file mode 100644 index 0000000000..31dbd19b08 --- /dev/null +++ b/spec/models/info_request/prominence/visible_query_spec.rb @@ -0,0 +1,16 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::Prominence::VisibleQuery do + + describe '#call' do + + it 'returns only results with a normal prominence' do + normal_request = FactoryGirl.create(:info_request) + hidden_request = FactoryGirl.create(:info_request, :prominence => 'hidden') + expect(described_class.new.call).to include(normal_request) + expect(described_class.new.call).not_to include(hidden_request) + end + end + +end diff --git a/spec/models/info_request_spec.rb b/spec/models/info_request_spec.rb index 5b9aabbaa8..0f0aa388af 100644 --- a/spec/models/info_request_spec.rb +++ b/spec/models/info_request_spec.rb @@ -1129,6 +1129,12 @@ expect(info_request.errors[:public_body_id]).to be_empty end + it 'rejects an invalid prominence' do + info_request = InfoRequest.new(:prominence => 'something') + info_request.valid? + expect(info_request.errors[:prominence]).to include("is not included in the list") + end + end describe 'when generating a user name slug' do @@ -1809,31 +1815,21 @@ def create_old_unclassified_holding_pen end + describe '#prominence' do - describe 'when an instance is asked if all can view it' do - - before do - @info_request = InfoRequest.new - end + let(:info_request){ FactoryGirl.build(:info_request) } - it 'returns true if its prominence is normal' do - @info_request.prominence = 'normal' - expect(@info_request.all_can_view?).to eq(true) + it 'returns the prominence of the request' do + expect(info_request.prominence).to eq("normal") end - it 'returns true if its prominence is backpage' do - @info_request.prominence = 'backpage' - expect(@info_request.all_can_view?).to eq(true) - end + context ':decorate option is true' do - it 'returns false if its prominence is hidden' do - @info_request.prominence = 'hidden' - expect(@info_request.all_can_view?).to eq(false) - end + it 'returns a prominence calculator' do + expect(InfoRequest.new.prominence(:decorate => true)) + .to be_a(InfoRequest::Prominence::Calculator) + end - it 'returns false if its prominence is requester_only' do - @info_request.prominence = 'requester_only' - expect(@info_request.all_can_view?).to eq(false) end end diff --git a/spec/views/request/_after_actions.html.erb_spec.rb b/spec/views/request/_after_actions.html.erb_spec.rb index c09cedcdb3..7a9b000842 100644 --- a/spec/views/request/_after_actions.html.erb_spec.rb +++ b/spec/views/request/_after_actions.html.erb_spec.rb @@ -15,11 +15,10 @@ :public_body => @mock_body, :comments_allowed? => true, :url_title => 'test_request', - :all_can_view? => true) + :is_public? => true) @mock_track = mock_model(TrackThing, :track_type => 'request_updates', :info_request => @mock_request) - assign :info_request, @mock_request assign :track_thing, @mock_track end From e8df9ca613add54d74e954f230c045d1abae297b Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 4 Nov 2016 11:53:05 +0000 Subject: [PATCH 025/311] Add scopes for embargoed and not embargoed. --- app/models/info_request.rb | 2 ++ .../prominence/embargoed_query.rb | 14 ++++++++++++++ .../prominence/not_embargoed_query.rb | 19 +++++++++++++++++++ spec/factories/info_requests.rb | 6 ++++++ .../prominence/embargoed_query_spec.rb | 15 +++++++++++++++ .../prominence/not_embargoed_query_spec.rb | 14 ++++++++++++++ 6 files changed, 70 insertions(+) create mode 100644 app/models/info_request/prominence/embargoed_query.rb create mode 100644 app/models/info_request/prominence/not_embargoed_query.rb create mode 100644 spec/models/info_request/prominence/embargoed_query_spec.rb create mode 100644 spec/models/info_request/prominence/not_embargoed_query_spec.rb diff --git a/app/models/info_request.rb b/app/models/info_request.rb index f5b262ceed..20a5ecabc8 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -83,6 +83,8 @@ class InfoRequest < ActiveRecord::Base has_tag_string scope :visible, Prominence::VisibleQuery.new + scope :embargoed, Prominence::EmbargoedQuery.new + scope :not_embargoed, Prominence::NotEmbargoedQuery.new # user described state (also update in info_request_event, admin_request/edit.rhtml) validate :must_be_valid_state diff --git a/app/models/info_request/prominence/embargoed_query.rb b/app/models/info_request/prominence/embargoed_query.rb new file mode 100644 index 0000000000..51dc79838b --- /dev/null +++ b/app/models/info_request/prominence/embargoed_query.rb @@ -0,0 +1,14 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module Prominence + class EmbargoedQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.includes(:embargo).where('embargoes.id IS NOT NULL') + end + end + end +end diff --git a/app/models/info_request/prominence/not_embargoed_query.rb b/app/models/info_request/prominence/not_embargoed_query.rb new file mode 100644 index 0000000000..9623ddd234 --- /dev/null +++ b/app/models/info_request/prominence/not_embargoed_query.rb @@ -0,0 +1,19 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module Prominence + class NotEmbargoedQuery + + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + # Specify an outer join as the default inner join + # will not retrieve NULL records which is what we want here + @relation.joins('LEFT OUTER JOIN embargoes + ON embargoes.info_request_id = info_requests.id') + .where('embargoes.id IS NULL') + end + end + end +end diff --git a/spec/factories/info_requests.rb b/spec/factories/info_requests.rb index 7c06af4184..5555c67497 100644 --- a/spec/factories/info_requests.rb +++ b/spec/factories/info_requests.rb @@ -65,6 +65,12 @@ end end + factory :embargoed_request do + after(:create) do |info_request, evaluator| + embargo = create(:embargo, :info_request => info_request) + end + end + factory :external_request do user nil external_user_name 'External User' diff --git a/spec/models/info_request/prominence/embargoed_query_spec.rb b/spec/models/info_request/prominence/embargoed_query_spec.rb new file mode 100644 index 0000000000..6b2643789c --- /dev/null +++ b/spec/models/info_request/prominence/embargoed_query_spec.rb @@ -0,0 +1,15 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::Prominence::EmbargoedQuery do + + describe '#call' do + + it 'limits the requests to those that have embargoes' do + info_request = FactoryGirl.create(:info_request) + embargoed_request = FactoryGirl.create(:embargoed_request) + expect(described_class.new.call).to eq([embargoed_request]) + end + + end +end diff --git a/spec/models/info_request/prominence/not_embargoed_query_spec.rb b/spec/models/info_request/prominence/not_embargoed_query_spec.rb new file mode 100644 index 0000000000..91d6c9cf7c --- /dev/null +++ b/spec/models/info_request/prominence/not_embargoed_query_spec.rb @@ -0,0 +1,14 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::Prominence::NotEmbargoedQuery do + + describe '#call' do + + it 'limits the requests to those that do not have embargoes' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect(described_class.new.call).not_to include(embargoed_request) + end + + end +end From e81bbdf89db965018ecc52c8fb765cf3f8b7a231 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 4 Nov 2016 11:53:28 +0000 Subject: [PATCH 026/311] Visible scope includes not embargoed. --- app/models/info_request/prominence/visible_query.rb | 2 +- spec/models/info_request/prominence/visible_query_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/info_request/prominence/visible_query.rb b/app/models/info_request/prominence/visible_query.rb index 948def73ae..d6ab6bef9d 100644 --- a/app/models/info_request/prominence/visible_query.rb +++ b/app/models/info_request/prominence/visible_query.rb @@ -7,7 +7,7 @@ def initialize(relation = InfoRequest) end def call - @relation.where(prominence: 'normal') + @relation.where(prominence: 'normal').not_embargoed end end end diff --git a/spec/models/info_request/prominence/visible_query_spec.rb b/spec/models/info_request/prominence/visible_query_spec.rb index 31dbd19b08..399ba4fe78 100644 --- a/spec/models/info_request/prominence/visible_query_spec.rb +++ b/spec/models/info_request/prominence/visible_query_spec.rb @@ -11,6 +11,11 @@ expect(described_class.new.call).to include(normal_request) expect(described_class.new.call).not_to include(hidden_request) end + + it 'does not return an embargoed request with normal prominence' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect(described_class.new.call).not_to include(embargoed_request) + end end end From 9a28df172da42da10950d2952de41eabd986dd68 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 4 Nov 2016 11:54:49 +0000 Subject: [PATCH 027/311] Calculator methods include embargo status. --- .../info_request/prominence/calculator.rb | 7 +- .../delivery_statuses_controller_spec.rb | 28 +++++--- .../prominence/calculator_spec.rb | 67 +++++++++++++++++++ 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/app/models/info_request/prominence/calculator.rb b/app/models/info_request/prominence/calculator.rb index 0457861146..0f7fc74364 100644 --- a/app/models/info_request/prominence/calculator.rb +++ b/app/models/info_request/prominence/calculator.rb @@ -7,19 +7,18 @@ def initialize(info_request) @info_request = info_request end - # Is this request visible to everyone? def is_public? - %w(normal backpage).include?(to_s) + %w(normal backpage).include?(to_s) && !@info_request.embargo end # Is this request findable via search? def is_searchable? - to_s == 'normal' + to_s == 'normal' && !@info_request.embargo end # Is this request hidden from some people? def is_private? - %w(hidden requester_only).include?(to_s) + return %w(hidden requester_only).include?(to_s) || @info_request.embargo.present? end # Is this request visible only to admins and the requester? diff --git a/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb b/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb index f50daf849e..5a76ddc910 100644 --- a/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb +++ b/spec/controllers/outgoing_messages/delivery_statuses_controller_spec.rb @@ -16,13 +16,18 @@ @status = MailServerLog::EximDeliveryStatus.new(:normal_message_delivery) end + def visible_info_request + mock_model(InfoRequest, { :prominence => 'normal', + :embargo => nil }) + end + describe 'GET show' do it 'assigns the outgoing message' do session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -37,7 +42,7 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'hidden', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -52,7 +57,8 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'hidden'}), + :info_request => mock_model(InfoRequest, { :prominence => 'hidden', + :embargo => nil }), :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -67,7 +73,7 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -88,7 +94,7 @@ session[:user_id] = FactoryGirl.create(:admin_user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -103,7 +109,7 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -118,7 +124,7 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -138,7 +144,7 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -158,7 +164,7 @@ session[:user_id] = FactoryGirl.create(:admin_user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } @@ -172,7 +178,7 @@ it 'does not assign mail server logs for a regular user' do attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => false, :mail_server_logs => @logs, :delivery_status => @status } @@ -187,7 +193,7 @@ session[:user_id] = FactoryGirl.create(:user).id attrs = { :id => '1', :prominence => 'normal', - :info_request => mock_model(InfoRequest, {:prominence => 'normal'}), + :info_request => visible_info_request, :is_owning_user? => true, :mail_server_logs => @logs, :delivery_status => @status } diff --git a/spec/models/info_request/prominence/calculator_spec.rb b/spec/models/info_request/prominence/calculator_spec.rb index 349e0aa71e..2e18e301c9 100644 --- a/spec/models/info_request/prominence/calculator_spec.rb +++ b/spec/models/info_request/prominence/calculator_spec.rb @@ -4,12 +4,18 @@ describe InfoRequest::Prominence::Calculator do let(:info_request){ FactoryGirl.build(:info_request) } + let(:embargoed_request){ FactoryGirl.create(:embargoed_request) } def expect_value(prominence, method, value) info_request.prominence = prominence expect(described_class.new(info_request).send(method)).to eq(value) end + def expect_embargoed_value(prominence, method, value) + embargoed_request.prominence = prominence + expect(described_class.new(embargoed_request).send(method)).to eq(value) + end + describe '#is_public?' do it 'returns true if its prominence is normal' do @@ -28,6 +34,26 @@ def expect_value(prominence, method, value) expect_value('requester_only', :is_public?, false) end + context 'when there is an embargo' do + + it 'returns false if its prominence is normal' do + expect_embargoed_value('normal', :is_public?, false) + end + + it 'returns false if its prominence is backpage' do + expect_embargoed_value('backpage', :is_public?, false) + end + + it 'returns false if its prominence is hidden' do + expect_embargoed_value('hidden', :is_public?, false) + end + + it 'returns false if its prominence is requester_only' do + expect_embargoed_value('requester_only', :is_public?, false) + end + + end + end describe '#is_searchable?' do @@ -48,6 +74,26 @@ def expect_value(prominence, method, value) expect_value('requester_only', :is_searchable?, false) end + context 'when there is an embargo' do + + it 'returns false if its prominence is normal' do + expect_embargoed_value('normal', :is_searchable?, false) + end + + it 'returns false if its prominence is backpage' do + expect_embargoed_value('backpage', :is_searchable?, false) + end + + it 'returns false if its prominence is hidden' do + expect_embargoed_value('hidden', :is_searchable?, false) + end + + it 'returns false if its prominence is requester_only' do + expect_embargoed_value('requester_only', :is_searchable?, false) + end + + end + end describe '#is_private?' do @@ -68,6 +114,27 @@ def expect_value(prominence, method, value) expect_value('requester_only', :is_private?, true) end + + context 'when there is an embargo' do + + it 'returns true if its prominence is normal' do + expect_embargoed_value('normal', :is_private?, true) + end + + it 'returns true if its prominence is backpage' do + expect_embargoed_value('backpage', :is_private?, true) + end + + it 'returns true if its prominence is hidden' do + expect_embargoed_value('hidden', :is_private?, true) + end + + it 'returns true if its prominence is requester_only' do + expect_embargoed_value('requester_only', :is_private?, true) + end + + end + end describe '#is_requester_only?' do From 53c359470ba0f944e9899046ad4e57f31d179caf Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 7 Nov 2016 17:32:02 +0000 Subject: [PATCH 028/311] Add some methods to indicate user permissions. Currently these all revert back to checking super user status. In a later pull request we intend to back them with finer-grained user permissions. --- app/models/user.rb | 8 ++++++++ spec/models/user_spec.rb | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 1ef8770c23..c7e1de0354 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -174,6 +174,14 @@ def self.view_hidden?(user) !user.nil? && user.super? end + def self.view_embargoed?(user) + self.view_hidden?(user) + end + + def self.view_hidden_and_embargoed?(user) + view_hidden?(user) && view_embargoed?(user) + end + # Should the user be kept logged into their own account # if they follow a /c/ redirect link belonging to another user? def self.stay_logged_in_on_redirect?(user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 601bb836ef..27181c85f2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1020,4 +1020,46 @@ end end + describe '.view_hidden?' do + it 'returns false if there is no user' do + expect(User.view_hidden?(nil)).to be false + end + + it 'returns false if the user is not a superuser' do + expect(User.view_hidden?(FactoryGirl.build(:user))).to be false + end + + it 'returns true if the user is an admin user' do + expect(User.view_hidden?(FactoryGirl.build(:admin_user))).to be true + end + end + + describe '.view_embargoed' do + it 'returns false if there is no user' do + expect(User.view_embargoed?(nil)).to be false + end + + it 'returns false if the user is not a superuser' do + expect(User.view_embargoed?(FactoryGirl.build(:user))).to be false + end + + it 'returns true if the user is an admin user' do + expect(User.view_embargoed?(FactoryGirl.build(:admin_user))).to be true + end + end + + describe '.view_hidden_and_embargoed' do + it 'returns false if there is no user' do + expect(User.view_hidden_and_embargoed?(nil)).to be false + end + + it 'returns false if the user is not a superuser' do + expect(User.view_hidden_and_embargoed?(FactoryGirl.build(:user))).to be false + end + + it 'returns true if the user is an admin user' do + expect(User.view_hidden_and_embargoed?(FactoryGirl.build(:admin_user))).to be true + end + end + end From 4ce36d5bf086909b392408e19603f2952b58397b Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 8 Nov 2016 11:59:23 +0000 Subject: [PATCH 029/311] Add embargo to logic used in can_view_with_prominence? --- app/models/ability.rb | 25 ++++++++++----- spec/models/ability_spec.rb | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 79350423c4..d534618358 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -54,13 +54,24 @@ def self.can_update_request_state?(user, request) end def self.can_view_with_prominence?(prominence, info_request, user) - if prominence == 'hidden' - return User.view_hidden?(user) + if info_request.embargo + case prominence + when 'hidden' + User.view_hidden_and_embargoed?(user) + when 'requester_only' + info_request.is_actual_owning_user?(user) || User.view_hidden_and_embargoed?(user) + else + info_request.is_actual_owning_user?(user) || User.view_embargoed?(user) + end + else + case prominence + when 'hidden' + User.view_hidden?(user) + when 'requester_only' + info_request.is_actual_owning_user?(user) || User.view_hidden?(user) + else + true + end end - if prominence == 'requester_only' - return info_request.is_owning_user?(user) - end - return true end - end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index ead2eef5b8..aae7edfc32 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -82,6 +82,68 @@ let!(:owner_ability) { Ability.new(resource.user) } it_behaves_like "a class with message prominence" + + context 'when the request is embargoed' do + let!(:resource) { FactoryGirl.create(:embargoed_request) } + let(:admin_ability) { Ability.new(FactoryGirl.create(:admin_user)) } + let(:other_user_ability) { Ability.new(FactoryGirl.create(:user)) } + + context 'if the prominence is hidden' do + before do + resource.prominence = 'hidden' + end + + it 'should return true for an admin user' do + expect(admin_ability).to be_able_to(:read, resource) + end + + it 'should return false for a non-admin user' do + expect(other_user_ability).not_to be_able_to(:read, resource) + end + + it 'should return false for the owner' do + expect(owner_ability).not_to be_able_to(:read, resource) + end + end + + context 'if the prominence is requester_only' do + before do + resource.prominence = 'requester_only' + end + + it 'should return true if the user owns the right resource' do + expect(owner_ability).to be_able_to(:read, resource) + end + + it 'should return true for an admin user' do + expect(admin_ability).to be_able_to(:read, resource) + end + + it 'should return false if the user does not own the right resource' do + expect(other_user_ability).not_to be_able_to(:read, resource) + end + end + + context 'if the prominence is normal' do + before do + resource.prominence = 'normal' + end + + it 'should return false for a non-admin user' do + expect(other_user_ability).not_to be_able_to(:read, resource) + end + + it 'should return true for an admin user' do + expect(admin_ability).to be_able_to(:read, resource) + end + + it 'should return true if the user owns the right resource' do + expect(owner_ability).to be_able_to(:read, resource) + end + end + + end + end describe "updating request state of InfoRequests" do From d20f9c43e4d02a4d90b4e8466542bfffcba7903e Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 8 Nov 2016 13:07:23 +0000 Subject: [PATCH 030/311] Make scope names match prominence methods --- app/controllers/admin_general_controller.rb | 2 +- app/controllers/general_controller.rb | 4 ++-- .../info_request_batch_controller.rb | 2 +- app/controllers/public_body_controller.rb | 2 +- app/models/comment.rb | 2 +- app/models/info_request.rb | 9 ++++++- .../info_request/prominence/public_query.rb | 14 +++++++++++ .../{visible_query.rb => searchable_query.rb} | 3 ++- .../general/_frontpage_search_box.html.erb | 2 +- app/views/info_request_batch/show.html.erb | 2 +- .../public_body/_body_listing_single.html.erb | 4 ++-- lib/tasks/stats.rake | 2 +- .../public_body_controller_spec.rb | 2 +- .../prominence/public_query_spec.rb | 24 +++++++++++++++++++ ...query_spec.rb => searchable_query_spec.rb} | 3 ++- spec/views/public_body/list.html.erb_spec.rb | 2 +- 16 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 app/models/info_request/prominence/public_query.rb rename app/models/info_request/prominence/{visible_query.rb => searchable_query.rb} (90%) create mode 100644 spec/models/info_request/prominence/public_query_spec.rb rename spec/models/info_request/prominence/{visible_query_spec.rb => searchable_query_spec.rb} (92%) diff --git a/app/controllers/admin_general_controller.rb b/app/controllers/admin_general_controller.rb index f1bb5f884f..c509c88063 100644 --- a/app/controllers/admin_general_controller.rb +++ b/app/controllers/admin_general_controller.rb @@ -19,7 +19,7 @@ def index select { |pb| !pb.defunct? } @old_unclassified = InfoRequest.where_old_unclassified. limit(20). - where(:prominence => 'normal') + is_searchable @holding_pen_messages = InfoRequest. includes(:incoming_messages => :raw_email). holding_pen_request. diff --git a/app/controllers/general_controller.rb b/app/controllers/general_controller.rb index ed3f929b98..c5b6941541 100644 --- a/app/controllers/general_controller.rb +++ b/app/controllers/general_controller.rb @@ -19,7 +19,7 @@ def frontpage successful_query = InfoRequestEvent.make_query_from_params( :latest_status => ['successful'] ) @request_events, @request_events_all_successful = InfoRequest.recent_requests @track_thing = TrackThing.create_track_for_search_query(successful_query) - @number_of_requests = InfoRequest.visible.count + @number_of_requests = InfoRequest.is_searchable.count @number_of_authorities = PublicBody.visible.count @feed_autodetect = [ { :url => do_track_url(@track_thing, 'feed'), :title => _('Successful requests'), @@ -220,7 +220,7 @@ def version :alaveteli_version => ALAVETELI_VERSION, :ruby_version => RUBY_VERSION, :visible_public_body_count => PublicBody.visible.count, - :visible_request_count => InfoRequest.visible.count, + :visible_request_count => InfoRequest.is_searchable.count, :confirmed_user_count => User.not_banned. where(:email_confirmed => true).count, :visible_comment_count => Comment.visible.count, diff --git a/app/controllers/info_request_batch_controller.rb b/app/controllers/info_request_batch_controller.rb index 1727533ec6..6432bc734a 100644 --- a/app/controllers/info_request_batch_controller.rb +++ b/app/controllers/info_request_batch_controller.rb @@ -6,7 +6,7 @@ def show @per_page = 25 @page = get_search_page_from_params if @info_request_batch.sent_at - @info_requests = @info_request_batch.info_requests.visible.all(:offset => (@page - 1) * @per_page, + @info_requests = @info_request_batch.info_requests.is_searchable.all(:offset => (@page - 1) * @per_page, :limit => @per_page) else @public_bodies = @info_request_batch.public_bodies.all(:offset => (@page - 1) * @per_page, diff --git a/app/controllers/public_body_controller.rb b/app/controllers/public_body_controller.rb index 8fcf57960e..b2c3f85436 100644 --- a/app/controllers/public_body_controller.rb +++ b/app/controllers/public_body_controller.rb @@ -45,7 +45,7 @@ def show set_last_body(@public_body) - @number_of_visible_requests = @public_body.info_requests.visible.count + @number_of_visible_requests = @public_body.info_requests.is_searchable.count @searched_to_send_request = false referrer = request.env['HTTP_REFERER'] diff --git a/app/models/comment.rb b/app/models/comment.rb index 29e14c5470..29682a6178 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -33,7 +33,7 @@ class Comment < ActiveRecord::Base :check_body_uses_mixed_capitals scope :visible, -> { - joins(:info_request).merge(InfoRequest.visible).where(:visible => true) + joins(:info_request).merge(InfoRequest.is_searchable).where(:visible => true) } after_save :event_xapian_update diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 20a5ecabc8..278aa8dcce 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -82,10 +82,17 @@ class InfoRequest < ActiveRecord::Base has_tag_string - scope :visible, Prominence::VisibleQuery.new + scope :is_public, Prominence::PublicQuery.new + scope :is_searchable, Prominence::SearchableQuery.new scope :embargoed, Prominence::EmbargoedQuery.new scope :not_embargoed, Prominence::NotEmbargoedQuery.new + def self.visible + warn %q([DEPRECATION] InfoRequest#visible will be removed in + 0.27. It has been replaced by InfoRequest#is_public).squish + self.is_public + end + # user described state (also update in info_request_event, admin_request/edit.rhtml) validate :must_be_valid_state validates_inclusion_of :prominence, :in => Prominence::VALUES diff --git a/app/models/info_request/prominence/public_query.rb b/app/models/info_request/prominence/public_query.rb new file mode 100644 index 0000000000..8a7bdcb3af --- /dev/null +++ b/app/models/info_request/prominence/public_query.rb @@ -0,0 +1,14 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module Prominence + class PublicQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.where(prominence: ['normal', 'backpage']).not_embargoed + end + end + end +end diff --git a/app/models/info_request/prominence/visible_query.rb b/app/models/info_request/prominence/searchable_query.rb similarity index 90% rename from app/models/info_request/prominence/visible_query.rb rename to app/models/info_request/prominence/searchable_query.rb index d6ab6bef9d..7b55d818f0 100644 --- a/app/models/info_request/prominence/visible_query.rb +++ b/app/models/info_request/prominence/searchable_query.rb @@ -1,7 +1,7 @@ # -*- encoding : utf-8 -*- class InfoRequest module Prominence - class VisibleQuery + class SearchableQuery def initialize(relation = InfoRequest) @relation = relation end @@ -12,3 +12,4 @@ def call end end end + diff --git a/app/views/general/_frontpage_search_box.html.erb b/app/views/general/_frontpage_search_box.html.erb index 747a725d5e..77af362d6f 100644 --- a/app/views/general/_frontpage_search_box.html.erb +++ b/app/views/general/_frontpage_search_box.html.erb @@ -3,7 +3,7 @@ "{{number_of_requests}} requests " \ "and
" \ "{{number_of_authorities}} authorities", - :number_of_requests => number_with_delimiter(InfoRequest.visible.count), + :number_of_requests => number_with_delimiter(InfoRequest.is_searchable.count), :number_of_authorities => number_with_delimiter(PublicBody.visible.count)) %>

- <%= will_paginate WillPaginate::Collection.new(@page, @per_page, @info_request_batch.info_requests.visible.count) %> + <%= will_paginate WillPaginate::Collection.new(@page, @per_page, @info_request_batch.info_requests.is_searchable.count) %> <% else %> diff --git a/app/views/public_body/_body_listing_single.html.erb b/app/views/public_body/_body_listing_single.html.erb index bcb5c82c22..b49a602fc7 100644 --- a/app/views/public_body/_body_listing_single.html.erb +++ b/app/views/public_body/_body_listing_single.html.erb @@ -32,8 +32,8 @@
<%= n_('{{count}} request.', '{{count}} requests.', - public_body.info_requests.visible.size, - :count => public_body.info_requests.visible.size) %> + public_body.info_requests.is_searchable.size, + :count => public_body.info_requests.is_searchable.size) %>
<% if request_link && !public_body.special_not_requestable_reason? %> diff --git a/lib/tasks/stats.rake b/lib/tasks/stats.rake index 10a307537f..1869fa3d36 100644 --- a/lib/tasks/stats.rake +++ b/lib/tasks/stats.rake @@ -278,7 +278,7 @@ namespace :stats do puts CSV.generate_line(["public_body_id", "public_body_name", "request_created_timestamp", "request_title", "request_body"]) PublicBody.limit(20).order('info_requests_visible_count DESC').each do |body| - body.info_requests.where(:prominence => 'normal').find_each do |request| + body.info_requests.is_searchable.find_each do |request| puts CSV.generate_line([request.public_body.id, request.public_body.name, request.created_at, request.url_title, request.initial_request_text.gsub("\r\n", " ").gsub("\n", " ")]) end end diff --git a/spec/controllers/public_body_controller_spec.rb b/spec/controllers/public_body_controller_spec.rb index 2e8b5df4cc..dfa1f5d122 100644 --- a/spec/controllers/public_body_controller_spec.rb +++ b/spec/controllers/public_body_controller_spec.rb @@ -338,7 +338,7 @@ def make_single_language_example(locale) visible_request = FactoryGirl.create(:info_request, :public_body => fake_pb) fake_pb.reload expect(fake_pb.info_requests.size).to eq(2) - expect(fake_pb.info_requests.visible.size).to eq(1) + expect(fake_pb.info_requests.is_searchable.size).to eq(1) fake_list = [fake_pb] allow(fake_list).to receive(:joins).and_return(fake_list) allow(fake_list).to receive(:paginate).and_return(fake_list) diff --git a/spec/models/info_request/prominence/public_query_spec.rb b/spec/models/info_request/prominence/public_query_spec.rb new file mode 100644 index 0000000000..a7391b7020 --- /dev/null +++ b/spec/models/info_request/prominence/public_query_spec.rb @@ -0,0 +1,24 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::Prominence::PublicQuery do + + describe '#call' do + + it 'returns only results with a normal or backpaged prominence' do + normal_request = FactoryGirl.create(:info_request) + backpaged_request = FactoryGirl.create(:info_request, :prominence => 'backpage') + hidden_request = FactoryGirl.create(:info_request, :prominence => 'hidden') + requests = described_class.new.call + expect(requests).to include(normal_request) + expect(requests).to include(backpaged_request) + expect(requests).not_to include(hidden_request) + end + + it 'does not return an embargoed request' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect(described_class.new.call).not_to include(embargoed_request) + end + end + +end \ No newline at end of file diff --git a/spec/models/info_request/prominence/visible_query_spec.rb b/spec/models/info_request/prominence/searchable_query_spec.rb similarity index 92% rename from spec/models/info_request/prominence/visible_query_spec.rb rename to spec/models/info_request/prominence/searchable_query_spec.rb index 399ba4fe78..2f99b54b5e 100644 --- a/spec/models/info_request/prominence/visible_query_spec.rb +++ b/spec/models/info_request/prominence/searchable_query_spec.rb @@ -1,7 +1,8 @@ + # -*- encoding : utf-8 -*- require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') -describe InfoRequest::Prominence::VisibleQuery do +describe InfoRequest::Prominence::SearchableQuery do describe '#call' do diff --git a/spec/views/public_body/list.html.erb_spec.rb b/spec/views/public_body/list.html.erb_spec.rb index c095a924d9..29a43a983b 100644 --- a/spec/views/public_body/list.html.erb_spec.rb +++ b/spec/views/public_body/list.html.erb_spec.rb @@ -14,7 +14,7 @@ :eir_only? => nil, :publication_scheme => '') pb_info_requests = [1, 2, 3, 4] - allow(pb_info_requests).to receive(:visible).and_return([2, 3, 4]) + allow(pb_info_requests).to receive(:is_searchable).and_return([2, 3, 4]) allow(@pb).to receive(:info_requests).and_return(pb_info_requests) From 1acc981b6f55b8cdc19c8caf2f49c5d91c6db2fe Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 8 Nov 2016 13:23:02 +0000 Subject: [PATCH 031/311] Use searchable scope rather than referencing prominence directly. --- app/controllers/request_game_controller.rb | 4 ++-- app/models/info_request.rb | 11 ++++++----- lib/tasks/stats.rake | 3 +-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/request_game_controller.rb b/app/controllers/request_game_controller.rb index b48f99708f..7c4a180c84 100644 --- a/app/controllers/request_game_controller.rb +++ b/app/controllers/request_game_controller.rb @@ -12,7 +12,7 @@ def play @missing = InfoRequest. where_old_unclassified. - where(:prominence => 'normal'). + is_searchable. count @total = InfoRequest.count @done = @total - @missing @@ -21,7 +21,7 @@ def play includes(:public_body, :user). where_old_unclassified. limit(3). - where(:prominence => 'normal'). + is_searchable. order('random()') if @missing == 0 diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 278aa8dcce..06f3d31f11 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -1272,14 +1272,15 @@ def update_counter_cache success_states = ['successful', 'partially_successful'] basic_params = { :public_body_id => public_body_id, - :prominence => 'normal' } - [['info_requests_not_held_count', {:awaiting_description => false, :described_state => 'not_held'}], - ['info_requests_successful_count', {:awaiting_description => false, :described_state => success_states}], - ['info_requests_visible_classified_count', {:awaiting_description => false}], + [['info_requests_not_held_count', { :awaiting_description => false, + :described_state => 'not_held' }], + ['info_requests_successful_count', { :awaiting_description => false, + :described_state => success_states }], + ['info_requests_visible_classified_count', { :awaiting_description => false }], ['info_requests_visible_count', {}]].each do |column, extra_params| params = basic_params.clone.update extra_params - public_body.send "#{column}=", InfoRequest.where(params).count + public_body.send "#{column}=", InfoRequest.where(params).is_searchable.count end public_body.without_revision do public_body.no_xapian_reindex = true diff --git a/lib/tasks/stats.rake b/lib/tasks/stats.rake index 1869fa3d36..6df0df0546 100644 --- a/lib/tasks/stats.rake +++ b/lib/tasks/stats.rake @@ -212,11 +212,10 @@ namespace :stats do # described_state column, and instead need to be calculated: overdue_count = 0 very_overdue_count = 0 - InfoRequest.find_each(:batch_size => 200, + InfoRequest.is_searchable.find_each(:batch_size => 200, :conditions => { :public_body_id => public_body.id, :awaiting_description => false, - :prominence => 'normal' }) do |ir| case ir.calculate_status when 'waiting_response_very_overdue' From 1cbbd8d55f40868c026facfb2930bf423fca80d1 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 8 Nov 2016 13:46:38 +0000 Subject: [PATCH 032/311] Don't send classification reminders for embargoed requests. --- app/mailers/request_mailer.rb | 5 +++-- spec/mailers/request_mailer_spec.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/mailers/request_mailer.rb b/app/mailers/request_mailer.rb index 296d989b13..210ecf0e86 100644 --- a/app/mailers/request_mailer.rb +++ b/app/mailers/request_mailer.rb @@ -345,8 +345,9 @@ def self.alert_new_response_reminders def self.alert_new_response_reminders_internal(days_since, type_code) info_requests = InfoRequest. where_old_unclassified(days_since). - order('info_requests.id'). - includes(:user) + not_embargoed. + order('info_requests.id'). + includes(:user) info_requests.each do |info_request| alert_event_id = info_request.get_last_public_response_event_id diff --git a/spec/mailers/request_mailer_spec.rb b/spec/mailers/request_mailer_spec.rb index 81bc32b57c..d4bc4989dd 100644 --- a/spec/mailers/request_mailer_spec.rb +++ b/spec/mailers/request_mailer_spec.rb @@ -283,6 +283,16 @@ def sent_alert_params(request, type) expect{ send_alerts }.to raise_error(expected_message) end + context 'if the request is embargoed' do + + it 'does not send the reminder' do + old_request.create_embargo(:publish_at => Time.now + 3.days) + expect(RequestMailer).not_to receive(:new_response_reminder_alert) + send_alerts + end + + end + context 'if an alert matching the attributes of the reminder to be sent has already been sent' do it 'should not send the reminder' do From 7dcffcb48a06ebff3c67511e7f7efa8f914a6ad6 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 8 Nov 2016 13:47:28 +0000 Subject: [PATCH 033/311] Update graph SQL - looks for normal, not embargoed requests. --- lib/tasks/graphs.rake | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/tasks/graphs.rake b/lib/tasks/graphs.rake index 8a70603061..571bc16467 100644 --- a/lib/tasks/graphs.rake +++ b/lib/tasks/graphs.rake @@ -151,10 +151,13 @@ namespace :graphs do def assemble_sql(where_clause="") "SELECT DATE(created_at), COUNT(*) " \ "FROM info_requests " \ + "LEFT OUTER JOIN embargoes " \ + "ON embargoes.info_request_id = info_requests.id " \ "WHERE #{where_clause} " \ - "AND PROMINENCE != 'backpage' " \ - "GROUP BY DATE(created_at)" \ - "ORDER BY DATE(created_at)" + "AND PROMINENCE = 'normal' " \ + "AND (embargoes.id IS NULL) " \ + "GROUP BY DATE(info_requests.created_at)" \ + "ORDER BY DATE(info_requests.created_at)" end def state_exclusion_sql(states) From ee322b7d88d2f47c782fe689711e92003557100f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 15 Nov 2016 14:41:32 +0000 Subject: [PATCH 034/311] Add some release notes. --- doc/CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/CHANGES.md b/doc/CHANGES.md index 8038e4248b..a5f84dd002 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -163,6 +163,9 @@ page (Louise Crow) * Added strong parameters gem for better mass assignment security (Gareth Rees) * Added experimental Xapian database replication (Hazel Smith, Louise Crow) +* Request prominence logic more consistent, embargoed requests introduced as an + Alaveteli Pro feature (Louise Crow) + ## Upgrade Notes From c7b5939984878c59896fbb21b6cf8ce46cbe465d Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 17 Nov 2016 14:43:53 +0000 Subject: [PATCH 035/311] Line length fixes. --- app/controllers/request_controller.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 5436fd1060..8382e49fe2 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -142,7 +142,12 @@ def details if cannot?(:read, @info_request) return render_hidden end - @columns = ['id', 'event_type', 'created_at', 'described_state', 'last_described_at', 'calculated_state' ] + @columns = ['id', + 'event_type', + 'created_at', + 'described_state', + 'last_described_at', + 'calculated_state' ] end # Requests similar to this one @@ -156,13 +161,15 @@ def similar raise ActiveRecord::RecordNotFound.new("Sorry. No pages after #{MAX_RESULTS / PER_PAGE}.") end @info_request = InfoRequest.find_by_url_title!(params[:url_title]) - raise ActiveRecord::RecordNotFound.new("Request not found") if @info_request.nil? if cannot?(:read, @info_request) return render_hidden end - @xapian_object = ActsAsXapian::Similar.new([InfoRequestEvent], @info_request.info_request_events, - :offset => (@page - 1) * @per_page, :limit => @per_page, :collapse_by_prefix => 'request_collapse') + @xapian_object = ActsAsXapian::Similar.new([InfoRequestEvent], + @info_request.info_request_events, + :offset => (@page - 1) * @per_page, + :limit => @per_page, + :collapse_by_prefix => 'request_collapse') @matches_estimated = @xapian_object.matches_estimated @show_no_more_than = (@matches_estimated > MAX_RESULTS) ? MAX_RESULTS : @matches_estimated end From 4686fb3e24cd2c3f2e64d166e32ff047c011ec47 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 17 Nov 2016 14:45:26 +0000 Subject: [PATCH 036/311] Cleanup spec a bit. Line length, clarity of description, repetition. --- spec/controllers/request_controller_spec.rb | 24 +++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 91ba4b6208..a765666175 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2265,36 +2265,42 @@ def expect_redirect(status, redirect_path) end describe RequestController, "when showing similar requests" do - render_views before do get_fixtures_xapian_index load_raw_emails_data end - it "should work" do + let(:badger_request){ info_requests(:badger_request) } + + it "renders the 'similar' template" do get :similar, :url_title => info_requests(:badger_request).url_title expect(response).to render_template("request/similar") + end + + it 'assigns the request' do + get :similar, :url_title => info_requests(:badger_request).url_title expect(assigns[:info_request]).to eq(info_requests(:badger_request)) end - it "should show similar requests" do - badger_request = info_requests(:badger_request) + it "assigns a xapian object with similar requests" do get :similar, :url_title => badger_request.url_title # Xapian seems to think *all* the requests are similar - expect(assigns[:xapian_object].results.map{|x|x[:model].info_request}).to match_array(InfoRequest.all.reject {|x| x == badger_request}) + results = assigns[:xapian_object].results + expected = InfoRequest.all.reject{ |request| request == badger_request } + expect(results.map{ |result| result[:model].info_request }) + .to match_array(expected) end - it "should 404 for non-existent paths" do + it "raises ActiveRecord::RecordNotFound for non-existent paths" do expect { get :similar, :url_title => "there_is_really_no_such_path_owNAFkHR" }.to raise_error(ActiveRecord::RecordNotFound) end - - it "should return 404 for pages we don't want to serve up" do - badger_request = info_requests(:badger_request) + it "raises ActiveRecord::RecordNotFound for pages beyond the last + page we want to show" do expect { get :similar, :url_title => badger_request.url_title, :page => 100 }.to raise_error(ActiveRecord::RecordNotFound) From 22e75784b73b654d86f3687b83c985ebf45eb638 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 2 Nov 2016 07:13:44 +0000 Subject: [PATCH 037/311] Add an ENABLE_ALAVETELI_PRO setting and feature --- config/general.yml-example | 15 +++++++++++++++ config/initializers/alaveteli_features.rb | 8 ++++++++ lib/configuration.rb | 1 + 3 files changed, 24 insertions(+) diff --git a/config/general.yml-example b/config/general.yml-example index 11aef065fa..76e345adf7 100644 --- a/config/general.yml-example +++ b/config/general.yml-example @@ -957,3 +957,18 @@ RESTRICTED_COUNTRIES: '' # # --- NEW_REQUEST_RECAPTCHA: false + +# Enable AlaveteliProfessional +# If ENABLE_ALAVETELI_PRO is set to true, Alaveteli will include extra +# functionality and account levels for professional FOI users, e.g. +# journalists. Professional users have access to a new dashboard, a more +# streamlined request process, and crucially, the ability to embargo their +# requests so that they remain private. +# +# Enabling this is a large change, and this is still in Alpha development, so +# you may want to contact the Alaveteli team before doing so. +# +# ENABLE_ALAVETELI_PRO – Boolean (default: false) +# +# --- +ENABLE_ALAVETELI_PRO: false diff --git a/config/initializers/alaveteli_features.rb b/config/initializers/alaveteli_features.rb index 44efe5c637..376dcb91ec 100644 --- a/config/initializers/alaveteli_features.rb +++ b/config/initializers/alaveteli_features.rb @@ -14,3 +14,11 @@ else AlaveteliFeatures.backend.disable(:annotations) end + +# AlaveteliPro +# We enable alaveteli_pro globally based on the ENABLE_ALAVETELI_PRO config +if AlaveteliConfiguration.enable_alaveteli_pro + AlaveteliFeatures.backend.enable(:alaveteli_pro) +else + AlaveteliFeatures.backend.disable(:alaveteli_pro) +end diff --git a/lib/configuration.rb b/lib/configuration.rb index 2b2b7ff92c..62dbd37f32 100644 --- a/lib/configuration.rb +++ b/lib/configuration.rb @@ -38,6 +38,7 @@ module AlaveteliConfiguration :ENABLE_ANTI_SPAM => false, :ENABLE_TWO_FACTOR_AUTH => false, :ENABLE_WIDGETS => false, + :ENABLE_ALAVETELI_PRO => false, :EXCEPTION_NOTIFICATIONS_FROM => 'errors@localhost', :EXCEPTION_NOTIFICATIONS_TO => 'user-support@localhost', :FORCE_REGISTRATION_ON_NEW_REQUEST => false, From 0e8936528d2c3ace34650d5876744b8d5396bab3 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 17 Nov 2016 14:49:54 +0000 Subject: [PATCH 038/311] Raise ActiveRecord::RecordNotFound for embargoed requests. This applies to the `show` action and should cause it to behave just like a request for a non-existent url_title. --- app/controllers/request_controller.rb | 10 ++++++++++ spec/controllers/request_controller_spec.rb | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 8382e49fe2..2c09896b02 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -829,6 +829,16 @@ def download_entire_request private + def render_hidden(template='request/hidden', opts = {}) + # An embargoed is totally hidden - no indication that anything exists there + # to see + if @info_request && @info_request.embargo + raise ActiveRecord::RecordNotFound + else + return super(template, opts) + end + end + def info_request_params(batch = false) if batch unless params[:info_request].nil? || params[:info_request].empty? diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index a765666175..6b4a9ee36d 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -239,6 +239,14 @@ end end + context 'when the request is embargoed' do + it 'raises ActiveRecord::RecordNotFound' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect{ get :show, :url_title => embargoed_request.url_title } + .to raise_error(ActiveRecord::RecordNotFound) + end + end + describe 'when the request is being viewed by an admin' do describe 'if the request is awaiting description' do @@ -2306,13 +2314,21 @@ def expect_redirect(status, redirect_path) }.to raise_error(ActiveRecord::RecordNotFound) end + it 'raises ActiveRecord::RecordNotFound if the request is embargoed' do + badger_request.create_embargo(:publish_at => Time.now + 3.days) + expect { + get :similar, :url_title => badger_request.url_title + }.to raise_error(ActiveRecord::RecordNotFound) + end + end describe RequestController, "when caching fragments" do it "should not fail with long filenames" do long_name = "blahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblah.txt" info_request = double(InfoRequest, :prominence => 'normal', - :is_public? => true) + :is_public? => true, + :embargo => nil) incoming_message = double(IncomingMessage, :info_request => info_request, :parse_raw_email! => true, :info_request_id => 132, From 1bf530d866153e2260ab2bf3a6e24b0b07ef55b8 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 17 Nov 2016 14:50:41 +0000 Subject: [PATCH 039/311] Add specs for details action. --- spec/controllers/request_controller_spec.rb | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 6b4a9ee36d..81a0c3346b 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2660,3 +2660,66 @@ def make_request end end end + +describe RequestController do + + describe 'GET details' do + + let(:info_request){ FactoryGirl.create(:info_request)} + + it 'renders the details template' do + get :details, :url_title => info_request.url_title + expect(response).to render_template('details') + end + + it 'assigns the info_request' do + get :details, :url_title => info_request.url_title + expect(assigns[:info_request]).to eq(info_request) + end + + it 'assigns columns' do + get :details, :url_title => info_request.url_title + expected_columns = ['id', + 'event_type', + 'created_at', + 'described_state', + 'last_described_at', + 'calculated_state' ] + expect(assigns[:columns]).to eq expected_columns + end + + context 'when the request is hidden' do + + before do + info_request.prominence = 'hidden' + info_request.save! + end + + it 'returns a 403' do + get :details, :url_title => info_request.url_title + expect(response.code).to eq("403") + end + + it 'shows the hidden request template' do + get :details, :url_title => info_request.url_title + expect(response).to render_template("request/hidden") + end + + end + + context 'when the request is embargoed' do + + before do + info_request.create_embargo(:publish_at => Time.now + 3.days) + end + + it 'raises an ActiveRecord::RecordNotFound error' do + expect{ get :details, :url_title => info_request.url_title } + .to raise_error(ActiveRecord::RecordNotFound) + end + end + + + end + +end From a3ec901986faf2686a1a2746637e4008f643af58 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 18 Nov 2016 16:58:45 +0000 Subject: [PATCH 040/311] Cleanup specs for describe_state Move away from fixtures and mocking lots to factories. --- spec/controllers/request_controller_spec.rb | 844 ++++++++++---------- 1 file changed, 438 insertions(+), 406 deletions(-) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 81a0c3346b..8e4c57559c 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -1525,537 +1525,569 @@ def expect_hidden(hidden_template) end +describe RequestController do + describe "POST describe_state" do + describe 'if the request is external' do + let(:external_request){ FactoryGirl.create(:external_request) } -describe RequestController, "when classifying an information request" do - - describe 'if the request is external' do - - before do - @external_request = info_requests(:external_request) - end - - it 'should redirect to the request page' do - post :describe_state, :id => @external_request.id - expect(response).to redirect_to(:action => 'show', - :controller => 'request', - :url_title => @external_request.url_title) - end - - end - - describe 'when the request is internal' do - - before(:each) do - @dog_request = info_requests(:fancy_dog_request) - allow(@dog_request).to receive(:is_old_unclassified?).and_return(false) - allow(InfoRequest).to receive(:find).and_return(@dog_request) - load_raw_emails_data - end + it 'should redirect to the request page' do + post :describe_state, :id => external_request.id + expect(response) + .to redirect_to(show_request_path(external_request.url_title)) + end - def post_status(status) - post :describe_state, :incoming_message => { :described_state => status }, - :id => @dog_request.id, - :last_info_request_event_id => @dog_request.last_event_id_needing_description end - it "should require login" do - post_status('rejected') - expect(response).to redirect_to(:controller => 'user', - :action => 'signin', - :token => get_last_post_redirect.token) - end + describe 'when the request is internal' do - it 'should ask whether the request is old and unclassified' do - session[:user_id] = users(:silly_name_user).id - expect(@dog_request).to receive(:is_old_unclassified?) - post_status('rejected') - end + let(:info_request){ FactoryGirl.create(:info_request) } - it "should not classify the request if logged in as the wrong user" do - session[:user_id] = users(:silly_name_user).id - post_status('rejected') - expect(response).to render_template('user/wrong_user') - end - - describe 'when the request is old and unclassified' do + def post_status(status, info_request) + post :describe_state, :incoming_message => { :described_state => status }, + :id => info_request.id, + :last_info_request_event_id => info_request.last_event_id_needing_description + end - before do - allow(@dog_request).to receive(:is_old_unclassified?).and_return(true) - mail_mock = double("mail") - allow(mail_mock).to receive(:deliver) - allow(RequestMailer).to receive(:old_unclassified_updated).and_return(mail_mock) + it "should require login" do + post_status('rejected', info_request) + expect(response).to redirect_to(:controller => 'user', + :action => 'signin', + :token => get_last_post_redirect.token) end - describe 'when the user is not logged in' do + it "should not classify the request if logged in as the wrong user" do + session[:user_id] = FactoryGirl.create(:user).id + post_status('rejected', info_request) + expect(response).to render_template('user/wrong_user') + end - it 'should require login' do - session[:user_id] = nil - post_status('rejected') - expect(response).to redirect_to(:controller => 'user', - :action => 'signin', - :token => get_last_post_redirect.token) - end + describe 'when the request is old and unclassified' do - end + let(:info_request){ FactoryGirl.create(:old_unclassified_request)} - describe 'when the user is logged in as a different user' do + describe 'when the user is not logged in' do - before do - @other_user = mock_model(User) - session[:user_id] = users(:silly_name_user).id - end + it 'should require login' do + session[:user_id] = nil + post_status('rejected', info_request) + expect(response) + .to redirect_to(signin_path(:token => get_last_post_redirect.token)) + end - it 'should classify the request' do - allow(@dog_request).to receive(:calculate_status).and_return('rejected') - expect(@dog_request).to receive(:set_described_state).with('rejected', users(:silly_name_user), nil) - post_status('rejected') end - it 'should log a status update event' do - expected_params = {:user_id => users(:silly_name_user).id, - :old_described_state => 'waiting_response', - :described_state => 'rejected'} - event = mock_model(InfoRequestEvent) - expect(@dog_request).to receive(:log_event).with("status_update", expected_params).and_return(event) - post_status('rejected') - end + describe 'when the user is logged in as a different user' do - it 'should send an email to the requester letting them know someone has updated the status of their request' do - expect(RequestMailer).to receive(:old_unclassified_updated) - post_status('rejected') - end + let(:other_user){ FactoryGirl.create(:user) } - it 'should redirect to the request page' do - post_status('rejected') - expect(response).to redirect_to(:action => 'show', :controller => 'request', :url_title => @dog_request.url_title) - end + before do + session[:user_id] = other_user + end - it 'should show a message thanking the user for a good deed' do - post_status('rejected') - expect(flash[:notice]).to eq('Thank you for updating this request!') - end + it 'should classify the request' do + post_status('rejected', info_request) + expect(info_request.reload.described_state).to eq('rejected') + end - context "playing the classification game" do - before :each do - session[:request_game] = true + it 'should log a status update event' do + expected_params = {:user_id => other_user.id, + :old_described_state => 'waiting_response', + :described_state => 'rejected'} + post_status('rejected', info_request) + last_event = info_request.reload.info_request_events.last + expect(last_event.params).to eq expected_params end - it "should continue the game after classifying a request" do - post_status("rejected") - expect(flash[:notice]).to match(/There are some more requests below for you to classify/) - expect(response).to redirect_to categorise_play_url + it 'should send an email to the requester letting them know someone + has updated the status of their request' do + post_status('rejected', info_request) + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(1) + expect(deliveries.first.subject) + .to match("Someone has updated the status of your request") end - end - it "should send a mail from the user who changed the state to requires_admin" do - post :describe_state, :incoming_message => - { :described_state => "requires_admin", - :message => "a message" }, - :id => @dog_request.id, - :incoming_message_id => - incoming_messages(:useless_incoming_message), - :last_info_request_event_id => - @dog_request.last_event_id_needing_description + it 'should redirect to the request page' do + post_status('rejected', info_request) + expect(response).to redirect_to(show_request_path(info_request.url_title)) + end - deliveries = ActionMailer::Base.deliveries - expect(deliveries.size).to eq(1) - mail = deliveries[0] - expect(mail.header['Reply-To'].to_s).to match(users(:silly_name_user).email) - end - end - end + it 'should show a message thanking the user for a good deed' do + post_status('rejected', info_request) + expect(flash[:notice]).to eq('Thank you for updating this request!') + end - describe 'when logged in as an admin user who is not the actual requester' do + context "playing the classification game" do + before :each do + session[:request_game] = true + end - before do - @admin_user = users(:admin_user) - session[:user_id] = @admin_user.id - @dog_request = info_requests(:fancy_dog_request) - allow(InfoRequest).to receive(:find).and_return(@dog_request) - allow(@dog_request).to receive(:each).and_return([@dog_request]) - end + it "should continue the game after classifying a request" do + post_status("rejected", info_request) + expect(flash[:notice]).to match(/There are some more requests below for you to classify/) + expect(response).to redirect_to categorise_play_url + end + end - it 'should update the status of the request' do - allow(@dog_request).to receive(:calculate_status).and_return('rejected') - expect(@dog_request).to receive(:set_described_state).with('rejected', @admin_user, nil) - post_status('rejected') + context 'when the new status is "requires_admin"' do + it "should send a mail to admins saying that the response requires admin + and one to the requester noting the status change" do + post :describe_state, :incoming_message => + { :described_state => "requires_admin", + :message => "a message" }, + :id => info_request.id, + :incoming_message_id => + info_request.incoming_messages.last, + :last_info_request_event_id => + info_request.last_event_id_needing_description + + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(2) + requires_admin_mail = deliveries.first + status_update_mail = deliveries.second + expect(requires_admin_mail.subject) + .to match(/FOI response requires admin/) + expect(requires_admin_mail.to) + .to match([AlaveteliConfiguration::contact_email]) + expect(status_update_mail.subject) + .to match('Someone has updated the status of your request') + expect(status_update_mail.to) + .to match([info_request.user.email]) + end + end + end end - it 'should log a status update event' do - event = mock_model(InfoRequestEvent) - expected_params = {:user_id => @admin_user.id, - :old_described_state => 'waiting_response', - :described_state => 'rejected'} - expect(@dog_request).to receive(:log_event).with("status_update", expected_params).and_return(event) - post_status('rejected') - end + describe 'when logged in as an admin user who is not the actual requester' do - it 'should record a classification' do - event = mock_model(InfoRequestEvent) - allow(@dog_request).to receive(:log_event).with("status_update", anything).and_return(event) - expect(RequestClassification).to receive(:create!).with(:user_id => @admin_user.id, - :info_request_event_id => event.id) - post_status('rejected') - end + let(:admin_user){ FactoryGirl.create(:admin_user) } + let(:info_request){ FactoryGirl.create(:info_request) } - it 'should send an email to the requester letting them know someone has updated the status of their request' do - mail_mock = double("mail") - allow(mail_mock).to receive :deliver - expect(RequestMailer).to receive(:old_unclassified_updated).and_return(mail_mock) - post_status('rejected') - end + before do + session[:user_id] = admin_user.id + end - it 'should redirect to the request page' do - post_status('rejected') - expect(response).to redirect_to(:action => 'show', :controller => 'request', :url_title => @dog_request.url_title) - end + it 'should update the status of the request' do + post_status('rejected', info_request) + expect(info_request.reload.described_state).to eq('rejected') + end - it 'should show a message thanking the user for a good deed' do - post_status('rejected') - expect(flash[:notice]).to eq('Thank you for updating this request!') - end - end + it 'should log a status update event' do + expected_params = {:user_id => admin_user.id, + :old_described_state => 'waiting_response', + :described_state => 'rejected'} + post_status('rejected', info_request) + last_event = info_request.reload.info_request_events.last + expect(last_event.params).to eq expected_params + end - describe 'when logged in as an admin user who is also the actual requester' do + it 'should record a classification' do + post_status('rejected', info_request) + last_event = info_request.reload.info_request_events.last + classification = RequestClassification.order('created_at DESC').last + expect(classification.user_id).to eq(admin_user.id) + expect(classification.info_request_event).to eq(last_event) + end - render_views + it 'should send an email to the requester letting them know someone has + updated the status of their request' do + mail_mock = double("mail") + allow(mail_mock).to receive :deliver + expect(RequestMailer).to receive(:old_unclassified_updated).and_return(mail_mock) + post_status('rejected', info_request) + end - before do - @admin_user = users(:admin_user) - session[:user_id] = @admin_user.id - @dog_request = info_requests(:fancy_dog_request) - @dog_request.user = @admin_user - @dog_request.save! - allow(InfoRequest).to receive(:find).and_return(@dog_request) - allow(@dog_request).to receive(:each).and_return([@dog_request]) - end + it 'should redirect to the request page' do + post_status('rejected', info_request) + expect(response) + .to redirect_to(show_request_path(info_request.url_title)) + end - it 'should update the status of the request' do - allow(@dog_request).to receive(:calculate_status).and_return('rejected') - expect(@dog_request).to receive(:set_described_state).with('rejected', @admin_user, nil) - post_status('rejected') + it 'should show a message thanking the user for a good deed' do + post_status('rejected', info_request) + expect(flash[:notice]).to eq('Thank you for updating this request!') + end end - it 'should log a status update event' do - expect(@dog_request).to receive(:log_event) - post_status('rejected') - end + describe 'when logged in as an admin user who is also the actual requester' do - it 'should not send an email to the requester letting them know someone has updated the status of their request' do - expect(RequestMailer).not_to receive(:old_unclassified_updated) - post_status('rejected') - end + let(:admin_user){ FactoryGirl.create(:admin_user) } + let(:info_request){ FactoryGirl.create(:info_request, :user => admin_user) } - it 'should say it is showing advice as to what to do next' do - post_status('rejected') - expect(flash[:notice]).to match(/Here is what to do now/) - end + before do + session[:user_id] = admin_user.id + end - it 'should redirect to the unhappy page' do - post_status('rejected') - expect(response).to redirect_to(:controller => 'help', :action => 'unhappy', :url_title => @dog_request.url_title) - end + it 'should update the status of the request' do + post_status('rejected', info_request) + expect(info_request.reload.described_state).to eq('rejected') + end - end + it 'should log a status update event' do + expected_params = { :user_id => admin_user.id, + :old_described_state => 'waiting_response', + :described_state => 'rejected' } + post_status('rejected', info_request) + last_event = info_request.reload.info_request_events.last + expect(last_event.params).to eq expected_params + end - describe 'when logged in as the requestor' do + it 'should not send an email to the requester letting them know someone + has updated the status of their request' do + expect(RequestMailer).not_to receive(:old_unclassified_updated) + post_status('rejected', info_request) + end - render_views + it 'should show advice for the new state' do + expect(controller) + .to receive(:render_to_string) + .with(:partial => 'request/describe_notices/rejected', + :locals => {:info_request => info_request}) + .and_return('') + post_status('rejected', info_request) + end - before do - @request_owner = users(:bob_smith_user) - session[:user_id] = @request_owner.id - expect(@dog_request.awaiting_description).to eq(true) - allow(@dog_request).to receive(:each).and_return([@dog_request]) - end + it 'should redirect to the unhappy page' do + post_status('rejected', info_request) + expect(response) + .to redirect_to(help_unhappy_path(info_request.url_title)) + end - it "should let you know when you forget to select a status" do - post :describe_state, :id => @dog_request.id, - :last_info_request_event_id => @dog_request.last_event_id_needing_description - expect(response).to redirect_to show_request_url(:url_title => @dog_request.url_title) - expect(flash[:error]).to eq(_("Please choose whether or not you got some of the information that you wanted.")) end - it "should not change the status if the request has changed while viewing it" do - allow(@dog_request).to receive(:last_event_id_needing_description).and_return(2) + describe 'when logged in as the requestor' do - post :describe_state, :incoming_message => { :described_state => "rejected" }, - :id => @dog_request.id, :last_info_request_event_id => 1 - expect(response).to redirect_to show_request_url(:url_title => @dog_request.url_title) - expect(flash[:error]).to match(/The request has been updated since you originally loaded this page/) - end + let(:info_request) do + FactoryGirl.create(:info_request, :awaiting_description => true) + end - it "should successfully classify response if logged in as user controlling request" do - post_status('rejected') - expect(response).to redirect_to(:controller => 'help', :action => 'unhappy', :url_title => @dog_request.url_title) - @dog_request.reload - expect(@dog_request.awaiting_description).to eq(false) - expect(@dog_request.described_state).to eq('rejected') - expect(@dog_request.get_last_public_response_event).to eq(info_request_events(:useless_incoming_message_event)) - expect(@dog_request.info_request_events.last.event_type).to eq("status_update") - expect(@dog_request.info_request_events.last.calculated_state).to eq('rejected') - end + before do + session[:user_id] = info_request.user_id + end - it 'should log a status update event' do - expect(@dog_request).to receive(:log_event) - post_status('rejected') - end + it "should let you know when you forget to select a status" do + post :describe_state, :id => info_request.id, + :last_info_request_event_id => + info_request.last_event_id_needing_description + expect(response).to redirect_to show_request_url(:url_title => info_request.url_title) + expect(flash[:error]) + .to eq("Please choose whether or not you got some of the information that you wanted.") + end - it 'should not send an email to the requester letting them know someone has updated the status of their request' do - expect(RequestMailer).not_to receive(:old_unclassified_updated) - post_status('rejected') - end + it "should not change the status if the request has changed while viewing it" do + post :describe_state, :incoming_message => { :described_state => "rejected" }, + :id => info_request.id, + :last_info_request_event_id => 1 + expect(response) + .to redirect_to show_request_url(:url_title => info_request.url_title) + expect(flash[:error]) + .to match(/The request has been updated since you originally loaded this page/) + end - it "should go to the page asking for more information when classified as requires_admin" do - post :describe_state, :incoming_message => { :described_state => "requires_admin" }, :id => @dog_request.id, :incoming_message_id => incoming_messages(:useless_incoming_message), :last_info_request_event_id => @dog_request.last_event_id_needing_description - expect(response).to redirect_to describe_state_message_url(:url_title => @dog_request.url_title, :described_state => "requires_admin") + it "should successfully classify response" do + post_status('rejected', info_request) + expect(response) + .to redirect_to(help_unhappy_path(info_request.url_title)) + info_request.reload + expect(info_request.awaiting_description).to eq(false) + expect(info_request.described_state).to eq('rejected') + expect(info_request.info_request_events.last.event_type).to eq("status_update") + expect(info_request.info_request_events.last.calculated_state).to eq('rejected') + end - @dog_request.reload - expect(@dog_request.described_state).not_to eq('requires_admin') + it 'should log a status update event' do + expected_params = {:user_id => info_request.user_id, + :old_described_state => 'waiting_response', + :described_state => 'rejected'} + post_status('rejected', info_request) + last_event = info_request.reload.info_request_events.last + expect(last_event.params).to eq expected_params + end - expect(ActionMailer::Base.deliveries).to be_empty - end + it 'should not send an email to the requester letting them know someone + has updated the status of their request' do + expect(RequestMailer).not_to receive(:old_unclassified_updated) + post_status('rejected', info_request) + end - context "message is included when classifying as requires_admin" do - it "should send an email including the message" do + it "should go to the page asking for more information when classified + as requires_admin" do post :describe_state, - :incoming_message => { - :described_state => "requires_admin", - :message => "Something weird happened" }, - :id => @dog_request.id, - :last_info_request_event_id => @dog_request.last_event_id_needing_description + :incoming_message => { :described_state => "requires_admin" }, + :id => info_request.id, + :incoming_message_id => info_request.incoming_messages.last, + :last_info_request_event_id => info_request.last_event_id_needing_description + expect(response) + .to redirect_to describe_state_message_url(:url_title => info_request.url_title, + :described_state => "requires_admin") - deliveries = ActionMailer::Base.deliveries - expect(deliveries.size).to eq(1) - mail = deliveries[0] - expect(mail.body).to match(/as needing admin/) - expect(mail.body).to match(/Something weird happened/) + info_request.reload + expect(info_request.described_state).not_to eq('requires_admin') + expect(ActionMailer::Base.deliveries).to be_empty end - end + context "message is included when classifying as requires_admin" do + it "should send an email including the message" do + post :describe_state, + :incoming_message => { + :described_state => "requires_admin", + :message => "Something weird happened" }, + :id => info_request.id, + :last_info_request_event_id => info_request.last_event_id_needing_description + + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(1) + mail = deliveries[0] + expect(mail.body).to match(/as needing admin/) + expect(mail.body).to match(/Something weird happened/) + end + end - it 'should say it is showing advice as to what to do next' do - post_status('rejected') - expect(flash[:notice]).to match(/Here is what to do now/) - end + it 'should show advice for the new state' do + expect(controller) + .to receive(:render_to_string) + .with(:partial => 'request/describe_notices/rejected', + :locals => {:info_request => info_request}) + .and_return('') + post_status('rejected', info_request) + end - it 'should redirect to the unhappy page' do - post_status('rejected') - expect(response).to redirect_to(:controller => 'help', :action => 'unhappy', :url_title => @dog_request.url_title) - end + it 'should redirect to the unhappy page' do + post_status('rejected', info_request) + expect(response).to redirect_to(help_unhappy_path(info_request.url_title)) + end - it "knows about extended states" do - InfoRequest.send(:require, File.expand_path(File.join(File.dirname(__FILE__), '..', 'models', 'customstates'))) - InfoRequest.send(:include, InfoRequestCustomStates) - InfoRequest.class_eval('@@custom_states_loaded = true') - RequestController.send(:require, File.expand_path(File.join(File.dirname(__FILE__), '..', 'models', 'customstates'))) - RequestController.send(:include, RequestControllerCustomStates) - RequestController.class_eval('@@custom_states_loaded = true') - allow(Time).to receive(:now).and_return(Time.utc(2007, 11, 10, 00, 01)) - post_status('deadline_extended') - expect(flash[:notice]).to eq('Authority has requested extension of the deadline.') + it "knows about extended states" do + InfoRequest.send(:require, File.expand_path(File.join(File.dirname(__FILE__), '..', 'models', 'customstates'))) + InfoRequest.send(:include, InfoRequestCustomStates) + InfoRequest.class_eval('@@custom_states_loaded = true') + RequestController.send(:require, File.expand_path(File.join(File.dirname(__FILE__), '..', 'models', 'customstates'))) + RequestController.send(:include, RequestControllerCustomStates) + RequestController.class_eval('@@custom_states_loaded = true') + allow(Time).to receive(:now).and_return(Time.utc(2007, 11, 10, 00, 01)) + post_status('deadline_extended', info_request) + expect(flash[:notice]).to eq('Authority has requested extension of the deadline.') + end end - end - describe 'after a successful status update by the request owner' do + describe 'after a successful status update by the request owner' do - render_views + render_views - before do - @request_owner = users(:bob_smith_user) - session[:user_id] = @request_owner.id - @dog_request = info_requests(:fancy_dog_request) - allow(@dog_request).to receive(:each).and_return([@dog_request]) - allow(InfoRequest).to receive(:find).and_return(@dog_request) - end + let(:info_request){ FactoryGirl.create(:info_request) } - def request_url - "request/#{@dog_request.url_title}" - end + before do + session[:user_id] = info_request.user_id + end - def unhappy_url - "help/unhappy/#{@dog_request.url_title}" - end + def expect_redirect(status, redirect_path) + post_status(status, info_request) + expect(response). + to redirect_to("http://" + "test.host/#{redirect_path}".squeeze("/")) + end - def expect_redirect(status, redirect_path) - post_status(status) - expect(response). - to redirect_to("http://" + "test.host/#{redirect_path}".squeeze("/")) - end + context 'when status is updated to "waiting_response"' do - context 'when status is updated to "waiting_response"' do + it 'should redirect to the "request url" with a message in the right tense when + the response is not overdue' do + expect_redirect("waiting_response", + show_request_path(info_request.url_title)) + expect(flash[:notice]).to match(/should get a response/) + end - it 'should redirect to the "request url" with a message in the right tense when - the response is not overdue' do - allow(@dog_request).to receive(:date_response_required_by).and_return(Time.now.to_date+1) - allow(@dog_request).to receive(:date_very_overdue_after).and_return(Time.now.to_date+40) + it 'should redirect to the "request url" with a message in the right tense when + the response is overdue' do + # Create the request with today's date + info_request + time_travel_to(1.month.from_now) do + expect_redirect('waiting_response', + show_request_path(info_request.url_title)) + expect(flash[:notice]).to match(/should have got a response/) + end + end - expect_redirect("waiting_response", "request/#{@dog_request.url_title}") - expect(flash[:notice]).to match(/should get a response/) + it 'should redirect to the "request url" with a message in the right tense when + response is very overdue' do + # Create the request with today's date + info_request + time_travel_to(2.month.from_now) do + expect_redirect('waiting_response', + help_unhappy_path(info_request.url_title)) + expect(flash[:notice]).to match(/is long overdue/) + expect(flash[:notice]).to match(/by more than 40 working days/) + expect(flash[:notice]).to match(/within 20 working days/) + end + end end - it 'should redirect to the "request url" with a message in the right tense when - the response is overdue' do - allow(@dog_request).to receive(:date_response_required_by).and_return(Time.now.to_date-1) - allow(@dog_request).to receive(:date_very_overdue_after).and_return(Time.now.to_date+40) - expect_redirect('waiting_response', request_url) - expect(flash[:notice]).to match(/should have got a response/) - end + context 'when status is updated to "not held"' do + + it 'should redirect to the "request url"' do + expect_redirect('not_held', + show_request_path(info_request.url_title)) + end - it 'should redirect to the "request url" with a message in the right tense when - the response is overdue' do - allow(@dog_request).to receive(:date_response_required_by).and_return(Time.now.to_date-2) - allow(@dog_request).to receive(:date_very_overdue_after).and_return(Time.now.to_date-1) - expect_redirect('waiting_response', unhappy_url) - expect(flash[:notice]).to match(/is long overdue/) - expect(flash[:notice]).to match(/by more than 40 working days/) - expect(flash[:notice]).to match(/within 20 working days/) end - end - context 'when status is updated to "not held"' do + context 'when status is updated to "successful"' do - it 'should redirect to the "request url"' do - expect_redirect('not_held', request_url) - end + it 'should redirect to the "request url"' do + expect_redirect('successful', + show_request_path(info_request.url_title)) + end - end + it 'should show a message including the donation url if there is one' do + allow(AlaveteliConfiguration).to receive(:donation_url).and_return('http://donations.example.com') + post_status('successful', info_request) + expect(flash[:notice]).to match('make a donation') + expect(flash[:notice]).to match('http://donations.example.com') + end - context 'when status is updated to "successful"' do + it 'should show a message without reference to donations if there is no + donation url' do + allow(AlaveteliConfiguration).to receive(:donation_url).and_return('') + post_status('successful', info_request) + expect(flash[:notice]).not_to match('make a donation') + end - it 'should redirect to the "request url"' do - expect_redirect('successful', request_url) end - it 'should show a message including the donation url if there is one' do - allow(AlaveteliConfiguration).to receive(:donation_url).and_return('http://donations.example.com') - post_status('successful') - expect(flash[:notice]).to match('make a donation') - expect(flash[:notice]).to match('http://donations.example.com') - end + context 'when status is updated to "waiting clarification"' do - it 'should show a message without reference to donations if there is no - donation url' do - allow(AlaveteliConfiguration).to receive(:donation_url).and_return('') - post_status('successful') - expect(flash[:notice]).not_to match('make a donation') - end + context 'when there is a last response' do - end + let(:info_request){ FactoryGirl.create(:info_request_with_incoming) } - context 'when status is updated to "waiting clarification"' do + it 'should redirect to the "response url"' do + session[:user_id] = info_request.user_id + expected_url = new_request_incoming_followup_path( + :request_id => info_request.id, + :incoming_message_id => + info_request.get_last_public_response.id) + expect_redirect('waiting_clarification', expected_url) + end + end - it 'should redirect to the "response url" when there is a last response' do - incoming_message = mock_model(IncomingMessage) - allow(@dog_request).to receive(:get_last_public_response).and_return(incoming_message) - expected_url = new_request_incoming_followup_path( - :request_id => @dog_request.id, - :incoming_message_id => @dog_request.get_last_public_response.id) - expect_redirect('waiting_clarification', expected_url) - end + context 'when there are no events needing description' do + it 'should redirect to the "followup no incoming url"' do + expected_url = new_request_followup_path( + :request_id => info_request.id, + :incoming_message_id => nil) + expect_redirect('waiting_clarification', expected_url) + end + end - it 'should redirect to the "followup no incoming url" when there are no events - needing description' do - allow(@dog_request).to receive(:get_last_public_response).and_return(nil) - expected_url = new_request_followup_path( - :request_id => @dog_request.id, - :incoming_message_id => nil) - expect_redirect('waiting_clarification', expected_url) end - end + context 'when status is updated to "rejected"' do - context 'when status is updated to "rejected"' do + it 'should redirect to the "unhappy url"' do + expect_redirect('rejected', help_unhappy_path(info_request.url_title)) + end - it 'should redirect to the "unhappy url"' do - expect_redirect('rejected', "help/unhappy/#{@dog_request.url_title}") end - end + context 'when status is updated to "partially successful"' do - context 'when status is updated to "partially successful"' do + it 'should redirect to the "unhappy url"' do + expect_redirect('partially_successful', + help_unhappy_path(info_request.url_title)) + end - it 'should redirect to the "unhappy url"' do - expect_redirect('partially_successful', "help/unhappy/#{@dog_request.url_title}") - end + it 'should show a message including the donation url if there is one' do + allow(AlaveteliConfiguration).to receive(:donation_url).and_return('http://donations.example.com') + post_status('successful', info_request) + expect(flash[:notice]).to match('make a donation') + expect(flash[:notice]).to match('http://donations.example.com') + end - it 'should show a message including the donation url if there is one' do - allow(AlaveteliConfiguration).to receive(:donation_url).and_return('http://donations.example.com') - post_status('successful') - expect(flash[:notice]).to match('make a donation') - expect(flash[:notice]).to match('http://donations.example.com') - end + it 'should show a message without reference to donations if there is no + donation url' do + allow(AlaveteliConfiguration).to receive(:donation_url).and_return('') + post_status('successful', info_request) + expect(flash[:notice]).not_to match('make a donation') + end - it 'should show a message without reference to donations if there is no - donation url' do - allow(AlaveteliConfiguration).to receive(:donation_url).and_return('') - post_status('successful') - expect(flash[:notice]).not_to match('make a donation') end - end + context 'when status is updated to "gone postal"' do + + let(:info_request){ FactoryGirl.create(:info_request_with_incoming) } - context 'when status is updated to "gone postal"' do + it 'should redirect to the "respond to last" url' do + session[:user_id] = info_request.user_id + expected_url = new_request_incoming_followup_path( + :request_id => info_request.id, + :incoming_message_id => + info_request.get_last_public_response.id, + :gone_postal => 1) + expect_redirect('gone_postal', expected_url) + end - it 'should redirect to the "respond to last url"' do - expected_url = new_request_incoming_followup_path( - :request_id => @dog_request.id, - :incoming_message_id => @dog_request.get_last_public_response.id) - expect_redirect('gone_postal', "#{expected_url}?gone_postal=1") end - end + context 'when status updated to "internal review"' do - context 'when status updated to "internal review"' do + it 'should redirect to the "request url"' do + expect_redirect('internal_review', + show_request_path(info_request.url_title)) + end - it 'should redirect to the "request url"' do - expect_redirect('internal_review', request_url) end - end - - context 'when status is updated to "requires admin"' do + context 'when status is updated to "requires admin"' do + + it 'should redirect to the "request url"' do + post :describe_state, :incoming_message => { + :described_state => 'requires_admin', + :message => "A message" + }, + :id => info_request.id, + :last_info_request_event_id => + info_request.last_event_id_needing_description + expect(response) + .to redirect_to show_request_url(:url_title => info_request.url_title) + end - it 'should redirect to the "request url"' do - post :describe_state, :incoming_message => { - :described_state => 'requires_admin', - :message => "A message" }, - :id => @dog_request.id, - :last_info_request_event_id => @dog_request.last_event_id_needing_description - expect(response).to redirect_to show_request_url(:url_title => @dog_request.url_title) end - end - - context 'when status is updated to "error message"' do + context 'when status is updated to "error message"' do + + it 'should redirect to the "request url"' do + post :describe_state, :incoming_message => { + :described_state => 'error_message', + :message => "A message" + }, + :id => info_request.id, + :last_info_request_event_id => + info_request.last_event_id_needing_description + expect(response) + .to redirect_to( + show_request_url(:url_title => info_request.url_title) + ) + end - it 'should redirect to the "request url"' do - post :describe_state, :incoming_message => { - :described_state => 'error_message', - :message => "A message" }, - :id => @dog_request.id, - :last_info_request_event_id => @dog_request.last_event_id_needing_description - expect(response).to redirect_to show_request_url(:url_title => @dog_request.url_title) end - end + context 'when status is updated to "user_withdrawn"' do - context 'when status is updated to "user_withdrawn"' do + let(:info_request){ FactoryGirl.create(:info_request_with_incoming) } + + it 'should redirect to the "respond to last" url' do + session[:user_id] = info_request.user_id + expected_url = new_request_incoming_followup_path( + :request_id => info_request.id, + :incoming_message_id => + info_request.get_last_public_response.id) + expect_redirect('user_withdrawn', expected_url) + end - it 'should redirect to the "respond to last url url" ' do - expected_url = new_request_incoming_followup_path( - :request_id => @dog_request.id, - :incoming_message_id => @dog_request.get_last_public_response.id) - expect_redirect('user_withdrawn', expected_url) end end - end - end - end describe RequestController, "when viewing comments" do From 788f017f12f4f1459602a30f46f827305349351d Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 18 Nov 2016 17:24:33 +0000 Subject: [PATCH 041/311] Handle case of embargoed request when describing state. Any post to the `describe_state` action for an embargoed request should return a 404 not found response as we are now looking for the request within the scope of those that are not embargoed. Add select statement to scope so that records returned by the scope aren't read only. http://stackoverflow.com/questions/7667052/proper-way-to-prevent-activerecordreadonlyrecord --- app/controllers/request_controller.rb | 2 +- .../info_request/prominence/not_embargoed_query.rb | 8 +++++--- spec/controllers/request_controller_spec.rb | 10 ++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 2c09896b02..f64ee05084 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -443,7 +443,7 @@ def new # Submitted to the describing state of messages form def describe_state - info_request = InfoRequest.find(params[:id].to_i) + info_request = InfoRequest.not_embargoed.find(params[:id].to_i) set_last_request(info_request) # If this is an external request, go to the request page - we don't allow diff --git a/app/models/info_request/prominence/not_embargoed_query.rb b/app/models/info_request/prominence/not_embargoed_query.rb index 9623ddd234..38844e9b10 100644 --- a/app/models/info_request/prominence/not_embargoed_query.rb +++ b/app/models/info_request/prominence/not_embargoed_query.rb @@ -10,9 +10,11 @@ def initialize(relation = InfoRequest) def call # Specify an outer join as the default inner join # will not retrieve NULL records which is what we want here - @relation.joins('LEFT OUTER JOIN embargoes - ON embargoes.info_request_id = info_requests.id') - .where('embargoes.id IS NULL') + @relation + .select("info_requests.*") + .joins('LEFT OUTER JOIN embargoes + ON embargoes.info_request_id = info_requests.id') + .where('embargoes.id IS NULL') end end end diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 8e4c57559c..a80be091d1 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -1549,6 +1549,16 @@ def post_status(status, info_request) :last_info_request_event_id => info_request.last_event_id_needing_description end + context 'when the request is embargoed' do + + let(:info_request){ FactoryGirl.create(:embargoed_request) } + + it 'should raise ActiveRecord::NotFound' do + expect{ post_status('rejected', info_request) } + .to raise_error ActiveRecord::RecordNotFound + end + end + it "should require login" do post_status('rejected', info_request) expect(response).to redirect_to(:controller => 'user', From 8ed25cea72cfb1857f2019cd59359fdbdc08c0fb Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 18 Nov 2016 17:24:55 +0000 Subject: [PATCH 042/311] Line length fixes. --- app/controllers/request_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index f64ee05084..071c1ff3f4 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -474,7 +474,8 @@ def describe_state end if params[:last_info_request_event_id].to_i != info_request.last_event_id_needing_description - flash[:error] = _("The request has been updated since you originally loaded this page. Please check for any new incoming messages below, and try again.") + flash[:error] = _("The request has been updated since you originally loaded this page. " \ + "Please check for any new incoming messages below, and try again.") redirect_to request_url(info_request) return end @@ -485,7 +486,8 @@ def describe_state # the administrators. # If this message hasn't been included then ask for it if ["error_message", "requires_admin"].include?(described_state) && message.nil? - redirect_to describe_state_message_url(:url_title => info_request.url_title, :described_state => described_state) + redirect_to describe_state_message_url(:url_title => info_request.url_title, + :described_state => described_state) return end From d75578b5e4d5806d0648ef3bd9e16632e4664f64 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 18 Nov 2016 17:44:49 +0000 Subject: [PATCH 043/311] Specs for describe_state_message action. --- spec/controllers/request_controller_spec.rb | 68 +++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index a80be091d1..1521c4579d 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -1665,6 +1665,24 @@ def post_status(status, info_request) expect(status_update_mail.to) .to match([info_request.user.email]) end + + context "if the params don't include a message" do + + it 'redirects to the message url' do + post :describe_state, :incoming_message => + { :described_state => "requires_admin" }, + :id => info_request.id, + :incoming_message_id => + info_request.incoming_messages.last, + :last_info_request_event_id => + info_request.last_event_id_needing_description + expected_url = describe_state_message_url( + :url_title => info_request.url_title, + :described_state => 'requires_admin') + expect(response).to redirect_to(expected_url) + end + + end end end end @@ -2078,6 +2096,24 @@ def expect_redirect(status, redirect_path) ) end + context "if the params don't include a message" do + + it 'redirects to the message url' do + post :describe_state, :incoming_message => + { :described_state => "error_message" }, + :id => info_request.id, + :incoming_message_id => + info_request.incoming_messages.last, + :last_info_request_event_id => + info_request.last_event_id_needing_description + expected_url = describe_state_message_url( + :url_title => info_request.url_title, + :described_state => 'error_message') + expect(response).to redirect_to(expected_url) + end + + end + end context 'when status is updated to "user_withdrawn"' do @@ -2765,3 +2801,35 @@ def make_request end end + +describe RequestController do + describe 'GET describe_state_message' do + let(:info_request){ FactoryGirl.create(:info_request_with_incoming) } + + it 'assigns the info_request to the view' do + get :describe_state_message, :url_title => info_request.url_title, + :described_state => 'error_message' + expect(assigns[:info_request]).to eq info_request + end + + it 'assigns the described state to the view' do + get :describe_state_message, :url_title => info_request.url_title, + :described_state => 'error_message' + expect(assigns[:described_state]).to eq 'error_message' + end + + it 'assigns the last info request event id to the view' do + get :describe_state_message, :url_title => info_request.url_title, + :described_state => 'error_message' + expect(assigns[:last_info_request_event_id]) + .to eq info_request.last_event_id_needing_description + end + + it 'assigns the title to the view' do + get :describe_state_message, :url_title => info_request.url_title, + :described_state => 'error_message' + expect(assigns[:title]).to eq "I've received an error message" + end + + end +end From 84de25104717b23944056ba80cd974953d0f7c88 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 18 Nov 2016 17:45:38 +0000 Subject: [PATCH 044/311] Handle embargo case in the `describe_state_message` action. This action is called when someone has tried to report a request to admins without giving an accompanying message. By scoping with the not_embargoed scope when finding the request, this action should now 404 for any embargoed request in production. --- app/controllers/request_controller.rb | 2 +- spec/controllers/request_controller_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 071c1ff3f4..2b4dee48a7 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -550,7 +550,7 @@ def describe_state # Collect a message to include with the change of state def describe_state_message - @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + @info_request = InfoRequest.not_embargoed.find_by_url_title!(params[:url_title]) @described_state = params[:described_state] @last_info_request_event_id = @info_request.last_event_id_needing_description @title = case @described_state diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 1521c4579d..af85ef95a5 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2831,5 +2831,17 @@ def make_request expect(assigns[:title]).to eq "I've received an error message" end + context 'when the request is embargoed' do + let(:info_request){ FactoryGirl.create(:embargoed_request) } + + it 'raises ActiveRecord::RecordNotFound' do + expect{ get :describe_state_message, + :url_title => info_request.url_title, + :described_state => 'error_message' } + .to raise_error(ActiveRecord::RecordNotFound) + + end + + end end end From 4bb8433320834b2bd97890405bd00483ef74957e Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 10:09:32 +0000 Subject: [PATCH 045/311] Amend scope for Comment.visible to exclude InfoRequest.is_public select The select on InfoRequest.is_public was added in 9e022ada so that the InfoRequest records returned would not be read-only (which they otherwise would be due to the use of :join rather than :include to get the outer join for non-embargoed requests). If carried over here, it results in the InfoRequest, not the Comment, being returned. --- app/models/comment.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 29682a6178..41422236d8 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -33,7 +33,9 @@ class Comment < ActiveRecord::Base :check_body_uses_mixed_capitals scope :visible, -> { - joins(:info_request).merge(InfoRequest.is_searchable).where(:visible => true) + joins(:info_request) + .merge(InfoRequest.is_searchable.except(:select)) + .where(:visible => true) } after_save :event_xapian_update From e4884dd7df3174e5ff8bab760ce9a4680e8c8477 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 10:50:24 +0000 Subject: [PATCH 046/311] Handle embargoed requests in followup actions. Any request that requires an info_request via the `set_info_request` filter should now raise an ActiveRecord::RecordNotFound error for an embargoed request, resulting in a 404 not found response in production. --- app/controllers/followups_controller.rb | 2 +- spec/controllers/followups_controller_spec.rb | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/controllers/followups_controller.rb b/app/controllers/followups_controller.rb index 8bcfc3ab0d..cc8e50a98d 100644 --- a/app/controllers/followups_controller.rb +++ b/app/controllers/followups_controller.rb @@ -165,7 +165,7 @@ def set_incoming_message end def set_info_request - @info_request = InfoRequest.find(params[:request_id].to_i) + @info_request = InfoRequest.not_embargoed.find(params[:request_id].to_i) end def set_last_request_data diff --git a/spec/controllers/followups_controller_spec.rb b/spec/controllers/followups_controller_spec.rb index bd85e06d03..8cfeacd013 100644 --- a/spec/controllers/followups_controller_spec.rb +++ b/spec/controllers/followups_controller_spec.rb @@ -10,6 +10,12 @@ describe "GET new" do + it 'raises an ActiveRecord::RecordNotFound error for an embargoed request' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect{ get :new, :request_id => embargoed_request.id } + .to raise_error(ActiveRecord::RecordNotFound) + end + it "displays 'wrong user' message when not logged in as the request owner" do session[:user_id] = FactoryGirl.create(:user) get :new, :request_id => request.id, @@ -127,6 +133,13 @@ :what_doing => 'normal_sort' } end + it 'raises an ActiveRecord::RecordNotFound error for an embargoed request' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect{ post :preview, :outgoing_message => dummy_message, + :request_id => embargoed_request.id } + .to raise_error(ActiveRecord::RecordNotFound) + end + it "redirects to the signin page if not logged in" do post :preview, :outgoing_message => dummy_message, :request_id => request.id, @@ -191,6 +204,13 @@ session[:user_id] = request_user.id end + it 'raises an ActiveRecord::RecordNotFound error for an embargoed request' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect{ post :create, :outgoing_message => dummy_message, + :request_id => embargoed_request.id } + .to raise_error(ActiveRecord::RecordNotFound) + end + it "redirects to the signin page if not logged in" do session[:user_id] = nil post :create, :outgoing_message => dummy_message, From 3e569aef269634556a57e282f82d262994df43bc Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 11:31:43 +0000 Subject: [PATCH 047/311] Handle embargo in the download action. Any request to download an embargoed request should result in an ActiveRecord::RecordNotFound error, returning a 404 not found response in production. --- app/controllers/request_controller.rb | 2 +- spec/controllers/request_controller_spec.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 2b4dee48a7..efc76b77c3 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -806,7 +806,7 @@ def search_typeahead def download_entire_request @locale = I18n.locale.to_s I18n.with_locale(@locale) do - @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + @info_request = InfoRequest.not_embargoed.find_by_url_title!(params[:url_title]) if authenticated?( :web => _("To download the zip file"), :email => _("Then you can download a zip file of {{info_request_title}}.", diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index af85ef95a5..4d96ee5609 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2845,3 +2845,17 @@ def make_request end end end +describe RequestController do + + describe 'GET download_entire_request' do + context 'when the request is embargoed' do + let(:info_request){ FactoryGirl.create(:embargoed_request) } + + it 'raises ActiveRecord::RecordNotFound' do + expect{ get :download_entire_request, + :url_title => info_request.url_title } + .to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end \ No newline at end of file From e390afbb07eba2228e3eb4bab41f7afaad1b4fe4 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 11:45:22 +0000 Subject: [PATCH 048/311] Remove unneeded 'render_views' --- spec/controllers/request_controller_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 4d96ee5609..86c2924b46 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2164,7 +2164,6 @@ def expect_redirect(status, redirect_path) describe RequestController, "authority uploads a response from the web interface" do - render_views before(:each) do # domain after the @ is used for authentication of FOI officers, so to test it From 26d380568940a36eb9f1469f304a3e4d657b1913 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 11:45:44 +0000 Subject: [PATCH 049/311] Handle embargo in response uploading. Scopes the finder for the upload_response action to non embargoed requests, so should result in ActiveRecord::RecordNotFound being raised for an embargoed request, and a 404 not found response being returned in production. --- app/controllers/request_controller.rb | 2 +- spec/controllers/request_controller_spec.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index efc76b77c3..34acb33ac1 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -733,7 +733,7 @@ def get_attachment_internal(html_conversion) def upload_response @locale = I18n.locale.to_s I18n.with_locale(@locale) do - @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + @info_request = InfoRequest.not_embargoed.find_by_url_title!(params[:url_title]) @reason_params = { :web => _("To upload a response, you must be logged in using an " \ diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 86c2924b46..5b808b2e4f 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2177,6 +2177,16 @@ def expect_redirect(status, redirect_path) @foi_officer_user.save! end + context 'when the request is embargoed' do + let(:embargoed_request){ FactoryGirl.create(:embargoed_request)} + + it 'raises an ActiveRecord::RecordNotFound error' do + expect{get :upload_response, :url_title => embargoed_request.url_title } + .to raise_error(ActiveRecord::RecordNotFound) + end + + end + it "should require login to view the form to upload" do @ir = info_requests(:fancy_dog_request) expect(@ir.public_body.is_foi_officer?(@normal_user)).to eq(false) From fa93786355d450287516c2f1bf2b9630ec69443f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 13:35:38 +0000 Subject: [PATCH 050/311] Add specs for show_request_event action. --- ...dmin_info_request_event_controller_spec.rb | 2 +- spec/controllers/request_controller_spec.rb | 57 ++++++++++++++++++- spec/factories/info_request_events.rb | 21 ++++++- spec/models/info_request_event_spec.rb | 6 +- spec/spec_helper.rb | 1 + .../event_digest.text.erb_spec.rb | 4 +- 6 files changed, 82 insertions(+), 9 deletions(-) diff --git a/spec/controllers/admin_info_request_event_controller_spec.rb b/spec/controllers/admin_info_request_event_controller_spec.rb index d7a5240aa5..a9e71b6ecd 100644 --- a/spec/controllers/admin_info_request_event_controller_spec.rb +++ b/spec/controllers/admin_info_request_event_controller_spec.rb @@ -8,7 +8,7 @@ describe 'when handling valid data' do before do - @info_request_event = FactoryGirl.create(:info_request_event) + @info_request_event = FactoryGirl.create(:response_event) put :update, :id => @info_request_event end diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 5b808b2e4f..9fa84a24c1 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2854,6 +2854,7 @@ def make_request end end end + describe RequestController do describe 'GET download_entire_request' do @@ -2867,4 +2868,58 @@ def make_request end end end -end \ No newline at end of file +end + +describe RequestController do + + describe 'GET show_request_event' do + + context 'when the event is an incoming message' do + let(:event){ FactoryGirl.create(:response_event) } + + it 'returns a 301 status' do + get :show_request_event, :info_request_event_id => event.id + expect(response.status).to eq(301) + end + + it 'redirects to the incoming message path' do + get :show_request_event, :info_request_event_id => event.id + expect(response) + .to redirect_to(incoming_message_path(event.incoming_message)) + end + + end + + context 'when the event is an outgoing message' do + let(:event){ FactoryGirl.create(:sent_event) } + + it 'returns a 301 status' do + get :show_request_event, :info_request_event_id => event.id + expect(response.status).to eq(301) + end + + it 'redirects to the outgoing message path' do + get :show_request_event, :info_request_event_id => event.id + expect(response) + .to redirect_to(outgoing_message_path(event.outgoing_message)) + end + end + + context 'for any other kind of event' do + let(:event){ FactoryGirl.create(:info_request_event) } + + it 'returns a 301 status' do + get :show_request_event, :info_request_event_id => event.id + expect(response.status).to eq(301) + end + + it 'redirects to the request path' do + get :show_request_event, :info_request_event_id => event.id + expect(response) + .to redirect_to(show_request_path(event.info_request.url_title)) + end + end + + end + +end diff --git a/spec/factories/info_request_events.rb b/spec/factories/info_request_events.rb index 5d01e26ef4..78e5e4ccc5 100644 --- a/spec/factories/info_request_events.rb +++ b/spec/factories/info_request_events.rb @@ -20,14 +20,33 @@ factory :info_request_event do info_request - event_type 'response' + event_type 'edit' params_yaml '' + factory :sent_event do event_type 'sent' + association :outgoing_message, :factory => :initial_request + end + + factory :response_event do + event_type 'response' + incoming_message + end + + factory :followup_sent_event do + event_type 'followup_sent' + association :outgoing_message, :factory => :new_information_followup end + + factory :comment_event do + event_type 'comment' + association :comment + end + factory :edit_event do event_type 'edit' end + factory :hide_event do event_type 'hide' end diff --git a/spec/models/info_request_event_spec.rb b/spec/models/info_request_event_spec.rb index 258760a7d5..def07a58fd 100644 --- a/spec/models/info_request_event_spec.rb +++ b/spec/models/info_request_event_spec.rb @@ -238,18 +238,16 @@ describe '#filetype' do context 'a response event' do - let(:ire) { ire = FactoryGirl.create(:info_request_event) } + let(:ire) { ire = FactoryGirl.create(:response_event) } it 'should raise an error if there is not incoming_message' do + ire.incoming_message = nil expect { ire.filetype }.to raise_error. with_message(/event type is 'response' but no incoming message for event/) end it 'should return a blank string if there are no attachments' do info_request = ire.info_request - incoming = FactoryGirl.create(:plain_incoming_message, - :info_request => info_request) - ire.incoming_message = incoming expect(ire.filetype).to eq('') end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9f904e3173..08b2cdbbe4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,6 +48,7 @@ config.include Capybara::DSL, :type => :request config.include Delorean + config.include LinkToHelper # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" diff --git a/spec/views/track_mailer/event_digest.text.erb_spec.rb b/spec/views/track_mailer/event_digest.text.erb_spec.rb index 597b103535..a54c0d52c5 100644 --- a/spec/views/track_mailer/event_digest.text.erb_spec.rb +++ b/spec/views/track_mailer/event_digest.text.erb_spec.rb @@ -22,7 +22,7 @@ describe "tracking a response" do let(:event) do - FactoryGirl.create(:info_request_event, + FactoryGirl.create(:response_event, :incoming_message => request.incoming_messages.last, :info_request => request) end @@ -51,7 +51,7 @@ describe "tracking a followup" do let(:event) do - FactoryGirl.create(:info_request_event, + FactoryGirl.create(:response_event, :outgoing_message => request.outgoing_messages.last, :info_request => request, :event_type => 'followup_sent') From aa177954452be12efcd104d53e8bb3ccf2ad8409 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 13:46:49 +0000 Subject: [PATCH 051/311] Handle embargo case in the show_request_event action. Check that the info request to which an event refers is not embargoed. If it is, raise ActiveRecord::RecordNotFound which should result in a 404 not found response in production. --- app/controllers/request_controller.rb | 3 +++ spec/controllers/request_controller_spec.rb | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 34acb33ac1..e939eefb9b 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -567,6 +567,9 @@ def describe_state_message # proper URL for the message the event refers to def show_request_event @info_request_event = InfoRequestEvent.find(params[:info_request_event_id]) + if @info_request_event.info_request.embargo + raise ActiveRecord::RecordNotFound + end if @info_request_event.is_incoming_message? redirect_to incoming_message_url(@info_request_event.incoming_message), :status => :moved_permanently elsif @info_request_event.is_outgoing_message? diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 9fa84a24c1..d7f452838a 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2888,6 +2888,11 @@ def make_request .to redirect_to(incoming_message_path(event.incoming_message)) end + it 'raises ActiveRecord::RecordNotFound when the request is embargoed' do + event.info_request.create_embargo(:publish_at => Time.now + 1.day) + expect{ get :show_request_event, :info_request_event_id => event.id } + .to raise_error ActiveRecord::RecordNotFound + end end context 'when the event is an outgoing message' do @@ -2903,6 +2908,12 @@ def make_request expect(response) .to redirect_to(outgoing_message_path(event.outgoing_message)) end + + it 'raises ActiveRecord::RecordNotFound when the request is embargoed' do + event.info_request.create_embargo(:publish_at => Time.now + 1.day) + expect{ get :show_request_event, :info_request_event_id => event.id } + .to raise_error ActiveRecord::RecordNotFound + end end context 'for any other kind of event' do @@ -2918,8 +2929,12 @@ def make_request expect(response) .to redirect_to(show_request_path(event.info_request.url_title)) end - end + it 'raises ActiveRecord::RecordNotFound when the request is embargoed' do + event.info_request.create_embargo(:publish_at => Time.now + 1.day) + expect{ get :show_request_event, :info_request_event_id => event.id } + .to raise_error ActiveRecord::RecordNotFound + end + end end - end From a7de5a7a356366c95e32b66a3e6b5f7f674a1bc6 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 21 Nov 2016 18:28:04 +0000 Subject: [PATCH 052/311] Cleanup specs for getting attachments. Move those that are testing the process of loading incoming messages into an integration spec. Rewrite the remainder using factories and separate them by controller action. --- spec/controllers/request_controller_spec.rb | 383 +++++++------------- spec/factories/info_requests.rb | 7 + spec/integration/incoming_mail_spec.rb | 147 ++++++++ 3 files changed, 280 insertions(+), 257 deletions(-) create mode 100644 spec/integration/incoming_mail_spec.rb diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index d7f452838a..73dbb52045 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -239,6 +239,24 @@ end end + it "should censor attachment names" do + info_request = FactoryGirl.create(:info_request_with_html_attachment) + get :show, :url_title => info_request.url_title + expect(response.body).to have_css('.attachment .attachment__name') do |s| + expect(s).to contain /interesting.pdf/m + end + # Note that the censor rule applies to the original filename, + # not the display_filename: + info_request.censor_rules.create!(:text => 'interesting.pdf', + :replacement => "Mouse.pdf", + :last_edit_editor => 'unknown', + :last_edit_comment => 'none') + get :show, :url_title => info_request.url_title + expect(response.body).to have_css('.attachment .attachment__name') do |s| + expect(s).to contain /Mouse.pdf/m + end + end + context 'when the request is embargoed' do it 'raises ActiveRecord::RecordNotFound' do embargoed_request = FactoryGirl.create(:embargoed_request) @@ -449,77 +467,25 @@ def make_request expect(response).to render_template "request/show" end end +end - describe 'when handling incoming mail' do - - render_views - - it "should receive incoming messages, send email to creator, and show them" do - ir = info_requests(:fancy_dog_request) - ir.incoming_messages.each { |x| x.parse_raw_email! } - - get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' - size_before = assigns[:info_request_events].size - - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email) - deliveries = ActionMailer::Base.deliveries - expect(deliveries.size).to eq(1) - mail = deliveries[0] - expect(mail.body).to match(/You have a new response to the Freedom of Information request/) - - get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' - expect(assigns[:info_request_events].size - size_before).to eq(1) - end - - it "should download attachments" do - ir = info_requests(:fancy_dog_request) - ir.incoming_messages.each { |x| x.parse_raw_email!(true) } - - get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' - expect(response.content_type).to eq("text/html") - size_before = assigns[:info_request_events].size - - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - - get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' - expect(assigns[:info_request_events].size - size_before).to eq(1) - ir.reload +describe RequestController do + describe 'GET get_attachment' do - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello world.txt', :skip_cache => 1 - expect(response.content_type).to eq("text/plain") - expect(response.body).to have_content "Second hello" + let(:info_request){ FactoryGirl.create(:info_request_with_incoming_attachments) } - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 3, :file_name => 'hello world.txt', :skip_cache => 1 - expect(response.content_type).to eq("text/plain") - expect(response.body).to have_content "First hello" + def get_attachment(params = {}) + default_params = { :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'interesting.pdf' } + get :get_attachment, default_params.merge(params) end it 'should cache an attachment on a request with normal prominence' do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload expect(@controller).to receive(:foi_fragment_cache_write) - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, - :id => ir.id, - :part => 2, - :file_name => 'hello world.txt' - end - - it "should convert message body to UTF8" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('iso8859_2_raw_email.email', ir.incoming_email) - get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' - expect(response.body).to have_content "tënde" - end - - it "should generate valid HTML verson of plain text attachments" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello world.txt.html', :skip_cache => 1 - expect(response.content_type).to eq("text/html") - expect(response.body).to have_content "Second hello" + get_attachment end # This is a regression test for a bug where URLs of this form were causing 500 errors @@ -533,231 +499,134 @@ def make_request # # https://github.com/mysociety/alaveteli/issues/351 it "should return 404 for ugly URLs containing a request id that isn't an integer" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload ugly_id = "55195" - expect { - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ugly_id, :part => 2, :file_name => 'hello world.txt.html', :skip_cache => 1 - }.to raise_error(ActiveRecord::RecordNotFound) - - expect { - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => ugly_id, :part => 2, :file_name => 'hello world.txt', :skip_cache => 1 - }.to raise_error(ActiveRecord::RecordNotFound) - end - it "should return 404 when incoming message and request ids don't match" do - ir = info_requests(:fancy_dog_request) - wrong_id = info_requests(:naughty_chicken_request).id - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload - expect { - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => wrong_id, :part => 2, :file_name => 'hello world.txt.html', :skip_cache => 1 - }.to raise_error(ActiveRecord::RecordNotFound) - end - it "should return 404 for ugly URLs contain a request id that isn't an integer, even if the integer prefix refers to an actual request" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload - ugly_id = "%d95" % [info_requests(:naughty_chicken_request).id] - - expect { - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ugly_id, :part => 2, :file_name => 'hello world.txt.html', :skip_cache => 1 - }.to raise_error(ActiveRecord::RecordNotFound) - - expect { - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => ugly_id, :part => 2, :file_name => 'hello world.txt', :skip_cache => 1 - }.to raise_error(ActiveRecord::RecordNotFound) - end - it "should return 404 when incoming message and request ids don't match" do - ir = info_requests(:fancy_dog_request) - wrong_id = info_requests(:naughty_chicken_request).id - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload - expect { - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => wrong_id, :part => 2, :file_name => 'hello world.txt.html', :skip_cache => 1 - }.to raise_error(ActiveRecord::RecordNotFound) + expect { get_attachment(:id => ugly_id) } + .to raise_error(ActiveRecord::RecordNotFound) end - it "should generate valid HTML verson of PDF attachments" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-pdf-attachment.email', ir.incoming_email) - ir.reload - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'fs 50379341.pdf.html', :skip_cache => 1 - expect(response.content_type).to eq("text/html") - expect(response.body).to have_content "Walberswick Parish Council" + it "should return 404 when incoming message and request ids + don't match" do + expect { get_attachment(:id => info_request.id + 1) } + .to raise_error(ActiveRecord::RecordNotFound) end - it "should not cause a reparsing of the raw email, even when the attachment can't be found" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload - attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename(ir.incoming_messages[1].get_attachments_for_display, 2, 'hello world.txt') - expect(attachment.body).to have_content "Second hello" - - # change the raw_email associated with the message; this only be reparsed when explicitly asked for - ir.incoming_messages[1].raw_email.data = ir.incoming_messages[1].raw_email.data.sub("Second", "Third") - # asking for an attachment by the wrong filename should result in redirecting - # back to the incoming message, but shouldn't cause a reparse: - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello world.txt.baz.html', :skip_cache => 1 - expect(response.status).to eq(303) - - attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename(ir.incoming_messages[1].get_attachments_for_display, 2, 'hello world.txt') - expect(attachment.body).to have_content "Second hello" - - # ...nor should asking for it by its correct filename... - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello world.txt.html', :skip_cache => 1 - expect(response.body).not_to have_content "Third hello" - - # ...but if we explicitly ask for attachments to be extracted, then they should be - force = true - ir.incoming_messages[1].parse_raw_email!(force) - ir.reload - attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename(ir.incoming_messages[1].get_attachments_for_display, 2, 'hello world.txt') - expect(attachment.body).to have_content "Third hello" - get :get_attachment_as_html, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello world.txt.html', :skip_cache => 1 - expect(response.body).to have_content "Third hello" + it "should return 404 for ugly URLs contain a request id that isn't an + integer, even if the integer prefix refers to an actual request" do + ugly_id = "#{FactoryGirl.create(:info_request).id}95" + expect { get_attachment(:id => ugly_id) } + .to raise_error(ActiveRecord::RecordNotFound) end - it "should redirect to the incoming message if there's a wrong part number and an ambiguous filename" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload - - im = ir.incoming_messages[1] - - attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename(im.get_attachments_for_display, 5, 'hello world.txt') + it "should redirect to the incoming message if there's a wrong part number + and an ambiguous filename" do + incoming_message = info_request.incoming_messages.first + attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename( + incoming_message.get_attachments_for_display, + 5, + 'interesting.pdf' + ) expect(attachment).to be_nil - - get :get_attachment_as_html, :incoming_message_id => im.id, :id => ir.id, :part => 5, :file_name => 'hello world.txt', :skip_cache => 1 + get_attachment(:part => 5) expect(response.status).to eq(303) new_location = response.header['Location'] - expect(new_location).to match(/request\/#{ir.url_title}#incoming-#{im.id}/) + expect(new_location) + .to match incoming_message_path(incoming_message) end it "should find a uniquely named filename even if the URL part number was wrong" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-pdf-attachment.email', ir.incoming_email) - ir.reload - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 5, :file_name => 'fs 50379341.pdf', :skip_cache => 1 - expect(response.content_type).to eq("application/pdf") - end - - it "should treat attachments with unknown extensions as binary" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-attachment-unknown-extension.email', ir.incoming_email) - ir.reload - - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello.qwglhm', :skip_cache => 1 - expect(response.content_type).to eq("application/octet-stream") - expect(response.body).to have_content "an unusual sort of file" + info_request = FactoryGirl.create(:info_request_with_html_attachment) + get :get_attachment, :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 5, + :file_name => 'interesting.html', + :skip_cache => 1 + expect(response.body).to match('dull') end it "should not download attachments with wrong file name" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'http://trying.to.hack' + info_request = FactoryGirl.create(:info_request_with_html_attachment) + get :get_attachment, :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'http://trying.to.hack', + :skip_cache => 1 expect(response.status).to eq(303) end it "should sanitise HTML attachments" do - incoming_message = FactoryGirl.create(:incoming_message_with_html_attachment) - get :get_attachment, :incoming_message_id => incoming_message.id, - :id => incoming_message.info_request.id, - :part => 2, - :file_name => 'interesting.html', - :skip_cache => 1 + info_request = FactoryGirl.create(:info_request_with_html_attachment) + get :get_attachment, :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'interesting.html', + :skip_cache => 1 expect(response.body).not_to match("script") expect(response.body).not_to match("interesting") expect(response.body).to match('dull') end - it "should censor attachments downloaded directly" do - ir = info_requests(:fancy_dog_request) + it "censors attachments downloaded directly" do + info_request = FactoryGirl.create(:info_request_with_html_attachment) + info_request.censor_rules.create!(:text => 'dull', + :replacement => "Mouse", + :last_edit_editor => 'unknown', + :last_edit_comment => 'none') + get :get_attachment, :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'interesting.html', + :skip_cache => 1 + expect(response.content_type).to eq("text/html") + expect(response.body).to have_content "Mouse" + end - censor_rule = CensorRule.new - censor_rule.text = "Second" - censor_rule.replacement = "Mouse" - censor_rule.last_edit_editor = "unknown" - censor_rule.last_edit_comment = "none" - ir.censor_rules << censor_rule + it "should censor with rules on the user (rather than the request)" do + info_request = FactoryGirl.create(:info_request_with_html_attachment) + info_request.user.censor_rules.create!(:text => 'dull', + :replacement => "Mouse", + :last_edit_editor => 'unknown', + :last_edit_comment => 'none') + get :get_attachment, :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'interesting.html', + :skip_cache => 1 + expect(response.content_type).to eq("text/html") + expect(response.body).to have_content "Mouse" + end + end +end - begin - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) +describe RequestController do + describe 'GET get_attachment_as_html' do + let(:info_request){ FactoryGirl.create(:info_request_with_incoming_attachments) } - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello world.txt', :skip_cache => 1 - expect(response.content_type).to eq("text/plain") - expect(response.body).to have_content "Mouse hello" - ensure - ir.censor_rules.clear - end + def get_html_attachment(params = {}) + default_params = { :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'interesting.pdf.html' } + get :get_attachment_as_html, default_params.merge(params) end - it "should censor with rules on the user (rather than the request)" do - ir = info_requests(:fancy_dog_request) - - censor_rule = CensorRule.new - censor_rule.text = "Second" - censor_rule.replacement = "Mouse" - censor_rule.last_edit_editor = "unknown" - censor_rule.last_edit_comment = "none" - ir.user.censor_rules << censor_rule - - begin - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - ir.reload - - get :get_attachment, :incoming_message_id => ir.incoming_messages[1].id, :id => ir.id, :part => 2, :file_name => 'hello world.txt', :skip_cache => 1 - expect(response.content_type).to eq("text/plain") - expect(response.body).to have_content "Mouse hello" - ensure - ir.user.censor_rules.clear - end - end - - it "should censor attachment names" do - ir = info_requests(:fancy_dog_request) - receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) - attachment_name_selector = '.attachment .attachment__name' - - # TODO: this is horrid, but don't know a better way. If we - # don't do this, the info_request_event to which the - # info_request is attached still uses the unmodified - # version from the fixture. - #event = info_request_events(:useless_incoming_message_event) - ir.reload - assert ir.info_request_events[3].incoming_message.get_attachments_for_display.count == 2 - ir.save! - ir.incoming_messages.last.save! - get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' - assert assigns[:info_request].info_request_events[3].incoming_message.get_attachments_for_display.count == 2 - # the issue is that the info_request_events have got cached on them the old info_requests. - # where i'm at: trying to replace those fields that got re-read from the raw email. however tests are failing in very strange ways. currently I don't appear to be getting any attachments parsed in at all when in the template (see "*****" in _correspondence.html.erb) but do when I'm in the code. - - # so at this point, assigns[:info_request].incoming_messages[1].get_attachments_for_display is returning stuff, but the equivalent thing in the template isn't. - # but something odd is that the above is return a whole load of attachments which aren't there in the controller - expect(response.body).to have_css(attachment_name_selector) do |s| - expect(s).to contain /hello world.txt/m - end - - censor_rule = CensorRule.new - # Note that the censor rule applies to the original filename, - # not the display_filename: - censor_rule.text = "hello-world.txt" - censor_rule.replacement = "goodbye.txt" - censor_rule.last_edit_editor = "unknown" - censor_rule.last_edit_comment = "none" - ir.censor_rules << censor_rule - begin - get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' - expect(response.body).to have_css(attachment_name_selector) do |s| - expect(s).to contain /goodbye.txt/m - end - ensure - ir.censor_rules.clear - end + it "should return 404 for ugly URLs containing a request id that isn't an integer" do + ugly_id = "55195" + expect { get_html_attachment(:id => ugly_id) } + .to raise_error(ActiveRecord::RecordNotFound) end + it "should return 404 for ugly URLs contain a request id that isn't an + integer, even if the integer prefix refers to an actual request" do + ugly_id = "#{FactoryGirl.create(:info_request).id}95" + expect { get_html_attachment(:id => ugly_id) } + .to raise_error(ActiveRecord::RecordNotFound) + end end end diff --git a/spec/factories/info_requests.rb b/spec/factories/info_requests.rb index 5555c67497..b32e4bbd96 100644 --- a/spec/factories/info_requests.rb +++ b/spec/factories/info_requests.rb @@ -52,6 +52,13 @@ end end + factory :info_request_with_html_attachment do + after(:create) do |info_request, evaluator| + incoming_message = create(:incoming_message_with_html_attachment, :info_request => info_request) + info_request.log_event("response", {:incoming_message_id => incoming_message.id}) + end + end + factory :info_request_with_incoming_attachments do after(:create) do |info_request, evaluator| incoming_message = create(:incoming_message_with_attachments, :info_request => info_request) diff --git a/spec/integration/incoming_mail_spec.rb b/spec/integration/incoming_mail_spec.rb new file mode 100644 index 0000000000..701b966d54 --- /dev/null +++ b/spec/integration/incoming_mail_spec.rb @@ -0,0 +1,147 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/alaveteli_dsl') + +describe 'when handling incoming mail' do + let(:info_request){ FactoryGirl.create(:info_request) } + + it "receives incoming messages, sends email to requester, and shows them" do + receive_incoming_mail('incoming-request-plain.email', + info_request.incoming_email) + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(1) + mail = deliveries[0] + expect(mail.to).to eq([info_request.user.email]) + expect(mail.body).to match(/You have a new response to the Freedom of Information request/) + visit show_request_path :url_title => info_request.url_title + expect(page).to have_content("No way!") + end + + it "makes attachments available for download" do + receive_incoming_mail('incoming-request-two-same-name.email', + info_request.incoming_email) + visit get_attachment_path( + :incoming_message_id => info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'hello world.txt', + :skip_cache => 1) + expect(page.response_headers['Content-Type']).to eq("text/plain; charset=utf-8") + expect(page).to have_content "Second hello" + + visit get_attachment_path( + :incoming_message_id => info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 3, + :file_name => 'hello world.txt', + :skip_cache => 1) + expect(page.response_headers['Content-Type']).to eq("text/plain; charset=utf-8") + expect(page).to have_content "First hello" + end + + it "converts message body to UTF8" do + receive_incoming_mail('iso8859_2_raw_email.email', + info_request.incoming_email) + visit show_request_path :url_title => info_request.url_title + expect(page).to have_content "tënde" + end + + it "generates a valid HTML verson of plain text attachments" do + receive_incoming_mail('incoming-request-two-same-name.email', + info_request.incoming_email) + visit get_attachment_as_html_path( + :incoming_message_id => info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'hello world.txt.html', + :skip_cache => 1) + expect(page.response_headers['Content-Type']).to eq("text/html; charset=utf-8") + expect(page).to have_content "Second hello" + end + + it "generates a valid HTML verson of PDF attachments" do + receive_incoming_mail('incoming-request-pdf-attachment.email', + info_request.incoming_email) + visit get_attachment_as_html_path( + :incoming_message_id => info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'fs 50379341.pdf.html', + :skip_cache => 1) + expect(page.response_headers['Content-Type']).to eq("text/html; charset=utf-8") + expect(page).to have_content "Walberswick Parish Council" + end + + it "does not cause a reparsing of the raw email, even when the attachment can't be found" do + receive_incoming_mail('incoming-request-two-same-name.email', + info_request.incoming_email) + incoming_message = info_request.incoming_messages.first + attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename( + incoming_message.get_attachments_for_display, + 2, + 'hello world.txt') + expect(attachment.body).to match "Second hello" + + # change the raw_email associated with the message; this should only be + # reparsed when explicitly asked for + incoming_message.raw_email.data = incoming_message.raw_email.data.sub("Second", "Third") + incoming_message.save! + # asking for an attachment by the wrong filename should result in redirecting + # back to the incoming message, but shouldn't cause a reparse: + visit get_attachment_as_html_path( + :incoming_message_id => incoming_message.id, + :id => info_request.id, + :part => 2, + :file_name => 'hello world.txt.baz.html', + :skip_cache => 1 + ) + + attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename( + incoming_message.get_attachments_for_display, + 2, + 'hello world.txt') + expect(attachment.body).to match "Second hello" + + # ...nor should asking for it by its correct filename... + visit get_attachment_as_html_path( + :incoming_message_id => incoming_message.id, + :id => info_request.id, + :part => 2, + :file_name => 'hello world.txt.html', + :skip_cache => 1 + ) + expect(page).not_to have_content "Third hello" + + # ...but if we explicitly ask for attachments to be extracted, then they should be + force = true + incoming_message.parse_raw_email!(force) + attachment = IncomingMessage.get_attachment_by_url_part_number_and_filename( + incoming_message.get_attachments_for_display, + 2, + 'hello world.txt') + expect(attachment.body).to match "Third hello" + visit get_attachment_as_html_path( + :incoming_message_id => incoming_message.id, + :id => info_request.id, + :part => 2, + :file_name => 'hello world.txt.html', + :skip_cache => 1 + ) + expect(page).to have_content "Third hello" + end + + it "treats attachments with unknown extensions as binary" do + receive_incoming_mail('incoming-request-attachment-unknown-extension.email', + info_request.incoming_email) + visit get_attachment_path( + :incoming_message_id => info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'hello.qwglhm', + :skip_cache => 1 + ) + expect(page.response_headers['Content-Type']).to eq("application/octet-stream; charset=utf-8") + expect(page).to have_content "an unusual sort of file" + end + +end \ No newline at end of file From dd615ffaf4c6d0de0f9efbff9d12e9ccd721efb5 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 22 Nov 2016 13:45:23 +0000 Subject: [PATCH 053/311] Add spec for returning a 404 for embargoed requests from attachments. --- spec/controllers/request_controller_spec.rb | 21 +++++++++++++++++++++ spec/factories/info_requests.rb | 2 ++ 2 files changed, 23 insertions(+) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 73dbb52045..a2da80ec7c 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -599,6 +599,17 @@ def get_attachment(params = {}) expect(response.content_type).to eq("text/html") expect(response.body).to have_content "Mouse" end + + it 'returns an ActiveRecord::RecordNotFound error for an embargoed request' do + info_request = FactoryGirl.create(:embargoed_request) + expect{ get :get_attachment, :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'interesting.pdf', + :skip_cache => 1 } + .to raise_error ActiveRecord::RecordNotFound + end end end @@ -628,6 +639,16 @@ def get_html_attachment(params = {}) .to raise_error(ActiveRecord::RecordNotFound) end + it 'returns an ActiveRecord::RecordNotFound error for an embargoed request' do + info_request = FactoryGirl.create(:embargoed_request) + expect{ get :get_attachment_as_html, :incoming_message_id => + info_request.incoming_messages.first.id, + :id => info_request.id, + :part => 2, + :file_name => 'interesting.pdf.html' } + .to raise_error ActiveRecord::RecordNotFound + end + end end diff --git a/spec/factories/info_requests.rb b/spec/factories/info_requests.rb index b32e4bbd96..7d506d46a0 100644 --- a/spec/factories/info_requests.rb +++ b/spec/factories/info_requests.rb @@ -75,6 +75,8 @@ factory :embargoed_request do after(:create) do |info_request, evaluator| embargo = create(:embargo, :info_request => info_request) + incoming_message = create(:incoming_message_with_attachments, :info_request => info_request) + info_request.log_event("response", {:incoming_message_id => incoming_message.id}) end end From 63f9d382b9afb5ab6e33486444d1e9f1ef1ec54f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 22 Nov 2016 14:14:15 +0000 Subject: [PATCH 054/311] Handle the case of a comment on an embargoed request. Scope the finder for the info_request to be commented on to non embargoed requests, so that a request for an embargoed request should result in an ActiveRecord::RecordNotFound error and return a 404 not found response in production. --- app/controllers/comment_controller.rb | 3 ++- spec/controllers/comment_controller_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/controllers/comment_controller.rb b/app/controllers/comment_controller.rb index 759524e8fb..ecb61a262e 100644 --- a/app/controllers/comment_controller.rb +++ b/app/controllers/comment_controller.rb @@ -92,7 +92,8 @@ def comment_params def find_info_request if params[:type] == 'request' - @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + @info_request = + InfoRequest.not_embargoed.find_by_url_title!(params[:url_title]) else raise "Unknown type #{ params[:type] }" end diff --git a/spec/controllers/comment_controller_spec.rb b/spec/controllers/comment_controller_spec.rb index 21552fbc93..cf91601a40 100644 --- a/spec/controllers/comment_controller_spec.rb +++ b/spec/controllers/comment_controller_spec.rb @@ -4,6 +4,16 @@ describe CommentController, "when commenting on a request" do render_views + it 'returns a 404 when the info request is embargoed' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect{ post :new, :url_title => embargoed_request.url_title, + :comment => { :body => "Some content" }, + :type => 'request', + :submitted_comment => 1, + :preview => 1 } + .to raise_error ActiveRecord::RecordNotFound + end + it "should give an error and render 'new' template when body text is just some whitespace" do post :new, :url_title => info_requests(:naughty_chicken_request).url_title, :comment => { :body => " " }, From 156ac4b3da641b5367b3249ebe295e5399529ed6 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 11:02:35 +0000 Subject: [PATCH 055/311] Standard format spec descriptions. --- spec/controllers/help_controller_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 838407c261..7a5368b0dd 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -4,7 +4,7 @@ describe HelpController do render_views - describe 'GET index' do + describe 'GET #index' do it 'redirects to the about page' do get :index @@ -13,7 +13,7 @@ end - describe 'GET about' do + describe 'GET #about' do it 'shows the about page' do get :about @@ -23,7 +23,7 @@ end - describe 'GET contact' do + describe 'GET #contact' do it 'shows contact form' do get :contact @@ -48,7 +48,7 @@ end - describe 'POST contact' do + describe 'POST #contact' do it 'sends a contact message' do post :contact, { :contact => { From c7eb874a728ad4b95f7559a700f07094693820b1 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 11:16:24 +0000 Subject: [PATCH 056/311] Add some specs for HelpController#unhappy. --- spec/controllers/help_controller_spec.rb | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 7a5368b0dd..2eb3d48f02 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -13,6 +13,36 @@ end + describe 'GET #unhappy' do + let(:info_request){ FactoryGirl.create(:info_request) } + + it 'shows the unhappy template' do + get :unhappy + expect(response).to render_template('help/unhappy') + end + + it 'does not assign an info_request' do + get :unhappy + expect(assigns[:info_request]).to be nil + end + + context 'when a url_title param is supplied' do + + it 'assigns the info_request' do + get :unhappy, :url_title => info_request.url_title + expect(assigns[:info_request]).to eq info_request + end + + it 'raises an ActiveRecord::RecordNotFound error if the InfoRequest + is not found' do + expect{ get :unhappy, :url_title => 'something_not_existing' } + .to raise_error ActiveRecord::RecordNotFound + end + + end + + end + describe 'GET #about' do it 'shows the about page' do From 86e60362ca90d309f34809280c2b903ecbaefc2f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 11:16:44 +0000 Subject: [PATCH 057/311] Raise RecordNotFound for unhappy action with embargoed request. This should be the same behaviour as if you passed the url_title of a non-existent request as a param. --- app/controllers/help_controller.rb | 4 +++- spec/controllers/help_controller_spec.rb | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index b20714ae19..49fc0f43db 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -20,7 +20,9 @@ def unhappy @country_code = AlaveteliConfiguration.iso_country_code @info_request = nil if params[:url_title] - @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + @info_request = InfoRequest + .not_embargoed + .find_by_url_title!(params[:url_title]) end end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 2eb3d48f02..73d36521cc 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -39,6 +39,12 @@ .to raise_error ActiveRecord::RecordNotFound end + it 'raises an ActiveRecord::RecordNotFound error if the InfoRequest + is embargoed' do + info_request = FactoryGirl.create(:embargoed_request) + expect{ get :unhappy, :url_title => info_request.url_title } + .to raise_error ActiveRecord::RecordNotFound + end end end From f85d2ca8a3495a76868fd7f8a0b0e857a06d40f7 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 11:32:55 +0000 Subject: [PATCH 058/311] Add some more specs to HelpController#unhappy to cover last_request_id --- spec/controllers/help_controller_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 73d36521cc..3b103661c8 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -82,6 +82,24 @@ end + context 'when a url_title param is supplied' do + let(:info_request){ FactoryGirl.create(:info_request) } + + it 'assigns the last request' do + request.cookies["last_request_id"] = info_request.id + get :contact + expect(assigns[:last_request]).to eq info_request + end + + it 'raises an ActiveRecord::RecordNotFound error if the InfoRequest + is not found' do + request.cookies["last_request_id"] = InfoRequest.maximum(:id)+1 + expect{ get :contact } + .to raise_error ActiveRecord::RecordNotFound + end + + end + end describe 'POST #contact' do From f43d64533065272974412c0ab1d51f88baea738b Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 11:33:38 +0000 Subject: [PATCH 059/311] Raise RecordNotFound if last_request_id is an embargoed request. This _should_ never happen as we're only setting last request id on actions that handle public requests, but an embargo could be added in the meantime, and it seems sensible to guard this action consistently with others. --- app/controllers/help_controller.rb | 2 +- spec/controllers/help_controller_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 49fc0f43db..ab51ee5dc5 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -39,7 +39,7 @@ def contact # look up link to request/body last_request_id = cookies["last_request_id"].to_i if last_request_id > 0 - @last_request = InfoRequest.find(last_request_id) + @last_request = InfoRequest.not_embargoed.find(last_request_id) else @last_request = nil end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 3b103661c8..36ba22f903 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -98,6 +98,14 @@ .to raise_error ActiveRecord::RecordNotFound end + it 'raises an ActiveRecord::RecordNotFound error if the InfoRequest + is embargoed' do + info_request = FactoryGirl.create(:embargoed_request) + request.cookies["last_request_id"] = info_request.id + expect{ get :contact } + .to raise_error ActiveRecord::RecordNotFound + end + end end From e11287f690bc1772372bc819b68ec74d7094edf7 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 11:38:37 +0000 Subject: [PATCH 060/311] Remove unneeded view rendering. --- spec/controllers/reports_controller_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/controllers/reports_controller_spec.rb b/spec/controllers/reports_controller_spec.rb index f173f6707e..a9d8dadcb9 100644 --- a/spec/controllers/reports_controller_spec.rb +++ b/spec/controllers/reports_controller_spec.rb @@ -11,7 +11,6 @@ end describe ReportsController, "when reporting a request (logged in)" do - render_views before do @user = users(:robin_user) From 75b4f6a3b92bf8946ab838b8f3ac1b2a6d875724 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 11:42:14 +0000 Subject: [PATCH 061/311] Standardise spec format. --- spec/controllers/reports_controller_spec.rb | 179 ++++++++++---------- 1 file changed, 94 insertions(+), 85 deletions(-) diff --git a/spec/controllers/reports_controller_spec.rb b/spec/controllers/reports_controller_spec.rb index a9d8dadcb9..92ac9a1872 100644 --- a/spec/controllers/reports_controller_spec.rb +++ b/spec/controllers/reports_controller_spec.rb @@ -1,102 +1,111 @@ # -*- encoding : utf-8 -*- require 'spec_helper' -describe ReportsController, "when reporting a request when not logged in" do - it "should only allow logged-in users to report requests" do - post :create, :request_id => info_requests(:badger_request).url_title, :reason => "my reason" +describe ReportsController do - expect(flash[:notice]).to match(/You need to be logged in/) - expect(response).to redirect_to show_request_path(:url_title => info_requests(:badger_request).url_title) - end -end - -describe ReportsController, "when reporting a request (logged in)" do - - before do - @user = users(:robin_user) - session[:user_id] = @user.id - end - - it "should 404 for non-existent requests" do - expect { - post :create, :request_id => "hjksfdhjk_louytu_qqxxx" - }.to raise_error(ActiveRecord::RecordNotFound) - end - - it "should mark a request as having been reported" do - ir = info_requests(:badger_request) - title = ir.url_title - expect(ir.attention_requested).to eq(false) - - post :create, :request_id => title, :reason => "my reason" - expect(response).to redirect_to show_request_path(:url_title => title) - - ir.reload - expect(ir.attention_requested).to eq(true) - expect(ir.described_state).to eq("attention_requested") - end - - it "should pass on the reason and message" do - info_request = mock_model(InfoRequest, :url_title => "foo", :attention_requested= => nil, :save! => nil) - expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) - expect(info_request).to receive(:report!).with("Not valid request", "It's just not", @user) - post :create, :request_id => "foo", :reason => "Not valid request", :message => "It's just not" - end + describe 'POST #create' do + context "when reporting a request when not logged in" do + it "should only allow logged-in users to report requests" do + post :create, :request_id => info_requests(:badger_request).url_title, :reason => "my reason" - it "should not allow a request to be reported twice" do - title = info_requests(:badger_request).url_title - - post :create, :request_id => title, :reason => "my reason" - expect(response).to redirect_to show_request_url(:url_title => title) + expect(flash[:notice]).to match(/You need to be logged in/) + expect(response).to redirect_to show_request_path(:url_title => info_requests(:badger_request).url_title) + end + end - post :create, :request_id => title, :reason => "my reason" - expect(response).to redirect_to show_request_url(:url_title => title) - expect(flash[:notice]).to match(/has already been reported/) + context "when reporting a request (logged in)" do + before do + @user = users(:robin_user) + session[:user_id] = @user.id + end + + it "should 404 for non-existent requests" do + expect { + post :create, :request_id => "hjksfdhjk_louytu_qqxxx" + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "should mark a request as having been reported" do + ir = info_requests(:badger_request) + title = ir.url_title + expect(ir.attention_requested).to eq(false) + + post :create, :request_id => title, :reason => "my reason" + expect(response).to redirect_to show_request_path(:url_title => title) + + ir.reload + expect(ir.attention_requested).to eq(true) + expect(ir.described_state).to eq("attention_requested") + end + + it "should pass on the reason and message" do + info_request = mock_model(InfoRequest, :url_title => "foo", :attention_requested= => nil, :save! => nil) + expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) + expect(info_request).to receive(:report!).with("Not valid request", "It's just not", @user) + post :create, :request_id => "foo", :reason => "Not valid request", :message => "It's just not" + end + + it "should not allow a request to be reported twice" do + title = info_requests(:badger_request).url_title + + post :create, :request_id => title, :reason => "my reason" + expect(response).to redirect_to show_request_url(:url_title => title) + + post :create, :request_id => title, :reason => "my reason" + expect(response).to redirect_to show_request_url(:url_title => title) + expect(flash[:notice]).to match(/has already been reported/) + end + + it "should send an email from the reporter to admins" do + ir = info_requests(:badger_request) + title = ir.url_title + post :create, :request_id => title, :reason => "my reason" + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(1) + mail = deliveries[0] + expect(mail.subject).to match(/attention_requested/) + expect(mail.header['Reply-To'].to_s).to include(@user.email) + expect(mail.body).to include(@user.name) + end + + it "should force the user to pick a reason" do + info_request = mock_model(InfoRequest, :report! => nil, :url_title => "foo", + :report_reasons => ["Not FOIish enough"]) + expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) + + post :create, :request_id => "foo", :reason => "" + expect(response).to render_template("new") + expect(flash[:error]).to eq("Please choose a reason") + end + end end +end - it "should send an email from the reporter to admins" do - ir = info_requests(:badger_request) - title = ir.url_title - post :create, :request_id => title, :reason => "my reason" - deliveries = ActionMailer::Base.deliveries - expect(deliveries.size).to eq(1) - mail = deliveries[0] - expect(mail.subject).to match(/attention_requested/) - expect(mail.header['Reply-To'].to_s).to include(@user.email) - expect(mail.body).to include(@user.name) - end +describe ReportsController do - it "should force the user to pick a reason" do - info_request = mock_model(InfoRequest, :report! => nil, :url_title => "foo", - :report_reasons => ["Not FOIish enough"]) - expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) + describe "GET #new" do - post :create, :request_id => "foo", :reason => "" - expect(response).to render_template("new") - expect(flash[:error]).to eq("Please choose a reason") - end -end + let(:info_request) { mock_model(InfoRequest, :url_title => "foo") } -describe ReportsController, "#new_report_request" do - let(:info_request) { mock_model(InfoRequest, :url_title => "foo") } - before :each do - expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) - end - - context "not logged in" do - it "should require the user to be logged in" do - get :new, :request_id => "foo" - expect(response).not_to render_template("new") + before :each do + expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) end - end - context "logged in" do - before :each do - session[:user_id] = users(:bob_smith_user).id + context "not logged in" do + it "should require the user to be logged in" do + get :new, :request_id => "foo" + expect(response).not_to render_template("new") + end end - it "should show the form" do - get :new, :request_id => "foo" - expect(response).to render_template("new") + + context "logged in" do + before :each do + session[:user_id] = users(:bob_smith_user).id + end + it "should show the form" do + get :new, :request_id => "foo" + expect(response).to render_template("new") + end end end end From 0e43a8c38c6bb8b76875063313242e5513980bd7 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 12:03:04 +0000 Subject: [PATCH 062/311] Cleanup specs Use factories, not fixtures and mocks. Remove spec testing action internals in favour of testing results. --- spec/controllers/reports_controller_spec.rb | 93 +++++++++++---------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/spec/controllers/reports_controller_spec.rb b/spec/controllers/reports_controller_spec.rb index 92ac9a1872..c83296c3b8 100644 --- a/spec/controllers/reports_controller_spec.rb +++ b/spec/controllers/reports_controller_spec.rb @@ -4,19 +4,23 @@ describe ReportsController do describe 'POST #create' do + let(:info_request){ FactoryGirl.create(:info_request) } + let(:user){ FactoryGirl.create(:user) } + context "when reporting a request when not logged in" do it "should only allow logged-in users to report requests" do - post :create, :request_id => info_requests(:badger_request).url_title, :reason => "my reason" - + post :create, :request_id => info_request.url_title, + :reason => "my reason" expect(flash[:notice]).to match(/You need to be logged in/) - expect(response).to redirect_to show_request_path(:url_title => info_requests(:badger_request).url_title) + expect(response) + .to redirect_to show_request_path(:url_title => + info_request.url_title) end end context "when reporting a request (logged in)" do before do - @user = users(:robin_user) - session[:user_id] = @user.id + session[:user_id] = user.id end it "should 404 for non-existent requests" do @@ -26,54 +30,51 @@ end it "should mark a request as having been reported" do - ir = info_requests(:badger_request) - title = ir.url_title - expect(ir.attention_requested).to eq(false) + expect(info_request.attention_requested).to eq(false) - post :create, :request_id => title, :reason => "my reason" - expect(response).to redirect_to show_request_path(:url_title => title) - - ir.reload - expect(ir.attention_requested).to eq(true) - expect(ir.described_state).to eq("attention_requested") - end + post :create, :request_id => info_request.url_title, + :reason => "my reason" + expect(response) + .to redirect_to show_request_path(:url_title => + info_request.url_title) - it "should pass on the reason and message" do - info_request = mock_model(InfoRequest, :url_title => "foo", :attention_requested= => nil, :save! => nil) - expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) - expect(info_request).to receive(:report!).with("Not valid request", "It's just not", @user) - post :create, :request_id => "foo", :reason => "Not valid request", :message => "It's just not" + info_request.reload + expect(info_request.attention_requested).to eq(true) + expect(info_request.described_state).to eq("attention_requested") end it "should not allow a request to be reported twice" do - title = info_requests(:badger_request).url_title - - post :create, :request_id => title, :reason => "my reason" - expect(response).to redirect_to show_request_url(:url_title => title) - - post :create, :request_id => title, :reason => "my reason" - expect(response).to redirect_to show_request_url(:url_title => title) + post :create, :request_id => info_request.url_title, + :reason => "my reason" + expect(response) + .to redirect_to show_request_url(:url_title => + info_request.url_title) + + post :create, :request_id => info_request.url_title, + :reason => "my reason" + expect(response) + .to redirect_to show_request_url(:url_title => + info_request.url_title) expect(flash[:notice]).to match(/has already been reported/) end it "should send an email from the reporter to admins" do - ir = info_requests(:badger_request) - title = ir.url_title - post :create, :request_id => title, :reason => "my reason" + post :create, :request_id => info_request.url_title, + :reason => "my reason", + :message => "It's just not" deliveries = ActionMailer::Base.deliveries expect(deliveries.size).to eq(1) mail = deliveries[0] expect(mail.subject).to match(/attention_requested/) - expect(mail.header['Reply-To'].to_s).to include(@user.email) - expect(mail.body).to include(@user.name) + expect(mail.header['Reply-To'].to_s).to include(user.email) + expect(mail.body).to include(user.name) + expect(mail.body) + .to include("Reason: my reason\n\nIt's just not") end it "should force the user to pick a reason" do - info_request = mock_model(InfoRequest, :report! => nil, :url_title => "foo", - :report_reasons => ["Not FOIish enough"]) - expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) - - post :create, :request_id => "foo", :reason => "" + post :create, :request_id => info_request.url_title, + :reason => "" expect(response).to render_template("new") expect(flash[:error]).to eq("Please choose a reason") end @@ -84,28 +85,28 @@ describe ReportsController do describe "GET #new" do - - let(:info_request) { mock_model(InfoRequest, :url_title => "foo") } - - before :each do - expect(InfoRequest).to receive(:find_by_url_title!).with("foo").and_return(info_request) - end + let(:info_request){ FactoryGirl.create(:info_request) } + let(:user){ FactoryGirl.create(:user) } context "not logged in" do it "should require the user to be logged in" do - get :new, :request_id => "foo" + get :new, :request_id => info_request.url_title expect(response).not_to render_template("new") end end context "logged in" do before :each do - session[:user_id] = users(:bob_smith_user).id + session[:user_id] = user.id end + it "should show the form" do - get :new, :request_id => "foo" + get :new, :request_id => info_request.url_title expect(response).to render_template("new") end + end + end + end From c0d38efea99ea1bbce57da6a9e7dddd65acc1d87 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 12:09:26 +0000 Subject: [PATCH 063/311] Raise RecordNotFound when reporting an embargoed request. This should be the same behaviour as when reporting a non existent request. --- app/controllers/reports_controller.rb | 9 ++++++--- spec/controllers/reports_controller_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 80a31a9e06..09ec37c2e3 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -1,7 +1,9 @@ # -*- encoding : utf-8 -*- class ReportsController < ApplicationController def create - @info_request = InfoRequest.find_by_url_title!(params[:request_id]) + @info_request = InfoRequest + .not_embargoed + .find_by_url_title!(params[:request_id]) @reason = params[:reason] @message = params[:message] if @reason.empty? @@ -9,7 +11,6 @@ def create render "new" return end - if !authenticated_user flash[:notice] = _("You need to be logged in to report a request for administrator attention") elsif @info_request.attention_requested @@ -22,7 +23,9 @@ def create end def new - @info_request = InfoRequest.find_by_url_title!(params[:request_id]) + @info_request = InfoRequest + .not_embargoed + .find_by_url_title!(params[:request_id]) if authenticated?( :web => _("To report this request"), :email => _("Then you can report the request '{{title}}'", :title => @info_request.title), diff --git a/spec/controllers/reports_controller_spec.rb b/spec/controllers/reports_controller_spec.rb index c83296c3b8..f175a3d134 100644 --- a/spec/controllers/reports_controller_spec.rb +++ b/spec/controllers/reports_controller_spec.rb @@ -29,6 +29,13 @@ }.to raise_error(ActiveRecord::RecordNotFound) end + it 'should 404 for embargoed requests' do + info_request = FactoryGirl.create(:embargoed_request) + expect { + post :create, :request_id => info_request.url_title + }.to raise_error(ActiveRecord::RecordNotFound) + end + it "should mark a request as having been reported" do expect(info_request.attention_requested).to eq(false) @@ -78,6 +85,7 @@ expect(response).to render_template("new") expect(flash[:error]).to eq("Please choose a reason") end + end end end @@ -105,6 +113,19 @@ expect(response).to render_template("new") end + it "should 404 for non-existent requests" do + expect { + get :new, :request_id => "hjksfdhjk_louytu_qqxxx" + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'should 404 for embargoed requests' do + info_request = FactoryGirl.create(:embargoed_request) + expect { + get :new, :request_id => info_request.url_title + }.to raise_error(ActiveRecord::RecordNotFound) + end + end end From bf17b37a97e1b8d5009558b4438a568104a91233 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 14:29:13 +0000 Subject: [PATCH 064/311] Clean up track_controller specs Group by action, with standard descriptions. Use factories rather than fixtures where easy to do, move integration specs to separate file. --- spec/controllers/track_controller_spec.rb | 355 +++++++++------------- spec/factories/track_things.rb | 27 +- spec/integration/alaveteli_dsl.rb | 2 +- spec/integration/track_alerts_spec.rb | 84 +++++ 4 files changed, 248 insertions(+), 220 deletions(-) create mode 100644 spec/integration/track_alerts_spec.rb diff --git a/spec/controllers/track_controller_spec.rb b/spec/controllers/track_controller_spec.rb index 1054f71671..cd67b56272 100644 --- a/spec/controllers/track_controller_spec.rb +++ b/spec/controllers/track_controller_spec.rb @@ -4,43 +4,42 @@ describe TrackController do let(:mock_cookie) { '0300fd3e1177127cebff' } - describe 'GET track_request' do - it 'clears widget votes for the request' do - allow(AlaveteliConfiguration).to receive(:enable_widgets).and_return(true) - @info_request = FactoryGirl.create(:info_request) - @info_request.widget_votes.create(:cookie => mock_cookie) - - session[:user_id] = FactoryGirl.create(:user).id - request.cookies['widget_vote'] = mock_cookie - - get :track_request, :url_title => @info_request.url_title, :feed => 'track' - expect(@info_request.reload.widget_votes).to be_empty - end - end - - describe "when making a new track on a request" do - let(:ir) do + describe 'GET #track_request' do + let(:info_request) do FactoryGirl.create(:info_request, :title => 'My request', :url_title => 'myrequest') end - let(:track_thing) do FactoryGirl.create(:request_update_track, - :info_request => ir, + :info_request => info_request, :track_medium => 'email_daily', :track_query => 'example') end - let(:user) { FactoryGirl.create(:user, :locale => 'en', :name => 'bob') } + it 'clears widget votes for the request' do + allow(AlaveteliConfiguration).to receive(:enable_widgets).and_return(true) + info_request.widget_votes.create(:cookie => mock_cookie) + + session[:user_id] = user.id + request.cookies['widget_vote'] = mock_cookie + + get :track_request, :url_title => info_request.url_title, + :feed => 'track' + expect(info_request.reload.widget_votes).to be_empty + end + it "should require login when making new track" do - get :track_request, :url_title => ir.url_title, :feed => 'track' - expect(response).to redirect_to(signin_path(:token => get_last_post_redirect.token)) + get :track_request, :url_title => info_request.url_title, + :feed => 'track' + expect(response) + .to redirect_to(signin_path(:token => get_last_post_redirect.token)) end it "should set no-cache headers on the login redirect" do - get :track_request, :url_title => ir.url_title, :feed => 'track' + get :track_request, :url_title => info_request.url_title, + :feed => 'track' expect(response.headers["Cache-Control"]). to eq('no-cache, no-store, max-age=0, must-revalidate') expect(response.headers['Pragma']).to eq('no-cache') @@ -51,19 +50,98 @@ session[:user_id] = user.id allow(TrackThing).to receive(:create_track_for_request).and_return(track_thing) expect(track_thing).to receive(:save).and_call_original - get :track_request, :url_title => ir.url_title, :feed => 'track' + get :track_request, :url_title => info_request.url_title, + :feed => 'track' expect(response).to redirect_to(:controller => 'request', - :action => 'show', :url_title => ir.url_title) + :action => 'show', + :url_title => info_request.url_title) end it "should 404 for non-existent requests" do session[:user_id] = user.id - expect { get :track_request, :url_title => "hjksfdh_louytu_qqxxx", :feed => 'track' }. - to raise_error(ActiveRecord::RecordNotFound) + expect { get :track_request, :url_title => "hjksfdh_louytu_qqxxx", + :feed => 'track' } + .to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when getting feeds' do + + before do + load_raw_emails_data + get_fixtures_xapian_index + end + + it "should get the RSS feed" do + + track_thing = track_things(:track_fancy_dog_request) + + get :track_request, :feed => 'feed', + :url_title => track_thing.info_request.url_title + expect(response).to render_template('track/atom_feed') + expect(response.content_type).to eq('application/atom+xml') + # TODO: should check it is an atom.builder type being rendered, + # not sure how to + expect(assigns[:xapian_object].matches_estimated).to eq(3) + expect(assigns[:xapian_object].results.size).to eq(3) + expect(assigns[:xapian_object].results[0][:model]) + .to eq(info_request_events(:silly_comment_event)) + expect(assigns[:xapian_object].results[1][:model]) + .to eq(info_request_events(:useless_incoming_message_event)) + expect(assigns[:xapian_object].results[2][:model]) + .to eq(info_request_events(:useless_outgoing_message_event)) + end + it "should get JSON version of the feed" do + track_thing = track_things(:track_fancy_dog_request) + + get :track_request, :feed => 'feed', + :url_title => track_thing.info_request.url_title, + :format => "json" + + a = JSON.parse(response.body) + expect(a.class.to_s).to eq('Array') + expect(a.size).to eq(3) + + expect(a[0]['id']) + .to eq(info_request_events(:silly_comment_event).id) + expect(a[1]['id']) + .to eq(info_request_events(:useless_incoming_message_event).id) + expect(a[2]['id']) + .to eq(info_request_events(:useless_outgoing_message_event).id) + + expect(a[0]['info_request']['url_title']) + .to eq('why_do_you_have_such_a_fancy_dog') + expect(a[1]['info_request']['url_title']) + .to eq('why_do_you_have_such_a_fancy_dog') + expect(a[2]['info_request']['url_title']) + .to eq('why_do_you_have_such_a_fancy_dog') + + expect(a[0]['public_body']['url_name']).to eq('tgq') + expect(a[1]['public_body']['url_name']).to eq('tgq') + expect(a[2]['public_body']['url_name']).to eq('tgq') + + expect(a[0]['user']['url_name']).to eq('bob_smith') + expect(a[1]['user']['url_name']).to eq('bob_smith') + expect(a[2]['user']['url_name']).to eq('bob_smith') + + expect(a[0]['event_type']).to eq('comment') + expect(a[1]['event_type']).to eq('response') + expect(a[2]['event_type']).to eq('sent') + end + + it 'should return atom/xml for a feed url without format specified, even if the + requester prefers json' do + request.env['HTTP_ACCEPT'] = 'application/json,text/xml' + track_thing = FactoryGirl.create(:request_update_track) + get :track_request, :feed => 'feed', + :url_title => track_thing.info_request.url_title + expect(response).to render_template('track/atom_feed') + expect(response.content_type).to eq('application/atom+xml') + end end + end - describe "when making a search track" do + describe "GET #track_search_query" do let(:track_thing) do FactoryGirl.create(:search_track, :track_medium => 'email_daily', @@ -93,7 +171,7 @@ end end - describe "when making a new track on a public body" do + describe "GET #track_public_body" do let(:public_body) { FactoryGirl.create(:public_body) } let(:user) { FactoryGirl.create(:user, :locale => 'en', :name => 'bob') } @@ -119,9 +197,34 @@ expect(flash[:error]).to match('too long') expect(response).to redirect_to("/body/#{public_body.url_name}") end + + it "should work" do + get :track_public_body, :feed => 'feed', + :url_name => public_body.url_name + expect(response).to be_success + expect(response).to render_template('track/atom_feed') + tt = assigns[:track_thing] + expect(tt.public_body).to eq(public_body) + expect(tt.track_type).to eq('public_body_updates') + expect(tt.track_query).to eq("requested_from:" + public_body.url_name) + end + + it "should filter by event type" do + get :track_public_body, :feed => 'feed', + :url_name => public_body.url_name, + :event_type => 'sent' + expect(response).to be_success + expect(response).to render_template('track/atom_feed') + tt = assigns[:track_thing] + expect(tt.public_body).to eq(public_body) + expect(tt.track_type).to eq('public_body_updates') + expect(tt.track_query) + .to eq("requested_from:#{public_body.url_name} variety:sent") + end + end - describe "when making a new track on a user" do + describe "GET #track_user" do let(:target_user) { FactoryGirl.create(:user) } let(:user) { FactoryGirl.create(:user) } @@ -146,9 +249,15 @@ expect(flash[:error]).to match('too long') expect(response).to redirect_to("/user/#{target_user.url_name}") end + + it "should return NotFound for a non-existent user" do + expect { get :track_user, :feed => 'feed', :url_name => "there_is_no_such_user" }. + to raise_error(ActiveRecord::RecordNotFound) + end + end - describe "when making a new track on a list" do + describe "GET #track_list" do let(:user) { FactoryGirl.create(:user) } it "should save a list track and redirect to the right place" do @@ -174,7 +283,7 @@ end end - describe "when unsubscribing from a track" do + describe "PUT #update" do let(:track_thing) { FactoryGirl.create(:search_track) } it 'should destroy the track thing' do @@ -203,189 +312,7 @@ end end - describe "when sending alerts for a track" do - render_views - - before(:each) do - load_raw_emails_data - get_fixtures_xapian_index - end - - it "should send alerts" do - # set the time the comment event happened at to within the last week - ire = info_request_events(:silly_comment_event) - ire.created_at = Time.now - 3.days - ire.save! - - TrackMailer.alert_tracks - - deliveries = ActionMailer::Base.deliveries - expect(deliveries.size).to eq(1) - mail = deliveries[0] - expect(mail.body).to match(/Alter your subscription/) - expect(mail.to_addrs.first.to_s).to include(users(:silly_name_user).email) - mail.body.to_s =~ /(http:\/\/.*\/c\/(.*))/ - mail_url = $1 - mail_token = $2 - - expect(mail.body).not_to match(/&/) - - expect(mail.body).not_to include('sent a request') # request not included - expect(mail.body).not_to include('sent a response') # response not included - expect(mail.body).to include('added an annotation') # comment included - - expect(mail.body).to match(/This a the daftest comment the world has ever seen/) # comment text included - # Check subscription managing link - # TODO: We can't do this, as it is redirecting to another controller. I'm - # apparently meant to be writing controller unit tests here, not functional - # tests. Bah, I so don't care, bit of an obsessive constraint. - # session[:user_id].should be_nil - # controller.test_code_redirect_by_email_token(mail_token, self) # TODO: hack to avoid having to call User controller for email link - # session[:user_id].should == users(:silly_name_user).id - # - # response.should render_template('users/show') - # assigns[:display_user].should == users(:silly_name_user) - - # Given we can't click the link, check the token is right instead - post_redirect = PostRedirect.find_by_email_token(mail_token) - expected_url = show_user_url(:url_name => users(:silly_name_user).url_name, - :anchor => "email_subscriptions") - expect(post_redirect.uri).to eq(expected_url) - - # Check nothing more is delivered if we try again - deliveries.clear - TrackMailer.alert_tracks - deliveries = ActionMailer::Base.deliveries - expect(deliveries.size).to eq(0) - end - - it "should send localised alerts" do - # set the time the comment event happened at to within the last week - ire = info_request_events(:silly_comment_event) - ire.created_at = Time.now - 3.days - ire.save! - user = users(:silly_name_user) - user.locale = "es" - user.save! - TrackMailer.alert_tracks - deliveries = ActionMailer::Base.deliveries - mail = deliveries[0] - expect(mail.body).to include('el equipo de ') - end - end - - describe "when viewing RSS feed for a track" do - render_views - - before(:each) do - load_raw_emails_data - get_fixtures_xapian_index - end - - it "should get the RSS feed" do - track_thing = track_things(:track_fancy_dog_request) - - get :track_request, :feed => 'feed', :url_title => track_thing.info_request.url_title - expect(response).to render_template('track/atom_feed') - expect(response.content_type).to eq('application/atom+xml') - # TODO: should check it is an atom.builder type being rendered, not sure how to - - expect(assigns[:xapian_object].matches_estimated).to eq(3) - expect(assigns[:xapian_object].results.size).to eq(3) - expect(assigns[:xapian_object].results[0][:model]).to eq(info_request_events(:silly_comment_event)) # created_at 2008-08-12 23:05:12.500942 - expect(assigns[:xapian_object].results[1][:model]).to eq(info_request_events(:useless_incoming_message_event)) # created_at 2007-11-13 18:09:20.042061 - expect(assigns[:xapian_object].results[2][:model]).to eq(info_request_events(:useless_outgoing_message_event)) # created_at 2007-10-14 10:41:12.686264 - end - - it "should return NotFound for a non-existent user" do - expect { get :track_user, :feed => 'feed', :url_name => "there_is_no_such_user" }. - to raise_error(ActiveRecord::RecordNotFound) - end - - it 'should return atom/xml for a feed url without format specified, even if the - requester prefers json' do - - request.env['HTTP_ACCEPT'] = 'application/json,text/xml' - track_thing = track_things(:track_fancy_dog_request) - - get :track_request, :feed => 'feed', :url_title => track_thing.info_request.url_title - expect(response).to render_template('track/atom_feed') - expect(response.content_type).to eq('application/atom+xml') - end - end - - describe "when viewing JSON version of a track feed" do - render_views - - before(:each) do - load_raw_emails_data - get_fixtures_xapian_index - end - - it "should get the feed" do - track_thing = track_things(:track_fancy_dog_request) - - get :track_request, :feed => 'feed', :url_title => track_thing.info_request.url_title, :format => "json" - - a = JSON.parse(response.body) - expect(a.class.to_s).to eq('Array') - expect(a.size).to eq(3) - - expect(a[0]['id']).to eq(info_request_events(:silly_comment_event).id) - expect(a[1]['id']).to eq(info_request_events(:useless_incoming_message_event).id) - expect(a[2]['id']).to eq(info_request_events(:useless_outgoing_message_event).id) - - expect(a[0]['info_request']['url_title']).to eq('why_do_you_have_such_a_fancy_dog') - expect(a[1]['info_request']['url_title']).to eq('why_do_you_have_such_a_fancy_dog') - expect(a[2]['info_request']['url_title']).to eq('why_do_you_have_such_a_fancy_dog') - - expect(a[0]['public_body']['url_name']).to eq('tgq') - expect(a[1]['public_body']['url_name']).to eq('tgq') - expect(a[2]['public_body']['url_name']).to eq('tgq') - - expect(a[0]['user']['url_name']).to eq('bob_smith') - expect(a[1]['user']['url_name']).to eq('bob_smith') - expect(a[2]['user']['url_name']).to eq('bob_smith') - - expect(a[0]['event_type']).to eq('comment') - expect(a[1]['event_type']).to eq('response') - expect(a[2]['event_type']).to eq('sent') - end - end - - describe "when tracking a public body" do - render_views - - before(:each) do - load_raw_emails_data - get_fixtures_xapian_index - end - - it "should work" do - geraldine = public_bodies(:geraldine_public_body) - get :track_public_body, :feed => 'feed', :url_name => geraldine.url_name - expect(response).to be_success - expect(response).to render_template('track/atom_feed') - tt = assigns[:track_thing] - expect(tt.public_body).to eq(geraldine) - expect(tt.track_type).to eq('public_body_updates') - expect(tt.track_query).to eq("requested_from:" + geraldine.url_name) - end - - it "should filter by event type" do - geraldine = public_bodies(:geraldine_public_body) - get :track_public_body, - :feed => 'feed', :url_name => geraldine.url_name, :event_type => 'sent' - expect(response).to be_success - expect(response).to render_template('track/atom_feed') - tt = assigns[:track_thing] - expect(tt.public_body).to eq(geraldine) - expect(tt.track_type).to eq('public_body_updates') - expect(tt.track_query).to eq("requested_from:" + geraldine.url_name + " variety:sent") - end - end - - describe 'POST delete_all_type' do + describe 'POST #delete_all_type' do let(:track_thing) { FactoryGirl.create(:search_track) } diff --git a/spec/factories/track_things.rb b/spec/factories/track_things.rb index 67d33491fd..ae8a940286 100644 --- a/spec/factories/track_things.rb +++ b/spec/factories/track_things.rb @@ -18,29 +18,46 @@ FactoryGirl.define do factory :track_thing do + track_medium 'email_daily' + track_query 'Example Query' + track_type 'search_query' association :tracking_user, :factory => :user - factory :search_track do - track_medium 'email_daily' - track_type 'search_query' - track_query 'Example Query' - end + factory :search_track factory :user_track do association :tracked_user, :factory => :user track_type 'user_updates' + after(:create) do |track_thing, evaluator| + track_thing.track_query = "requested_by:#{ user.url_name }" \ + " OR commented_by: #{ user.url_name }" + track_thing.save + end end factory :public_body_track do association :public_body, :factory => :public_body track_type 'public_body_updates' + after(:create) do |track_thing, evaluator| + track_thing.track_query = "requested_from:" \ + "#{ track_thing.public_body.url_name }" + track_thing.save + end end factory :request_update_track do association :info_request, :factory => :info_request track_type 'request_updates' + after(:create) do |track_thing, evaluator| + track_thing.track_query = "request:" \ + "#{ track_thing.info_request.url_title }" + track_thing.save + end end factory :successful_request_track do track_type 'all_successful_requests' + track_query 'variety:response ' \ + '(status:successful OR status:partially_successful)' end factory :new_request_track do track_type 'all_new_requests' + track_query 'variety:sent' end end diff --git a/spec/integration/alaveteli_dsl.rb b/spec/integration/alaveteli_dsl.rb index b3a82cc364..b8ff483c85 100644 --- a/spec/integration/alaveteli_dsl.rb +++ b/spec/integration/alaveteli_dsl.rb @@ -48,7 +48,7 @@ def alaveteli_session(session_id) def login(user) u = user.is_a?(User) ? user : users(user) alaveteli_session(u.id) do - visit signin_path + visit 'en/profile/sign_in' within '#signin_form' do fill_in "Your e-mail:", :with => u.email fill_in "Password:", :with => "jonespassword" diff --git a/spec/integration/track_alerts_spec.rb b/spec/integration/track_alerts_spec.rb new file mode 100644 index 0000000000..89b2a1f4ca --- /dev/null +++ b/spec/integration/track_alerts_spec.rb @@ -0,0 +1,84 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/alaveteli_dsl') + +describe "When sending track alerts" do + + it "should send alerts" do + + info_request = FactoryGirl.create(:info_request) + user = FactoryGirl.create(:user, :last_daily_track_email => 3.days.ago) + user_session = login(user) + using_session(user_session) do + visit "track/request/#{info_request.url_title}" + end + + other_user = FactoryGirl.create(:user) + other_user_session = login(other_user) + using_session(other_user_session) do + visit "en/annotate/request/#{info_request.url_title}" + fill_in "comment[body]", :with => 'test comment' + click_button 'Preview your annotation' + click_button 'Post annotation' + end + + rebuild_xapian_index + + TrackMailer.alert_tracks + + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(1) + mail = deliveries[0] + expect(mail.body).to match(/Alter your subscription/) + expect(mail.to_addrs.first.to_s).to include(user.email) + mail.body.to_s =~ /(http:\/\/.*\/c\/(.*))/ + mail_url = $1 + mail_token = $2 + + expect(mail.body).not_to match(/&/) + + expect(mail.body).not_to include('sent a request') # request not included + expect(mail.body).not_to include('sent a response') # response not included + expect(mail.body).to include('added an annotation') # comment included + + expect(mail.body).to match(/test comment/) # comment text included + + post_redirect = PostRedirect.find_by_email_token(mail_token) + expected_path = show_user_path(:url_name => user.url_name, + :anchor => "email_subscriptions") + expect(post_redirect.uri).to match(expected_path) + + # Check nothing more is delivered if we try again + deliveries.clear + TrackMailer.alert_tracks + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(0) + end + + it "should send localised alerts" do + info_request = FactoryGirl.create(:info_request) + user = FactoryGirl.create(:user, :last_daily_track_email => 3.days.ago, + :locale => 'es') + user_session = login(user) + using_session(user_session) do + visit "es/track/request/#{info_request.url_title}" + end + + other_user = FactoryGirl.create(:user, :locale => 'en') + other_user_session = login(other_user) + using_session(other_user_session) do + visit "annotate/request/#{info_request.url_title}" + fill_in "comment[body]", :with => 'test comment' + click_button 'Preview your annotation' + click_button 'Post annotation' + end + + rebuild_xapian_index + + TrackMailer.alert_tracks + deliveries = ActionMailer::Base.deliveries + mail = deliveries[0] + expect(mail.body).to include('el equipo de ') + end +end + From 0338f6b5ddbb4949243000c38be4c093dfbd25bb Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 14:35:08 +0000 Subject: [PATCH 065/311] Raise RecordNotFound on attempt to track embargoed requests. This should be the same behaviour as happens on an attempt to track a non-existent request. --- app/controllers/track_controller.rb | 4 +++- spec/controllers/track_controller_spec.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/controllers/track_controller.rb b/app/controllers/track_controller.rb index 721d95546d..f9133332b0 100644 --- a/app/controllers/track_controller.rb +++ b/app/controllers/track_controller.rb @@ -11,7 +11,9 @@ class TrackController < ApplicationController # Track all updates to a particular request def track_request - @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + @info_request = InfoRequest + .not_embargoed + .find_by_url_title!(params[:url_title]) @track_thing = TrackThing.create_track_for_request(@info_request) return atom_feed_internal if params[:feed] == 'feed' diff --git a/spec/controllers/track_controller_spec.rb b/spec/controllers/track_controller_spec.rb index cd67b56272..62a9a8a8a1 100644 --- a/spec/controllers/track_controller_spec.rb +++ b/spec/controllers/track_controller_spec.rb @@ -64,6 +64,14 @@ .to raise_error(ActiveRecord::RecordNotFound) end + it "should 404 for embargoed requests" do + session[:user_id] = user.id + embargoed_request = FactoryGirl.create(:embargoed_request) + expect { get :track_request, :url_title => embargoed_request.url_title, + :feed => 'track' } + .to raise_error(ActiveRecord::RecordNotFound) + end + context 'when getting feeds' do before do From 114c57dff27512427fe805147d96e7b3214e8e91 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 14:55:40 +0000 Subject: [PATCH 066/311] Cleanup specs, use standard descriptions. --- .../widget_votes_controller_spec.rb | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/controllers/widget_votes_controller_spec.rb b/spec/controllers/widget_votes_controller_spec.rb index 04d96f6e29..4ab91ee000 100644 --- a/spec/controllers/widget_votes_controller_spec.rb +++ b/spec/controllers/widget_votes_controller_spec.rb @@ -5,21 +5,21 @@ include LinkToHelper - describe 'POST create' do + describe 'POST #create' do + let(:info_request){ FactoryGirl.create(:info_request) } before do - @info_request = FactoryGirl.create(:info_request) allow(AlaveteliConfiguration).to receive(:enable_widgets).and_return(true) end it 'should find the info request' do - post :create, :request_id => @info_request.id - expect(assigns[:info_request]).to eq(@info_request) + post :create, :request_id => info_request.id + expect(assigns[:info_request]).to eq(info_request) end it 'should redirect to the track path for the info request' do - post :create, :request_id => @info_request.id - track_thing = TrackThing.create_track_for_request(@info_request) + post :create, :request_id => info_request.id + track_thing = TrackThing.create_track_for_request(info_request) expect(response).to redirect_to(do_track_path(track_thing)) end @@ -27,17 +27,17 @@ it 'sets a tracking cookie' do allow(SecureRandom).to receive(:hex).and_return(mock_cookie) - post :create, :request_id => @info_request.id + post :create, :request_id => info_request.id expect(cookies[:widget_vote]).to eq(mock_cookie) end it 'creates a widget vote' do allow(SecureRandom).to receive(:hex).and_return(mock_cookie) - votes = @info_request. + votes = info_request. widget_votes. where(:cookie => mock_cookie) - post :create, :request_id => @info_request.id + post :create, :request_id => info_request.id expect(votes.size).to eq(1) end @@ -48,17 +48,17 @@ it 'retains the existing tracking cookie' do request.cookies['widget_vote'] = mock_cookie - post :create, :request_id => @info_request.id + post :create, :request_id => info_request.id expect(cookies[:widget_vote]).to eq(mock_cookie) end it 'creates a widget vote' do request.cookies['widget_vote'] = mock_cookie - votes = @info_request. + votes = info_request. widget_votes. where(:cookie => mock_cookie) - post :create, :request_id => @info_request.id + post :create, :request_id => info_request.id expect(votes.size).to eq(1) end @@ -69,7 +69,7 @@ it 'raises ActiveRecord::RecordNotFound' do allow(AlaveteliConfiguration).to receive(:enable_widgets).and_return(false) - expect{ post :create, :request_id => @info_request.id }. + expect{ post :create, :request_id => info_request.id }. to raise_error(ActiveRecord::RecordNotFound) end @@ -78,9 +78,9 @@ context "when the request's prominence is not 'normal'" do it 'should return a 403' do - @info_request.prominence = 'hidden' - @info_request.save! - post :create, :request_id => @info_request.id + info_request.prominence = 'hidden' + info_request.save! + post :create, :request_id => info_request.id expect(response.code).to eq("403") end From 42b7b11884cedc6deaf8f606e7625cd423e1abdf Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 15:07:40 +0000 Subject: [PATCH 067/311] Raise RecordNotFound on WidgetVoteController#create with an embargoed request. This should be the same response as when a non-existent request id is used. --- app/controllers/widget_votes_controller.rb | 2 +- spec/controllers/widget_votes_controller_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/widget_votes_controller.rb b/app/controllers/widget_votes_controller.rb index 27ea4326c7..360bf9ea56 100644 --- a/app/controllers/widget_votes_controller.rb +++ b/app/controllers/widget_votes_controller.rb @@ -39,7 +39,7 @@ def check_widget_config end def find_info_request - @info_request = InfoRequest.find(params[:request_id]) + @info_request = InfoRequest.not_embargoed.find(params[:request_id]) end def check_prominence diff --git a/spec/controllers/widget_votes_controller_spec.rb b/spec/controllers/widget_votes_controller_spec.rb index 4ab91ee000..86c4033b15 100644 --- a/spec/controllers/widget_votes_controller_spec.rb +++ b/spec/controllers/widget_votes_controller_spec.rb @@ -86,6 +86,15 @@ end + context 'when the request is embargoed' do + + it 'should raise an ActiveRecord::RecordNotFound error' do + embargoed_request = FactoryGirl.create(:embargoed_request) + expect{ + post :create, :request_id => embargoed_request.id + }.to raise_error ActiveRecord::RecordNotFound + end + end end end From 0ee6fbe1ff35cefeff200ccfde0c8487e33cafd6 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 15:12:37 +0000 Subject: [PATCH 068/311] Standard controller action descriptions. --- spec/controllers/followups_controller_spec.rb | 6 +++--- spec/controllers/request_controller_spec.rb | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/controllers/followups_controller_spec.rb b/spec/controllers/followups_controller_spec.rb index 8cfeacd013..50b25d3c27 100644 --- a/spec/controllers/followups_controller_spec.rb +++ b/spec/controllers/followups_controller_spec.rb @@ -8,7 +8,7 @@ let(:request) { FactoryGirl.create(:info_request_with_incoming, :user => request_user) } let(:message_id) { request.incoming_messages[0].id } - describe "GET new" do + describe "GET #new" do it 'raises an ActiveRecord::RecordNotFound error for an embargoed request' do embargoed_request = FactoryGirl.create(:embargoed_request) @@ -126,7 +126,7 @@ end - describe "POST preview" do + describe "POST #preview" do let(:dummy_message) do { :body => "What a useless response! You suck.", @@ -193,7 +193,7 @@ end - describe "POST create" do + describe "POST #create" do let(:dummy_message) do { :body => "What a useless response! You suck.", diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index a2da80ec7c..ef381258a3 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -2640,7 +2640,7 @@ def make_request describe RequestController do - describe 'GET details' do + describe 'GET #details' do let(:info_request){ FactoryGirl.create(:info_request)} @@ -2702,7 +2702,7 @@ def make_request end describe RequestController do - describe 'GET describe_state_message' do + describe 'GET #describe_state_message' do let(:info_request){ FactoryGirl.create(:info_request_with_incoming) } it 'assigns the info_request to the view' do @@ -2747,7 +2747,7 @@ def make_request describe RequestController do - describe 'GET download_entire_request' do + describe 'GET #download_entire_request' do context 'when the request is embargoed' do let(:info_request){ FactoryGirl.create(:embargoed_request) } @@ -2762,7 +2762,7 @@ def make_request describe RequestController do - describe 'GET show_request_event' do + describe 'GET #show_request_event' do context 'when the event is an incoming message' do let(:event){ FactoryGirl.create(:response_event) } From f513163a6c05dade93d164e2728ce460edd075d8 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 7 Nov 2016 12:07:49 +0000 Subject: [PATCH 069/311] Provide a hook in the signin process for overriding pro PostRedirects --- app/controllers/application_controller.rb | 25 ++++++++++++++++++--- app/controllers/user_controller.rb | 4 ++-- spec/controllers/request_controller_spec.rb | 4 ++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a60667f604..8fc0454ead 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -300,11 +300,17 @@ def authenticated_user # Do a POST redirect. This is a nasty hack - we store the posted values in # the session, and when the GET redirect with "?post_redirect=1" happens, # load them in. - def do_post_redirect(post_redirect) + def do_post_redirect(post_redirect, user=nil) uri = URI.parse(post_redirect.uri).path - + if feature_enabled?(:alaveteli_pro) && user && user.pro? + uri = override_post_redirect_for_pro(uri, post_redirect, user) + end session[:post_redirect_token] = post_redirect.token + uri = add_post_redirect_param_to_uri(uri) + redirect_to uri + end + def add_post_redirect_param_to_uri(uri) # TODO: what is the built in Ruby URI munging function that can do this # choice of & vs. ? more elegantly than this dumb if statement? if uri.include?("?") @@ -322,7 +328,20 @@ def do_post_redirect(post_redirect) uri += "?post_redirect=1" end end - redirect_to uri + return uri + end + + # A hook for us to override certain post redirects for pro users, e.g. + # if they start making a request, then we realise they're a pro when they + # log in, so we want to send them into the pro system + def override_post_redirect_for_pro(uri, post_redirect, user) + case uri + when "/new" + # TODO: Send this to the new 'draft' controller action for pros + # instead. I guess we could just update the uri, but we might need to + # tweak the saved params too. + end + return uri end # If we are in a faked redirect to POST request, then set post params. diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index e2b31e38ff..59a00b19d9 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -127,7 +127,7 @@ def signin if is_modal_dialog render :action => 'signin_successful' else - do_post_redirect @post_redirect + do_post_redirect @post_redirect, @user_signin end else send_confirmation_mail @user_signin @@ -231,7 +231,7 @@ def confirm session[:user_circumstance] = post_redirect.circumstance - do_post_redirect post_redirect + do_post_redirect post_redirect, @user end def signout diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index ef381258a3..3f18768efc 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -1375,6 +1375,10 @@ def expect_hidden(hidden_template) end + describe 'when a pro user uses the request form and then logs in' do + it 'should do what we decide is appropriate' + end + end # These go with the previous set, but use mocks instead of fixtures. From cada4623386403bf255c757988028f544648061a Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 8 Nov 2016 12:51:35 +0000 Subject: [PATCH 070/311] Add a base controller for alaveteli_pro with auth checking --- .../alaveteli_pro/base_controller.rb | 34 +++++++++++ .../alaveteli_pro/base_controller_spec.rb | 58 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 app/controllers/alaveteli_pro/base_controller.rb create mode 100644 spec/controllers/alaveteli_pro/base_controller_spec.rb diff --git a/app/controllers/alaveteli_pro/base_controller.rb b/app/controllers/alaveteli_pro/base_controller.rb new file mode 100644 index 0000000000..1a6d8e584d --- /dev/null +++ b/app/controllers/alaveteli_pro/base_controller.rb @@ -0,0 +1,34 @@ +# -*- encoding : utf-8 -*- +# app/controllers/alaveteli_pro/base_controller.rb +# Base controller for other controllers in the alaveteli_pro module. +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ + +class AlaveteliPro::BaseController < ApplicationController + before_filter :pro_user_authenticated? + + # A pro-specific version of user_authenticated? that pro controller actions + # can use to check for (or force a login for) an authenticated pro user + def pro_user_authenticated?(reason_params = nil) + if reason_params.nil? + reason_params = { + web: _("To access Alaveteli Professional"), + email: _("Then you can access Alaveteli Professional") + } + end + if authenticated?(reason_params) + unless current_user.pro? + redirect_to( + frontpage_path, + flash: { + notice: _("This page is only accessible to Alaveteli " \ + "Professional users") + } + ) + end + return true + end + return false + end +end diff --git a/spec/controllers/alaveteli_pro/base_controller_spec.rb b/spec/controllers/alaveteli_pro/base_controller_spec.rb new file mode 100644 index 0000000000..158a2ff218 --- /dev/null +++ b/spec/controllers/alaveteli_pro/base_controller_spec.rb @@ -0,0 +1,58 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') + +describe AlaveteliPro::BaseController do + controller(AlaveteliPro::BaseController) do + def index + render :nothing => true + end + end + + describe "#pro_user_authenticated?" do + # Testing the fact that every controller action inherits the before_filter + # of pro_user_authenticated? + + before do + allow(controller).to receive(:feature_enabled?).with(:alaveteli_pro).and_return(true) + end + + context "when the user is not logged in" do + it "redirects to the signin path" do + get :index + expect(@response.redirect_url).to match(/http:\/\/test\.host\/profile\/sign_in/) + end + end + + context "when the user is logged in but not a pro" do + let(:user) { FactoryGirl.create(:user) } + + before do + session[:user_id] = user.id + end + + it "redirects to the homepage" do + get :index + expect(@response).to redirect_to frontpage_path + end + + it "sets a flash notice to inform the user they're not a pro" do + get :index + expect(flash[:notice]).to eq "This page is only accessible to " \ + "Alaveteli Professional users" + end + end + + context "when the user is logged in and is a pro" do + let(:user) { FactoryGirl.create(:pro_user) } + + before do + session[:user_id] = user.id + end + + it "doesn't redirect anywhere" do + get :index + expect(@response.status).to be 200 + end + end + end +end From fa17179c41a574c9e328fa651e1ad62d05381178 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 8 Nov 2016 12:52:03 +0000 Subject: [PATCH 071/311] Add a really basic dashboard controller for alaveteli pro --- .../alaveteli_pro/dashboard_controller.rb | 12 ++++++++++ .../alaveteli_pro/dashboard/index.html.erb | 2 ++ config/routes.rb | 12 ++++++++++ .../dashboard_controller_spec.rb | 22 +++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 app/controllers/alaveteli_pro/dashboard_controller.rb create mode 100644 app/views/alaveteli_pro/dashboard/index.html.erb create mode 100644 spec/controllers/alaveteli_pro/dashboard_controller_spec.rb diff --git a/app/controllers/alaveteli_pro/dashboard_controller.rb b/app/controllers/alaveteli_pro/dashboard_controller.rb new file mode 100644 index 0000000000..3b59af0767 --- /dev/null +++ b/app/controllers/alaveteli_pro/dashboard_controller.rb @@ -0,0 +1,12 @@ +# -*- encoding : utf-8 -*- +# app/controllers/alaveteli_pro/dashboard_controller.rb +# Dashboard controller, for pro user dashboards. +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ + +class AlaveteliPro::DashboardController < AlaveteliPro::BaseController + def index + @user = current_user + end +end diff --git a/app/views/alaveteli_pro/dashboard/index.html.erb b/app/views/alaveteli_pro/dashboard/index.html.erb new file mode 100644 index 0000000000..9435f616be --- /dev/null +++ b/app/views/alaveteli_pro/dashboard/index.html.erb @@ -0,0 +1,2 @@ +

Your dashboard

+

Welcome to your dashboard <%= @user.name %>

diff --git a/config/routes.rb b/config/routes.rb index 988286ef80..c50d6ddd34 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,8 @@ # Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ +include AlaveteliFeatures::Constraints + # Allow easy extension from themes. Note these will have the highest priority. $alaveteli_route_extensions.each do |f| load File.join('config', f) @@ -592,5 +594,15 @@ :feed_type => '^(json|atom)$' #### + #### Alaveteli Pro + constraints FeatureConstraint.new(:alaveteli_pro) do + namespace :alaveteli_pro do + match '/' => 'dashboard#index', + :as => 'dashboard', + :via => :get + end + end + #### + filter :conditionallyprependlocale end diff --git a/spec/controllers/alaveteli_pro/dashboard_controller_spec.rb b/spec/controllers/alaveteli_pro/dashboard_controller_spec.rb new file mode 100644 index 0000000000..cd402c1d27 --- /dev/null +++ b/spec/controllers/alaveteli_pro/dashboard_controller_spec.rb @@ -0,0 +1,22 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') + +describe AlaveteliPro::DashboardController do + describe "#index" do + let(:user) { FactoryGirl.create(:pro_user) } + + before do + session[:user_id] = user.id + end + + it "exists" do + get :index + expect(@response.status).to be 200 + end + + it "sets @user" do + get :index + expect(assigns[:user]).to eq user + end + end +end From df8a20359b791d189a3773478dd975cfa1658660 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 8 Nov 2016 13:52:57 +0000 Subject: [PATCH 072/311] Redirect pro users to dashboard from the homepage --- app/controllers/general_controller.rb | 10 ++++++++++ spec/controllers/general_controller_spec.rb | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/app/controllers/general_controller.rb b/app/controllers/general_controller.rb index c5b6941541..20d46dfba8 100644 --- a/app/controllers/general_controller.rb +++ b/app/controllers/general_controller.rb @@ -12,6 +12,8 @@ class GeneralController < ApplicationController MAX_RESULTS = 500 + before_filter :redirect_pros_to_dashboard, only: :frontpage + # New, improved front page! def frontpage medium_cache @@ -234,4 +236,12 @@ def version }} end end + + private + + def redirect_pros_to_dashboard + if feature_enabled?(:alaveteli_pro) && current_user && current_user.pro? + redirect_to alaveteli_pro_dashboard_path + end + end end diff --git a/spec/controllers/general_controller_spec.rb b/spec/controllers/general_controller_spec.rb index a0b4e9681b..60164f03c3 100644 --- a/spec/controllers/general_controller_spec.rb +++ b/spec/controllers/general_controller_spec.rb @@ -256,6 +256,19 @@ end + describe 'when handling pro users' do + before do + @user = FactoryGirl.create(:pro_user) + session[:user_id] = @user.id + allow(controller).to receive(:feature_enabled?).with(:alaveteli_pro).and_return(true) + end + + it 'should redirect pro users to the pro dashboard' do + get :frontpage + expect(@response).to redirect_to alaveteli_pro_dashboard_path + end + end + end From a1377b4042e09da449ef06e6f3311cf27fb00b5d Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 15 Nov 2016 21:49:34 +0000 Subject: [PATCH 073/311] Add a routing constraint object to AlaveteliFeatures --- .../lib/alaveteli_features.rb | 16 ++++++++----- .../lib/alaveteli_features/constraints.rb | 15 ++++++++++++ .../constraints/feature_constraint_spec.rb | 23 +++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 gems/alaveteli_features/lib/alaveteli_features/constraints.rb create mode 100644 gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb diff --git a/gems/alaveteli_features/lib/alaveteli_features.rb b/gems/alaveteli_features/lib/alaveteli_features.rb index 89793fc865..c10c6b1395 100644 --- a/gems/alaveteli_features/lib/alaveteli_features.rb +++ b/gems/alaveteli_features/lib/alaveteli_features.rb @@ -1,5 +1,6 @@ require "alaveteli_features/version" require "alaveteli_features/helpers" +require "alaveteli_features/constraints" require "alaveteli_features/railtie" if defined?(Rails) require "flipper" require "flipper-active_record" @@ -7,14 +8,17 @@ module AlaveteliFeatures def self.backend return @backend if @backend - if ActiveRecord::Base.connection.table_exists? :flipper_features + if ActiveRecord::Base.connected? && \ + ActiveRecord::Base.connection.table_exists?(:flipper_features) @backend = Flipper.new(Flipper::Adapters::ActiveRecord.new) else - Rails.logger.warn "No database tables found for feature flags, you " \ - "might need to set a backend explicitly if you " \ - "don't want them stored in a database, or run" \ - "rake db:migrate to create the table." - Rails.logger.warn "Using memory-based feature storage instead." + if defined?(Rails) + Rails.logger.warn "No database tables found for feature flags, you " \ + "might need to set a backend explicitly if you " \ + "don't want them stored in a database, or run" \ + "rake db:migrate to create the table." + Rails.logger.warn "Using memory-based feature storage instead." + end require 'flipper/adapters/memory' @backend = Flipper.new(Flipper::Adapters::Memory.new) end diff --git a/gems/alaveteli_features/lib/alaveteli_features/constraints.rb b/gems/alaveteli_features/lib/alaveteli_features/constraints.rb new file mode 100644 index 0000000000..ba091e0aa1 --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features/constraints.rb @@ -0,0 +1,15 @@ +module AlaveteliFeatures + module Constraints + class FeatureConstraint + include AlaveteliFeatures::Helpers + + def initialize(feature) + @feature = feature + end + + def matches?(request) + return feature_enabled? @feature + end + end + end +end \ No newline at end of file diff --git a/gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb b/gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb new file mode 100644 index 0000000000..00f957a811 --- /dev/null +++ b/gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe AlaveteliFeatures::Constraints::FeatureConstraint do + let(:test_backend) { Flipper.new(Flipper::Adapters::Memory.new) } + + before do + AlaveteliFeatures.backend = test_backend + end + + describe "#matches?" do + it "should return true when a feature is enabled" do + constraint = AlaveteliFeatures::Constraints::FeatureConstraint.new(:feature) + allow(constraint).to receive(:feature_enabled?).with(:feature).and_return(true) + expect(constraint.matches?(nil)).to be true + end + + it "should return false when a feature is disabled" do + constraint = AlaveteliFeatures::Constraints::FeatureConstraint.new(:feature) + allow(constraint).to receive(:feature_enabled?).with(:feature).and_return(false) + expect(constraint.matches?(nil)).to be false + end + end +end \ No newline at end of file From 9268e655fd4348095ca25dd468b5cdd50db3826c Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 17 Nov 2016 18:07:32 +0000 Subject: [PATCH 074/311] Ruby style fixups in the AlaveteliFeatures gem --- gems/alaveteli_features/lib/alaveteli_features/constraints.rb | 2 +- gems/alaveteli_features/lib/alaveteli_features/helpers.rb | 2 +- gems/alaveteli_features/lib/alaveteli_features/railtie.rb | 2 +- .../spec/constraints/feature_constraint_spec.rb | 2 +- gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gems/alaveteli_features/lib/alaveteli_features/constraints.rb b/gems/alaveteli_features/lib/alaveteli_features/constraints.rb index ba091e0aa1..177a15d685 100644 --- a/gems/alaveteli_features/lib/alaveteli_features/constraints.rb +++ b/gems/alaveteli_features/lib/alaveteli_features/constraints.rb @@ -12,4 +12,4 @@ def matches?(request) end end end -end \ No newline at end of file +end diff --git a/gems/alaveteli_features/lib/alaveteli_features/helpers.rb b/gems/alaveteli_features/lib/alaveteli_features/helpers.rb index 6e9f769c7d..cd12a8d6c8 100644 --- a/gems/alaveteli_features/lib/alaveteli_features/helpers.rb +++ b/gems/alaveteli_features/lib/alaveteli_features/helpers.rb @@ -4,4 +4,4 @@ def feature_enabled?(feature, *args) AlaveteliFeatures.backend.enabled?(feature, *args) end end -end \ No newline at end of file +end diff --git a/gems/alaveteli_features/lib/alaveteli_features/railtie.rb b/gems/alaveteli_features/lib/alaveteli_features/railtie.rb index 138bedadee..7046f1ba00 100644 --- a/gems/alaveteli_features/lib/alaveteli_features/railtie.rb +++ b/gems/alaveteli_features/lib/alaveteli_features/railtie.rb @@ -14,4 +14,4 @@ class Railtie < Rails::Railtie require 'alaveteli_features/generators/alaveteli_features/install/install_generator' end end -end \ No newline at end of file +end diff --git a/gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb b/gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb index 00f957a811..a0bba6b5f7 100644 --- a/gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb +++ b/gems/alaveteli_features/spec/constraints/feature_constraint_spec.rb @@ -20,4 +20,4 @@ expect(constraint.matches?(nil)).to be false end end -end \ No newline at end of file +end diff --git a/gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb b/gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb index 234de2b96f..b40a15a7fb 100644 --- a/gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb +++ b/gems/alaveteli_features/spec/helpers/feature_enabled_spec.rb @@ -59,4 +59,4 @@ def admin? instance.feature_enabled?(:test_feature, user1) end end -end \ No newline at end of file +end From caabe61dd75ad3336a3a7021bcf928b955f177f7 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 9 Nov 2016 09:59:28 +0000 Subject: [PATCH 075/311] Add a cancan ability for accessing alaveteli pro generally --- app/models/ability.rb | 8 +++++++ spec/models/ability_spec.rb | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/app/models/ability.rb b/app/models/ability.rb index d534618358..7597f73fe0 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,5 +1,6 @@ class Ability include CanCan::Ability + include AlaveteliFeatures::Helpers def initialize(user) # Define abilities for the passed in user here. For example: @@ -45,6 +46,13 @@ def initialize(user) can :read, InfoRequest do |request| self.class.can_view_with_prominence?(request.prominence, request, user) end + + # Accessing alaveteli professional + if feature_enabled? :alaveteli_pro + if user && (user.super? || user.pro?) + can :access, :alaveteli_pro + end + end end private diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index aae7edfc32..bac4098c1d 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -215,4 +215,47 @@ end end end + + describe "accessing Alaveteli Pro" do + subject(:ability) { Ability.new(user) } + + context "when the user is a pro" do + let(:user) { FactoryGirl.create(:pro_user) } + it "should return true" do + with_feature_enabled(:alaveteli_pro) do + expect(ability).to be_able_to(:access, :alaveteli_pro) + end + end + end + + context "when the user is an admin" do + let(:user) { FactoryGirl.create(:admin_user) } + + it "should return true" do + with_feature_enabled(:alaveteli_pro) do + expect(ability).to be_able_to(:access, :alaveteli_pro) + end + end + end + + context "when the user is a normal user" do + let(:user) { FactoryGirl.create(:user) } + + it "should return false" do + with_feature_enabled(:alaveteli_pro) do + expect(ability).not_to be_able_to(:access, :alaveteli_pro) + end + end + end + + context "when the user is nil" do + let(:user) { nil } + + it "should return false" do + with_feature_enabled(:alaveteli_pro) do + expect(ability).not_to be_able_to(:access, :alaveteli_pro) + end + end + end + end end From 05ddb9051fbe6f63163775d48f4306d9b0266eb7 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 16 Nov 2016 10:39:08 +0000 Subject: [PATCH 076/311] Add some spec helper methods for testing with features --- .../lib/alaveteli_features/spec_helpers.rb | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb diff --git a/gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb b/gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb new file mode 100644 index 0000000000..63fceac4ee --- /dev/null +++ b/gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb @@ -0,0 +1,21 @@ +module AlaveteliFeatures + module SpecHelpers + def with_feature_enabled(feature) + allow(AlaveteliFeatures.backend). + to receive(:enabled?).with(feature).and_return(true) + yield + ensure + allow(AlaveteliFeatures.backend). + to receive(:enabled?).with(feature).and_call_original + end + + def with_feature_disabled(feature) + allow(AlaveteliFeatures.backend). + to receive(:enabled?).with(feature).and_return(false) + yield + ensure + allow(AlaveteliFeatures.backend). + to receive(:enabled?).with(feature).and_call_original + end + end +end \ No newline at end of file From 16abae1b51ab9fad9c81835e34bda00f6d36a364 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 16 Nov 2016 10:40:13 +0000 Subject: [PATCH 077/311] Include AlaveteliFeatures::SpecHelpers in spec helper --- spec/spec_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 08b2cdbbe4..aa99d40b29 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,7 @@ require 'rubygems' require 'simplecov' require 'coveralls' +require "alaveteli_features/spec_helpers" cov_formats = [Coveralls::SimpleCov::Formatter] cov_formats << SimpleCov::Formatter::HTMLFormatter if ENV['COVERAGE'] == 'local' @@ -126,6 +127,9 @@ end end +# Helper with_xxx methods for working with feature flags +include AlaveteliFeatures::SpecHelpers + # Use the before create job hook to simulate a race condition with # another process by creating an acts_as_xapian_job record for the # same model: From 36978fef0f99f2ddd6fac4f3b9d2e9f26b4a2bed Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 25 Nov 2016 20:42:41 +0000 Subject: [PATCH 078/311] Only override the named feature in spec helper Before, checking for other features would raise an error in rspec because they hadn't been explicitly allowed, now anything other feature will be passed through to the original method on the backend. --- .../lib/alaveteli_features/spec_helpers.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb b/gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb index 63fceac4ee..7fb5e65568 100644 --- a/gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb +++ b/gems/alaveteli_features/lib/alaveteli_features/spec_helpers.rb @@ -1,21 +1,17 @@ module AlaveteliFeatures module SpecHelpers def with_feature_enabled(feature) + allow(AlaveteliFeatures.backend).to receive(:enabled?).and_call_original allow(AlaveteliFeatures.backend). to receive(:enabled?).with(feature).and_return(true) yield - ensure - allow(AlaveteliFeatures.backend). - to receive(:enabled?).with(feature).and_call_original end def with_feature_disabled(feature) + allow(AlaveteliFeatures.backend).to receive(:enabled?).and_call_original allow(AlaveteliFeatures.backend). to receive(:enabled?).with(feature).and_return(false) yield - ensure - allow(AlaveteliFeatures.backend). - to receive(:enabled?).with(feature).and_call_original end end -end \ No newline at end of file +end From aa725c2a6e2835fee69d66ab0d728fa52a7267e9 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 28 Nov 2016 15:06:07 +0000 Subject: [PATCH 079/311] Annotate MailServerLog model and factory --- app/models/mail_server_log.rb | 1 + spec/factories/mail_server_logs.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/models/mail_server_log.rb b/app/models/mail_server_log.rb index 18d3ddfff9..072abb56fa 100644 --- a/app/models/mail_server_log.rb +++ b/app/models/mail_server_log.rb @@ -1,5 +1,6 @@ # -*- encoding : utf-8 -*- # == Schema Information +# Schema version: 20161128095350 # # Table name: mail_server_logs # diff --git a/spec/factories/mail_server_logs.rb b/spec/factories/mail_server_logs.rb index 7642cb85e3..ec3abbff1a 100644 --- a/spec/factories/mail_server_logs.rb +++ b/spec/factories/mail_server_logs.rb @@ -1,5 +1,6 @@ # -*- encoding : utf-8 -*- # == Schema Information +# Schema version: 20161128095350 # # Table name: mail_server_logs # From fee0ccc1093d677680911fd1ee4c7a493f7a65e0 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 25 Nov 2016 20:42:32 +0000 Subject: [PATCH 080/311] Tidy up formatting in User factory --- spec/factories/users.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 34625f35fa..625c05db82 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -43,10 +43,12 @@ hashed_password '6b7cd45a5f35fd83febc0452a799530398bfb6e8' # jonespassword email_confirmed true ban_text "" + factory :admin_user do name 'Admin User' admin_level 'super' end + factory :pro_user do name 'Pro User' after(:create) do |user, evaluator| From fb11609ca3bc0d8373a54ae040ed66f2033370ef Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 28 Nov 2016 18:18:00 +0000 Subject: [PATCH 081/311] Add a duration field to Embargo --- app/models/embargo.rb | 40 ++++++++++++++++--- .../20161128095350_add_duration_to_embargo.rb | 5 +++ spec/factories/embargos.rb | 2 + spec/models/embargo_spec.rb | 39 ++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20161128095350_add_duration_to_embargo.rb diff --git a/app/models/embargo.rb b/app/models/embargo.rb index 5f91c1d762..7887e08e40 100644 --- a/app/models/embargo.rb +++ b/app/models/embargo.rb @@ -1,16 +1,44 @@ # -*- encoding : utf-8 -*- # == Schema Information +# Schema version: 20161128095350 # -# Table name: embargos +# Table name: embargoes # -# id :integer not null, primary key -# info_request_id :integer not null -# publish_at :datetime not null -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# info_request_id :integer +# publish_at :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# embargo_duration :string(255) # class Embargo < ActiveRecord::Base belongs_to :info_request validates_presence_of :info_request + validates_presence_of :publish_at + validates_inclusion_of :embargo_duration, + in: lambda { |e| e.allowed_durations }, + allow_nil: true + after_initialize :set_publish_at_from_duration + + DURATIONS = { + "3_months" => Proc.new { 3.months }, + "6_months" => Proc.new { 6.months }, + "12_months" => Proc.new { 12.months } + }.freeze + + def allowed_durations + DURATIONS.keys + end + + def duration_as_duration + DURATIONS[self.embargo_duration].call + end + + def set_publish_at_from_duration + unless self.publish_at.present? || self.embargo_duration.blank? + self.publish_at = Time.zone.today + duration_as_duration + end + end + end diff --git a/db/migrate/20161128095350_add_duration_to_embargo.rb b/db/migrate/20161128095350_add_duration_to_embargo.rb new file mode 100644 index 0000000000..6abd2655e4 --- /dev/null +++ b/db/migrate/20161128095350_add_duration_to_embargo.rb @@ -0,0 +1,5 @@ +class AddDurationToEmbargo < ActiveRecord::Migration + def change + add_column :embargoes, :embargo_duration, :string + end +end diff --git a/spec/factories/embargos.rb b/spec/factories/embargos.rb index e87b104f52..79e86e1063 100644 --- a/spec/factories/embargos.rb +++ b/spec/factories/embargos.rb @@ -8,11 +8,13 @@ # publish_at :datetime not null # created_at :datetime not null # updated_at :datetime not null +# embargo_duration :string(255) # FactoryGirl.define do factory :embargo do info_request publish_at Time.now + 3.months + embargo_duration "3_months" end end diff --git a/spec/models/embargo_spec.rb b/spec/models/embargo_spec.rb index f0c44c4069..7d519d6f94 100644 --- a/spec/models/embargo_spec.rb +++ b/spec/models/embargo_spec.rb @@ -23,4 +23,43 @@ expect(embargo.publish_at).to be_a(ActiveSupport::TimeWithZone) end + it 'requires a publish_at field' do + embargo.publish_at = nil + expect(embargo).not_to be_valid + end + + it 'has an embargo_duration field' do + expect(embargo.embargo_duration).to be_a(String) + end + + it 'validates embargo_duration field is in list' do + embargo.allowed_durations.each do |duration| + embargo.embargo_duration = duration + expect(embargo).to be_valid + end + embargo.embargo_duration = "not_in_list" + expect(embargo).not_to be_valid + end + + it 'allows embargo_duration to be nil' do + embargo.embargo_duration = nil + expect(embargo).to be_valid + end + + describe 'setting publish_at' do + let(:info_request) { FactoryGirl.create(:info_request) } + + it 'sets publish_at from duration during creation' do + embargo = Embargo.create(info_request: info_request, + embargo_duration: "3_months") + expect(embargo.publish_at).to eq Time.zone.today + 3.months + end + + it 'doesnt set publish_at from duration if its already set' do + embargo = Embargo.create(info_request: info_request, + publish_at: Time.zone.today, + embargo_duration: "3_months") + expect(embargo.publish_at).to eq Time.zone.today + end + end end From d4bee681042bc4c73aea1b95379869acce6d0679 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 25 Nov 2016 20:39:31 +0000 Subject: [PATCH 082/311] Add a draft info request model --- app/models/draft_info_request.rb | 23 +++++++++++++++ ...161116121007_create_draft_info_requests.rb | 13 +++++++++ spec/factories/draft_info_requests.rb | 28 +++++++++++++++++++ spec/models/draft_info_request_spec.rb | 28 +++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 app/models/draft_info_request.rb create mode 100644 db/migrate/20161116121007_create_draft_info_requests.rb create mode 100644 spec/factories/draft_info_requests.rb create mode 100644 spec/models/draft_info_request_spec.rb diff --git a/app/models/draft_info_request.rb b/app/models/draft_info_request.rb new file mode 100644 index 0000000000..a46362c0e1 --- /dev/null +++ b/app/models/draft_info_request.rb @@ -0,0 +1,23 @@ +# == Schema Information +# Schema version: 20161128095350 +# +# Table name: draft_info_requests +# +# id :integer not null, primary key +# title :string(255) +# user_id :integer +# public_body_id :integer +# body :text +# embargo_duration :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# + +class DraftInfoRequest < ActiveRecord::Base + validates_presence_of :user + + belongs_to :user + belongs_to :public_body + + strip_attributes +end diff --git a/db/migrate/20161116121007_create_draft_info_requests.rb b/db/migrate/20161116121007_create_draft_info_requests.rb new file mode 100644 index 0000000000..b63ec1d436 --- /dev/null +++ b/db/migrate/20161116121007_create_draft_info_requests.rb @@ -0,0 +1,13 @@ +class CreateDraftInfoRequests < ActiveRecord::Migration + def change + create_table :draft_info_requests do |t| + t.string :title + t.integer :user_id + t.integer :public_body_id + t.text :body + t.string :embargo_duration + + t.timestamps + end + end +end diff --git a/spec/factories/draft_info_requests.rb b/spec/factories/draft_info_requests.rb new file mode 100644 index 0000000000..1b5ba939d8 --- /dev/null +++ b/spec/factories/draft_info_requests.rb @@ -0,0 +1,28 @@ +# == Schema Information +# Schema version: 20161128095350 +# +# Table name: draft_info_requests +# +# id :integer not null, primary key +# title :string(255) +# user_id :integer +# public_body_id :integer +# body :text +# embargo_duration :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# + +FactoryGirl.define do + factory :draft_info_request do + sequence(:title) { |n| "Draft: Example Title #{n}" } + public_body + user + sequence(:body) { |n| "Do you have information about record #{n}?" } + embargo_duration "3_months" + + factory :draft_with_no_duration do + embargo_duration nil + end + end +end diff --git a/spec/models/draft_info_request_spec.rb b/spec/models/draft_info_request_spec.rb new file mode 100644 index 0000000000..9ca44cea20 --- /dev/null +++ b/spec/models/draft_info_request_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe DraftInfoRequest do + let(:draft) { FactoryGirl.create(:draft_info_request) } + + it "belongs to a public body" do + expect(draft.public_body).to be_a(PublicBody) + end + + it "belongs to a user" do + expect(draft.user).to be_a(User) + end + + it "has a title" do + expect(draft.title).to be_a(String) + end + + it "has a body" do + expect(draft.body).to be_a(String) + end + + it "requires a user" do + draft_request = DraftInfoRequest.new + expect(draft_request.valid?).to be false + draft_request.user = FactoryGirl.create(:user) + expect(draft_request.valid?).to be true + end +end From d2400d6e1b714dc325d6eeeb719de3088c432992 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 25 Nov 2016 20:40:30 +0000 Subject: [PATCH 083/311] Add a .from_draft class method to InfoRequest So that we can create an InfoRequest and the associated OutgoingMessage and Embargo objects directly from a draft. --- app/models/info_request.rb | 18 +++++++++++++ spec/models/info_request_spec.rb | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 06f3d31f11..7d6a41a320 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -948,6 +948,24 @@ def self.create_from_attributes(info_request_atts, outgoing_message_atts, user=n info_request end + def self.from_draft(draft) + info_request = new(:title => draft.title, + :user => draft.user, + :public_body => draft.public_body) + info_request.outgoing_messages.new(:body => draft.body, + :status => 'ready', + :message_type => 'initial_request', + :what_doing => 'normal_sort', + :info_request => info_request) + if draft.embargo_duration + info_request.embargo = Embargo.new( + :embargo_duration => draft.embargo_duration, + :info_request => info_request + ) + end + info_request + end + def self.hash_from_id(id) Digest::SHA1.hexdigest(id.to_s + AlaveteliConfiguration::incoming_email_secret)[0,8] end diff --git a/spec/models/info_request_spec.rb b/spec/models/info_request_spec.rb index 0f0aa388af..9472a9dadf 100644 --- a/spec/models/info_request_spec.rb +++ b/spec/models/info_request_spec.rb @@ -2749,6 +2749,50 @@ def apply_filters(filters) end end + describe ".from_draft" do + let(:draft) { FactoryGirl.create(:draft_info_request) } + let(:info_request) { InfoRequest.from_draft(draft) } + + it "builds an info_request from the draft" do + expect(info_request.title).to eq draft.title + expect(info_request.public_body).to eq draft.public_body + expect(info_request.user).to eq draft.user + expect(info_request).not_to be_persisted + end + + it "builds an initial outgoing message" do + expect(info_request.outgoing_messages.length).to eq 1 + outgoing_message = info_request.outgoing_messages.first + expect(outgoing_message.body).to eq draft.body + expect(outgoing_message.info_request).to eq info_request + expect(outgoing_message.info_request).not_to be_persisted + end + + context "when the draft has a duration" do + it "builds an embargo" do + expect(info_request.embargo).not_to be nil + embargo = info_request.embargo + expect(embargo.embargo_duration).to eq draft.embargo_duration + expect(embargo.info_request).to eq info_request + expect(embargo).not_to be_persisted + end + end + + context "when the draft doesnt have a duration" do + let(:draft_with_no_duration) do + FactoryGirl.create(:draft_with_no_duration) + end + + let(:request_with_no_embargo) do + InfoRequest.from_draft(draft_with_no_duration) + end + + it "doesnt build an embargo" do + expect(request_with_no_embargo.embargo).to be nil + end + end + end + def email_and_raw_email(opts = {}) raw_email = opts[:raw_email] || <<-EOF.strip_heredoc From: EMAIL_FROM From a2d959ce06d92827d306bf2b6bed14a8f07eda81 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 25 Nov 2016 20:41:58 +0000 Subject: [PATCH 084/311] Add a has_many :draft_info_requests to the User model --- app/models/user.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index c7e1de0354..421b212a58 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,6 +45,9 @@ class User < ActiveRecord::Base has_many :info_requests, :order => 'created_at desc', :dependent => :destroy + has_many :draft_info_requests, + :order => 'created_at desc', + :dependent => :destroy has_many :user_info_request_sent_alerts, :dependent => :destroy has_many :post_redirects, From c50f3e228a28215518ac701434edecf39d9293e5 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 28 Nov 2016 17:06:06 +0000 Subject: [PATCH 085/311] Add a DURATION_LABELS constant to Embargo For use in printing the list of duration options in views --- app/models/embargo.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/models/embargo.rb b/app/models/embargo.rb index 7887e08e40..dfc493c8ad 100644 --- a/app/models/embargo.rb +++ b/app/models/embargo.rb @@ -27,6 +27,12 @@ class Embargo < ActiveRecord::Base "12_months" => Proc.new { 12.months } }.freeze + DURATION_LABELS = { + "3_months" => _("3 Months"), + "6_months" => _("6 Months"), + "12_months" => _("12 Months") + }.freeze + def allowed_durations DURATIONS.keys end @@ -35,6 +41,10 @@ def duration_as_duration DURATIONS[self.embargo_duration].call end + def duration_label + DURATION_LABELS[self.embargo_duration] + end + def set_publish_at_from_duration unless self.publish_at.present? || self.embargo_duration.blank? self.publish_at = Time.zone.today + duration_as_duration From 096ecaca4bf3d11e889e156f557f03f495e72d39 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 28 Nov 2016 17:06:45 +0000 Subject: [PATCH 086/311] Add routes for info requests and drafts --- config/routes.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index c50d6ddd34..cc1030945e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -597,9 +597,11 @@ #### Alaveteli Pro constraints FeatureConstraint.new(:alaveteli_pro) do namespace :alaveteli_pro do - match '/' => 'dashboard#index', - :as => 'dashboard', - :via => :get + match '/' => 'dashboard#index', :as => 'dashboard', :via => :get + resources :draft_info_requests, :only => [:create, :update] + resources :info_requests, :only => [:new, :create] do + get :preview, on: :new # /info_request/new/preview + end end end #### From b7e35fda30a63bb8b733f0f686450b5734c561e1 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 28 Nov 2016 17:31:57 +0000 Subject: [PATCH 087/311] Add pages for writing, previewing and sending InfoRequests --- .../draft_info_requests_controller.rb | 46 ++++ .../alaveteli_pro/info_requests_controller.rb | 96 +++++++++ .../alaveteli_pro/info_requests_helper.rb | 7 + .../info_requests/_embargo_info.html | 9 + .../alaveteli_pro/info_requests/new.html.erb | 78 +++++++ .../info_requests/preview.html.erb | 42 ++++ .../info_requests_controller_spec.rb | 40 ++++ spec/integration/alaveteli_dsl.rb | 8 + .../alaveteli_pro/request_creation_spec.rb | 197 ++++++++++++++++++ 9 files changed, 523 insertions(+) create mode 100644 app/controllers/alaveteli_pro/draft_info_requests_controller.rb create mode 100644 app/controllers/alaveteli_pro/info_requests_controller.rb create mode 100644 app/helpers/alaveteli_pro/info_requests_helper.rb create mode 100644 app/views/alaveteli_pro/info_requests/_embargo_info.html create mode 100644 app/views/alaveteli_pro/info_requests/new.html.erb create mode 100644 app/views/alaveteli_pro/info_requests/preview.html.erb create mode 100644 spec/controllers/alaveteli_pro/info_requests_controller_spec.rb create mode 100644 spec/integration/alaveteli_pro/request_creation_spec.rb diff --git a/app/controllers/alaveteli_pro/draft_info_requests_controller.rb b/app/controllers/alaveteli_pro/draft_info_requests_controller.rb new file mode 100644 index 0000000000..fbfa870615 --- /dev/null +++ b/app/controllers/alaveteli_pro/draft_info_requests_controller.rb @@ -0,0 +1,46 @@ +# -*- encoding : utf-8 -*- +# app/controllers/alaveteli_pro/draft_info_requests_controller.rb +# Controller for draft info requests +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ + +class AlaveteliPro::DraftInfoRequestsController < AlaveteliPro::BaseController + def create + @draft = current_user.draft_info_requests.create(draft_params) + redirect_after_create_or_update + end + + def update + @draft = current_user.draft_info_requests.find(params[:id]) + @draft.update_attributes(draft_params) + redirect_after_create_or_update + end + + private + + def redirect_after_create_or_update + if params[:preview] + redirect_to preview_new_alaveteli_pro_info_request_path(draft_id: @draft.id) + else + redirect_to new_alaveteli_pro_info_request_path(draft_id: @draft.id), + notice: _("Your draft has been saved!") + end + end + + def draft_params + info_request_params.merge(outgoing_message_params.merge(embargo_params)) + end + + def info_request_params + params.require(:info_request).permit(:title, :public_body_id) + end + + def outgoing_message_params + params.require(:outgoing_message).permit(:body) + end + + def embargo_params + params.require(:embargo).permit(:embargo_duration) + end +end diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb new file mode 100644 index 0000000000..91109c3776 --- /dev/null +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -0,0 +1,96 @@ +# -*- encoding : utf-8 -*- +# app/controllers/alaveteli_pro/info_requests_controller.rb +# Controller for info requests +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ + +class AlaveteliPro::InfoRequestsController < AlaveteliPro::BaseController + before_filter :set_draft + before_filter :load_data_from_draft, only: [:preview, :create] + + def new + if @draft_info_request + load_data_from_draft + else + create_initial_objects + end + end + + def preview + if all_models_valid? + render "preview" + else + show_errors + end + end + + def create + if all_models_valid? + @info_request.save # Saves @outgoing_message too + @embargo.save if @embargo.present? + send_initial_message(@outgoing_message) + destroy_draft + redirect_to show_request_path(url_title: @info_request.url_title) + else + show_errors + end + end + + private + + def show_errors + # There'll be a duplicate error if the outgoing_message is invalid, so + # delete it + @info_request.errors.delete(:outgoing_messages) + render "new" + end + + + def all_models_valid? + @info_request.valid? && @outgoing_message.valid? && \ + (@embargo.nil? || @embargo.present? && @embargo.valid?) + end + + def set_draft + if params[:draft_id] + @draft_info_request = current_user.draft_info_requests.find( + params[:draft_id] + ) + end + end + + def load_data_from_draft + @info_request = InfoRequest.from_draft(@draft_info_request) + @outgoing_message = @info_request.outgoing_messages.first + @embargo = @info_request.embargo + end + + def create_initial_objects + @draft_info_request = DraftInfoRequest.new + @info_request = InfoRequest.new + @outgoing_message = OutgoingMessage.new(info_request: @info_request) + # TODO: set duration based on current user's account settings + @embargo = Embargo.new(info_request: @info_request) + end + + def destroy_draft + if params[:draft_id] + current_user.draft_info_requests.destroy(params[:draft_id]) + end + end + + def send_initial_message(outgoing_message) + if outgoing_message.sendable? + mail_message = OutgoingMailer.initial_request( + outgoing_message.info_request, + outgoing_message + ).deliver + + outgoing_message.record_email_delivery( + mail_message.to_addrs.join(', '), + mail_message.message_id + ) + end + end +end \ No newline at end of file diff --git a/app/helpers/alaveteli_pro/info_requests_helper.rb b/app/helpers/alaveteli_pro/info_requests_helper.rb new file mode 100644 index 0000000000..4040797902 --- /dev/null +++ b/app/helpers/alaveteli_pro/info_requests_helper.rb @@ -0,0 +1,7 @@ +# -*- encoding : utf-8 -*- +module AlaveteliPro::InfoRequestsHelper + def publish_at_options + options = { _("Publish immediately") => '' } + options.merge(Embargo::DURATION_LABELS.invert) + end +end diff --git a/app/views/alaveteli_pro/info_requests/_embargo_info.html b/app/views/alaveteli_pro/info_requests/_embargo_info.html new file mode 100644 index 0000000000..f5c8f72fa8 --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/_embargo_info.html @@ -0,0 +1,9 @@ +

Embargo

+<% if @embargo && @embargo.publish_at %> +

+ <%= _("This request will be embargoed until {{embargo_publish_at}}", + embargo_publish_at: @embargo.publish_at.to_date) %> +

+<% else %> +

<%= _("This request will be published immediately") %>

+<% end %> \ No newline at end of file diff --git a/app/views/alaveteli_pro/info_requests/new.html.erb b/app/views/alaveteli_pro/info_requests/new.html.erb new file mode 100644 index 0000000000..ebea57d476 --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/new.html.erb @@ -0,0 +1,78 @@ +<% @title = _("Make an {{law_used_short}} request", + :law_used_short => h(@info_request.law_used_human(:short))) %> + +<%= form_tag([:alaveteli_pro, @draft_info_request], + method: @draft_info_request.id ? :put : :post, + html: { id: 'write_form', :class => 'new_info_request' }) do %> + + <% fields_for @info_request do |f| %> + +
+
+

<%= _("Make a request") %>

+ + <%= foi_error_messages_for :info_request, :outgoing_message %> + + <% if !AlaveteliConfiguration::override_all_public_body_request_emails.blank? %> +
+ <%= _("Note: Because we're testing, requests are being sent to {{email}} rather than to the actual authority.", :email => AlaveteliConfiguration::override_all_public_body_request_emails) %> +
+ <% end %> + +

+ + <%= f.collection_select(:public_body_id, PublicBody.visible, :id, :name) %> +

+
+
+ +
+
+ <%= render partial: "embargo_info" %> +

+ + <%= fields_for :embargo do |e| %> + <%= e.select :embargo_duration, + options_for_select( + publish_at_options, + :selected => @embargo ? @embargo.embargo_duration : '' + ) %> + <% end %> +

+
+
+
+

+ + <%= f.text_field :title, :size => 50 %> +

+ +
+ <%= _("A one line summary of the information you are requesting, e.g.") %> +
+
+ + <%= fields_for @outgoing_message do |o| %> +

+ + <%= o.text_area :body, :rows => 20, :cols => 60 %> +

+ <% end %> + +
+ + +
+
+
+ <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/alaveteli_pro/info_requests/preview.html.erb b/app/views/alaveteli_pro/info_requests/preview.html.erb new file mode 100644 index 0000000000..52ee178c67 --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/preview.html.erb @@ -0,0 +1,42 @@ +<% @title = _("Preview new {{law_used_short}} request to '{{public_body_name}}", + :law_used_short => h(@info_request.law_used_human(:short)), + :public_body_name => h(@info_request.public_body.name)) %> + +
<%# TODO - hacky id + class to get same styling as current preview form %> +

<%= _('Preview your request') %>

+ +
+
+
+ <%= render partial: "embargo_info" %> +
+
+ +
+
+

+ <%= _('To') %> + <%=h(@info_request.public_body.name)%> + +

+

+ <%= _('Subject') %> <%= @info_request.title %> +

+ +
+

<%= simple_format(@outgoing_message.body) %>

+
+
+ +

+ <%= form_for([:alaveteli_pro, @info_request]) do |f| %> + <%= hidden_field_tag :draft_id, @draft_info_request.id %> + + <%= link_to _("Edit your request"), "#{new_alaveteli_pro_info_request_path}?draft_id=#{@draft_info_request.id}" %> + <%= submit_tag _("Send request"), :disable_with => _("Sending..."), + :id => 'submit_button' %> + <% end %> +

+
+
+
\ No newline at end of file diff --git a/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb new file mode 100644 index 0000000000..923a843aa5 --- /dev/null +++ b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb @@ -0,0 +1,40 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') + +describe AlaveteliPro::InfoRequestsController do + let(:pro_user) { FactoryGirl.create(:pro_user) } + + describe "#preview" do + let(:draft) do + FactoryGirl.create(:draft_info_request, body: nil, user: pro_user) + end + + context "when there are errors on the outgoing message" do + it "removes duplicate errors from the info_request" do + session[:user_id] = pro_user.id + with_feature_enabled(:alaveteli_pro) do + post :preview, draft_id: draft + expect(assigns[:info_request].errors[:outgoing_messages]).to be_empty + expect(assigns[:outgoing_message].errors).not_to be_empty + end + end + end + end + + describe "#create" do + let(:draft) do + FactoryGirl.create(:draft_info_request, body: nil, user: pro_user) + end + + context "when there are errors on the outgoing message" do + it "removes duplicate errors from the info_request" do + session[:user_id] = pro_user.id + with_feature_enabled(:alaveteli_pro) do + post :create, draft_id: draft + expect(assigns[:info_request].errors[:outgoing_messages]).to be_empty + expect(assigns[:outgoing_message].errors).not_to be_empty + end + end + end + end +end diff --git a/spec/integration/alaveteli_dsl.rb b/spec/integration/alaveteli_dsl.rb index b8ff483c85..64ae8b0856 100644 --- a/spec/integration/alaveteli_dsl.rb +++ b/spec/integration/alaveteli_dsl.rb @@ -45,6 +45,14 @@ def alaveteli_session(session_id) end end +def using_pro_session(session_id) + with_feature_enabled(:alaveteli_pro) do + using_session(session_id) do + yield + end + end +end + def login(user) u = user.is_a?(User) ? user : users(user) alaveteli_session(u.id) do diff --git a/spec/integration/alaveteli_pro/request_creation_spec.rb b/spec/integration/alaveteli_pro/request_creation_spec.rb new file mode 100644 index 0000000000..ca2fdd3778 --- /dev/null +++ b/spec/integration/alaveteli_pro/request_creation_spec.rb @@ -0,0 +1,197 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/../alaveteli_dsl') + +describe "creating requests in alaveteli_pro" do + context "when writing a new request from scratch" do + let!(:public_body) { FactoryGirl.create(:public_body) } + let!(:pro_user) { FactoryGirl.create(:pro_user) } + let!(:pro_user_session) { login(pro_user) } + + it "allows us to save a draft" do + using_pro_session(pro_user_session) do + # New request form + visit new_alaveteli_pro_info_request_path + expect(page).to have_content "Make a request" + select public_body.name, from: "To:" + fill_in "Summary:", with: "Does the pro request form work?" + fill_in "Your request:", with: "A very short letter." + select "3 Months", from: "Set embargo:" + click_button "Save draft" + + # Redirected back to new request form + drafts = DraftInfoRequest.where(title: "Does the pro request form work?") + expect(drafts).to exist + draft = drafts.first + expect(draft.body).to eq "A very short letter." + expect(draft.embargo_duration).to eq "3_months" + + expect(page).to have_content("Your draft has been saved!") + expect(page).to have_content("This request will be embargoed " \ + "until #{Time.zone.today + 3.months}") + + # The page should pre-fill the form with data from the draft + expect(page).to have_select("To:", selected: public_body.name) + expect(page).to have_field("Summary:", + with: "Does the pro request form work?") + expect(page).to have_field("Your request:", + with: "A very short letter.") + expect(page).to have_select("Set embargo:", selected: "3 Months") + end + end + + it "allows us to preview the request" do + using_pro_session(pro_user_session) do + # New request form + visit new_alaveteli_pro_info_request_path + expect(page).to have_content "Make a request" + select public_body.name, from: "To:" + fill_in "Summary:", with: "Does the pro request form work?" + fill_in "Your request:", with: "A very short letter." + select "3 Months", from: "Set embargo:" + click_button "Preview and send request" + + # Preview page + drafts = DraftInfoRequest.where(title: "Does the pro request form work?") + expect(drafts).to exist + + expect(page).to have_content("Preview your request") + # The fact there's a draft should be hidden from the user + expect(page).not_to have_content("Your draft has been saved!") + + expect(page).to have_content("To #{public_body.name}") + expect(page).to have_content("Subject Does the pro request form " \ + "work?") + expect(page).to have_content("A very short letter.") + expect(page).to have_content("This request will be embargoed " \ + "until #{Time.zone.today + 3.months}") + end + end + + it "allows us to send the request" do + using_pro_session(pro_user_session) do + # New request form + visit new_alaveteli_pro_info_request_path + expect(page).to have_content "Make a request" + select public_body.name, from: "To:" + fill_in "Summary:", with: "Does the pro request form work?" + fill_in "Your request:", with: "A very short letter." + select "3 Months", from: "Set embargo:" + click_button "Preview and send request" + + # Preview page + click_button "Send request" + + # Request page + expect(page).to have_selector("h1", text: "Does the pro request form work?") + + drafts = DraftInfoRequest.where(title: "Does the pro request form work?") + expect(drafts).not_to exist + + info_requests = InfoRequest.where(title: "Does the pro request form work?") + expect(info_requests).to exist + + info_request = info_requests.first + + embargo = info_request.embargo + expect(embargo).not_to be_nil + expect(embargo.publish_at).to eq Time.zone.today + 3.months + + expect(info_request.outgoing_messages.length).to eq 1 + expect(info_request.outgoing_messages.first.body).to eq "A very short letter." + + deliveries = ActionMailer::Base.deliveries + expect(deliveries.size).to eq(1) + mail = deliveries[0] + expect(mail.body).to match(/A very short letter\./) + expect(mail.subject).to match(/Freedom of Information request - Does the pro request form work\?/) + expect(mail.to).to eq([public_body.request_email]) + end + end + + it "allow us to edit a request after previewing" do + using_pro_session(pro_user_session) do + # New request form + visit new_alaveteli_pro_info_request_path + expect(page).to have_content "Make a request" + select public_body.name, from: "To:" + fill_in "Summary:", with: "Does the pro request form work?" + fill_in "Your request:", with: "A very short letter." + select "3 Months", from: "Set embargo:" + click_button "Preview and send request" + + # Preview page + click_link "Edit your request" + + # New request form again + # The page should pre-fill the form with data from the draft + expect(page).to have_select("To:", selected: public_body.name) + expect(page).to have_field("Summary:", + with: "Does the pro request form work?") + expect(page).to have_field("Your request:", + with: "A very short letter.") + expect(page).to have_select("Set embargo:", selected: "3 Months") + + fill_in "Your request:", with: "A very short letter, edited." + click_button "Save draft" + + # Redirected back to new request form + drafts = DraftInfoRequest.where(title: "Does the pro request form work?") + expect(drafts).to exist + draft = drafts.first + expect(draft.body).to eq "A very short letter, edited." + expect(draft.embargo_duration).to eq "3_months" + + expect(page).to have_content("Your draft has been saved!") + + click_button "Preview and send request" + + # Preview page again + expect(page).to have_content("Preview your request") + # The fact there's a draft should be hidden from the user + expect(page).not_to have_content("Your draft has been saved!") + + expect(page).to have_content("To #{public_body.name}") + expect(page).to have_content("Subject Does the pro request form " \ + "work?") + expect(page).to have_content("A very short letter, edited.") + expect(page).to have_content("This request will be embargoed " \ + "until #{Time.zone.today + 3.months}") + end + end + + it "shows errors if we leave fields blank" do + using_pro_session(pro_user_session) do + # New request form + visit new_alaveteli_pro_info_request_path + expect(page).to have_content "Make a request" + select public_body.name, from: "To:" + click_button "Preview and send request" + + # New request form with errors + expect(page).to have_content "Please enter a summary of your " \ + "request" + expect(page).to have_content 'Please sign at the bottom with ' \ + 'your name, or alter the "Yours ' \ + 'faithfully," signature' + end + end + + it "saves the draft even if we leave fields blank" do + using_pro_session(pro_user_session) do + # New request form + visit new_alaveteli_pro_info_request_path + expect(page).to have_content "Make a request" + select public_body.name, from: "To:" + click_button "Save draft" + + # New request form with errors + expect(page).not_to have_content "Please enter a summary of your " \ + "request" + expect(page).not_to have_content 'Please sign at the bottom with ' \ + 'your name, or alter the "Yours ' \ + 'faithfully," signature' + end + end + end +end From 66d0c9d36b3fc4103212a8564ada5888eb5fde4b Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 2 Dec 2016 12:25:57 +0000 Subject: [PATCH 088/311] Fix for bug introduced in 773d5776d1085928d81c2ef3700f8a04ff1df74a. --- spec/integration/track_alerts_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/integration/track_alerts_spec.rb b/spec/integration/track_alerts_spec.rb index 89b2a1f4ca..a8e524e64a 100644 --- a/spec/integration/track_alerts_spec.rb +++ b/spec/integration/track_alerts_spec.rb @@ -4,6 +4,14 @@ describe "When sending track alerts" do + before do + # TODO: required to make sure xapian index can find files for raw emails + # associated with fixtures - can be removed when fixtures no longer + # automatically loaded for all specs + load_raw_emails_data + get_fixtures_xapian_index + end + it "should send alerts" do info_request = FactoryGirl.create(:info_request) From 4207084e2345386af428670c84460e1988d2453f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 25 Nov 2016 15:38:00 +0000 Subject: [PATCH 089/311] Add InfoRequestsController#index Basic view templates, styles and idea of request phases. --- .../alaveteli_pro/_dashboard_layout.scss | 123 ++++++++++++++++++ .../alaveteli_pro/_dashboard_style.scss | 90 +++++++++++++ .../alaveteli_pro/_requests_layout.scss | 79 +++++++++++ .../alaveteli_pro/_requests_style.scss | 26 ++++ app/assets/stylesheets/responsive/all.scss | 6 + .../alaveteli_pro/info_requests_controller.rb | 4 + app/models/info_request.rb | 36 +++-- app/models/info_request/state.rb | 43 ++++++ .../state/awaiting_response_query.rb | 15 +++ .../state/clarification_needed_query.rb | 15 +++ .../info_request/state/complete_query.rb | 19 +++ app/models/info_request/state/other_query.rb | 21 +++ .../state/response_received_query.rb | 14 ++ app/models/info_request_event.rb | 2 +- app/views/admin_request/edit.html.erb | 2 +- .../dashboard/_projects.html.erb | 30 +++++ .../info_requests/_info_request.html.erb | 41 ++++++ .../info_requests/index.html.erb | 48 +++++++ config/routes.rb | 2 +- .../info_requests_controller_spec.rb | 19 +++ spec/factories/info_requests.rb | 12 ++ .../state/awaiting_response_query_spec.rb | 21 +++ .../state/clarification_needed_query_spec.rb | 22 ++++ .../info_request/state/complete_query_spec.rb | 22 ++++ .../info_request/state/other_query_spec.rb | 22 ++++ .../state/response_received_query_spec.rb | 21 +++ spec/models/info_request/state_spec.rb | 27 ++++ 27 files changed, 758 insertions(+), 24 deletions(-) create mode 100644 app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss create mode 100644 app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss create mode 100644 app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss create mode 100644 app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss create mode 100644 app/models/info_request/state.rb create mode 100644 app/models/info_request/state/awaiting_response_query.rb create mode 100644 app/models/info_request/state/clarification_needed_query.rb create mode 100644 app/models/info_request/state/complete_query.rb create mode 100644 app/models/info_request/state/other_query.rb create mode 100644 app/models/info_request/state/response_received_query.rb create mode 100644 app/views/alaveteli_pro/dashboard/_projects.html.erb create mode 100644 app/views/alaveteli_pro/info_requests/_info_request.html.erb create mode 100644 app/views/alaveteli_pro/info_requests/index.html.erb create mode 100644 spec/models/info_request/state/awaiting_response_query_spec.rb create mode 100644 spec/models/info_request/state/clarification_needed_query_spec.rb create mode 100644 spec/models/info_request/state/complete_query_spec.rb create mode 100644 spec/models/info_request/state/other_query_spec.rb create mode 100644 spec/models/info_request/state/response_received_query_spec.rb create mode 100644 spec/models/info_request/state_spec.rb diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss new file mode 100644 index 0000000000..fb6cde49a3 --- /dev/null +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss @@ -0,0 +1,123 @@ +/* Layout for the dashboard */ + +.dashboard-header, +.dashboard-body { + @include flexbox; + padding: 1em; +} + +.dashboard-left-column { + width: 30%; + padding-left: 2em; + padding-right: 1.5em; +} + +.dashboard-right-column { + width: 70%; + padding-right: 2em; +} + +.dashboard-header { + margin-top: 2em; + padding-left: 2em; + h1 { + margin-top: 0.5em; + margin-bottom: 0.5em; + display:block; + width:100%; + font-weight: bold; + } +} + +.dashboard-profile__image { + float:left; + margin-right: 1em; + border-radius: 5px; +} + +.dashboard-nav { + padding: 0; + margin: 0; + @include clearfix; +} + +.dashboard-nav__item { + float: left; + padding-bottom: 0.5em; + padding-right: 1em; +} + +.dashboard-nav__item--right { + float: right; +} + +h1 { + margin-left: 1em; + margin-bottom: 1em; +} + +.dashboard-folders, +.dashboard-sub-folders { + margin: 0; + padding: 0; +} + +.dashboard-sub-folders { + margin: 1em 0 2em 0; +} + +.dashboard-folder { + margin-bottom: 0.5em; + font-size: 0.9em; +} + +.dashboard-folder a, +.dashboard-folder a:hover, +.dashboard-folder--disabled .fake-link { + display: block; + padding: 0.5em 1em 0.5em 1em; + width: 100%; +} + +.dashboard-folder--selected { + margin-bottom: 0; +} + +.dashboard-folder__count { + float: right; +} + +.dashboard-todo { + margin-bottom: 3em; +} + +.dashboard-todo__list { + margin: 0; + padding: 0; +} + +.dashboard-todo__title, +.dashboard-activity__title { + padding-bottom: 1em; + margin-bottom: 0; + margin-top: 0; +} + +.dashboard-todo__list__item, +.dashboard-activity__item { + padding: 1em 0 1em 0; +} + +.dashboard-activity__item { + @include flexbox; +} + +.dashboard-activity__item__time { + flex: 0 0 25%; + padding-right: 2em; +} + +.dashboard-activity__item__dashboard { + flex: 0 0 75%; + padding-left: 2em; +} diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss new file mode 100644 index 0000000000..6e80a995d7 --- /dev/null +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss @@ -0,0 +1,90 @@ +/* Style for the dashboard */ + +.dashboard { + background-color: white; +} + +.dashboard-header { + background-color: #e6e4df; + border: 1px solid #e1dfda; +} + +.dashboard-nav { + list-style-type: none; + list-style: none; + border-bottom: 1px solid #DDDDDD; +} + +.dashboard-nav__item { + border-bottom: 1px solid #DDDDDD; + // To overlap the bottom border with that of the containing list, making it + // appear continuous + margin-bottom: -2px; + a, + a:hover { + font-weight: normal; + color: white; + text-decoration: none; + } +} + +.dashboard-nav__item:hover, +.dashboard-nav__item--highlighted { + border-bottom: 2px solid orange; + cursor: pointer; +} + +.dashboard-folders, +.dashboard-sub-folders { + list-style: none; + list-style-type: none; +} + +.dashboard-folder a, +.dashboard-folder a:hover, +.dashboard-folder--disabled .fake-link { + color: black; + border-bottom: 1px solid #ccc; + text-decoration: none; +} +.dashboard-folder a:hover { + color: black; + border-bottom-color: orange; +} + +.dashboard-folder--disabled { + color: #999; +} + +.dashboard-folder--top-level { + background-color: #E9E9E9; + margin-bottom: 1em; + border-bottom: transparent; +} + +.dashboard-folder--selected { + font-weight: bold; + font-size: 1em; +} + +.dashboard-todo__list { + list-style: none; + list-style-type: none; +} + +.dashboard-activity__title, +.dashboard-todo__title { + font-size: 1em; + font-weight: bold; +} + +.dashboard-todo__title, +.dashboard-todo__list__item, +.dashboard-activity__title, +.dashboard-activity__item { + border-bottom: 1px solid #ccc; +} + +.dashboard-activity__item__time { + color: #999; +} diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss new file mode 100644 index 0000000000..d63054ccad --- /dev/null +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss @@ -0,0 +1,79 @@ +/* Layout for the requests page */ + +.requests__folder-title { + h2 { + margin-top: 0; + } +} +.requests__header { + @include flexbox; + padding: 0.5em; +} + +.requests__search, +.requests__sort { + @include flex(0 0 33%); + margin-right: 1em; + + input, select, label { + display: inline-block; + margin-bottom: 0; + } + select { + max-width: 60%; + } +} + +.requests__list { + margin-top: 0.5em; +} + +.request { + padding-top: 1em; + padding-bottom: 1em; + margin-bottom: 1em; +} + +.request__title { + margin-top: 0; +} + +.request__meta { + @include flexbox; +} + +.request__recipient, +.request__created, +.request__updated, +.request__status, +.request__actions { + margin-right: 1em; +} + +// TODO - the flex() rules for these add up to way less than 100%, yet the +// elements still overflow their container. Not sure why. I've hacked it by +// reducing the width of the select in the last element and floating it right +.request__recipient, +.request__status { + @include flex(0 0 15%); +} + +.request__created, +.request__updated { + @include flex(0 0 15%); +} + +.request__actions { + @include flex(0 0 31%); // No idea why this has to be 31% in particular + @include clearfix; + margin-right: 0; + .actions { + max-width: 90%; + float: right; + } + +} + +.request__meta__label { + display: block; +} diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss new file mode 100644 index 0000000000..fd8833898a --- /dev/null +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss @@ -0,0 +1,26 @@ +/* Style for the requests page */ + +.requests__header { + background-color: #f3f1eb; +} + +.request { + border-bottom: 1px solid #ccc; +} + +.request__title { + font-size: 1.25em; +} + +.request__recipient, +.request__created, +.request__updated, +.request__status, +.request__actions { + font-size: 0.9em; +} + +.request__meta__label { + font-size: 0.8em; + color: #999; +} diff --git a/app/assets/stylesheets/responsive/all.scss b/app/assets/stylesheets/responsive/all.scss index 79d3880e47..5280a24f8a 100644 --- a/app/assets/stylesheets/responsive/all.scss +++ b/app/assets/stylesheets/responsive/all.scss @@ -72,4 +72,10 @@ @import "_time_series"; +@import "alaveteli_pro/_dashboard_layout"; +@import "alaveteli_pro/_dashboard_style"; + +@import "alaveteli_pro/_requests_layout"; +@import "alaveteli_pro/_requests_style"; + @import "custom"; diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index 91109c3776..dc0244a581 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -9,6 +9,10 @@ class AlaveteliPro::InfoRequestsController < AlaveteliPro::BaseController before_filter :set_draft before_filter :load_data_from_draft, only: [:preview, :create] + def index + @info_requests = current_user.info_requests + end + def new if @draft_info_request load_data_from_draft diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 7d6a41a320..c2ed61e6ee 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -87,6 +87,13 @@ class InfoRequest < ActiveRecord::Base scope :embargoed, Prominence::EmbargoedQuery.new scope :not_embargoed, Prominence::NotEmbargoedQuery.new + + scope :awaiting_response, State::AwaitingResponseQuery.new + scope :response_received, State::ResponseReceivedQuery.new + scope :clarification_needed, State::ClarificationNeededQuery.new + scope :complete, State::CompleteQuery.new + scope :other, State::OtherQuery.new + def self.visible warn %q([DEPRECATION] InfoRequest#visible will be removed in 0.27. It has been replaced by InfoRequest#is_public).squish @@ -131,26 +138,9 @@ def self.visible before_validation :compute_idhash def self.enumerate_states - states = [ - 'waiting_response', - 'waiting_clarification', - 'gone_postal', - 'not_held', - 'rejected', # this is called 'refused' in UK FOI law and the user interface, but 'rejected' internally for historic reasons - 'successful', - 'partially_successful', - 'internal_review', - 'error_message', - 'requires_admin', - 'user_withdrawn', - 'attention_requested', - 'vexatious', - 'not_foi' - ] - if @@custom_states_loaded - states += InfoRequest.theme_extra_states - end - states + warn %q([DEPRECATION] InfoRequest.enumerate_states will be removed in + 0.28. It has been replaced by InfoRequest::States#all).squish + State.all end # Subset of states accepted via the API @@ -191,7 +181,7 @@ def prominence(opts = {}) end def must_be_valid_state - unless InfoRequest.enumerate_states.include?(described_state) + unless State.all.include?(described_state) errors.add(:described_state, "is not a valid state") end end @@ -253,6 +243,10 @@ def user_json_for_api rescue MissingSourceFile, NameError end + def self.custom_states_loaded + @@custom_states_loaded + end + OLD_AGE_IN_DAYS = 21.days # If the URL name has changed, then all request: queries will break unless diff --git a/app/models/info_request/state.rb b/app/models/info_request/state.rb new file mode 100644 index 0000000000..5ce432c4dc --- /dev/null +++ b/app/models/info_request/state.rb @@ -0,0 +1,43 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module State + + def self.all + states = [ + 'waiting_response', + 'waiting_clarification', + 'gone_postal', + 'not_held', + 'rejected', # this is called 'refused' in UK FOI law and the user interface, but 'rejected' internally for historic reasons + 'successful', + 'partially_successful', + 'internal_review', + 'error_message', + 'requires_admin', + 'user_withdrawn', + 'attention_requested', + 'vexatious', + 'not_foi' + ] + if InfoRequest.custom_states_loaded + states += InfoRequest.theme_extra_states + end + states + end + + def self.phases + [ { name: _('Awaiting response'), + scope: :awaiting_response }, + { name: _('Response received'), + scope: :response_received }, + { name: _('Clarification needed'), + scope: :clarification_needed }, + { name: _('Complete'), + scope: :complete }, + { name: _('Status unknown'), + scope: :other } + ] + end + + end +end diff --git a/app/models/info_request/state/awaiting_response_query.rb b/app/models/info_request/state/awaiting_response_query.rb new file mode 100644 index 0000000000..5c2057506b --- /dev/null +++ b/app/models/info_request/state/awaiting_response_query.rb @@ -0,0 +1,15 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module State + class AwaitingResponseQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.where(described_state: 'waiting_response', + awaiting_description: false) + end + end + end +end diff --git a/app/models/info_request/state/clarification_needed_query.rb b/app/models/info_request/state/clarification_needed_query.rb new file mode 100644 index 0000000000..4e3b83facd --- /dev/null +++ b/app/models/info_request/state/clarification_needed_query.rb @@ -0,0 +1,15 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module State + class ClarificationNeededQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.where(described_state: 'waiting_clarification', + awaiting_description: false) + end + end + end +end diff --git a/app/models/info_request/state/complete_query.rb b/app/models/info_request/state/complete_query.rb new file mode 100644 index 0000000000..2fc6a0707b --- /dev/null +++ b/app/models/info_request/state/complete_query.rb @@ -0,0 +1,19 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module State + class CompleteQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.where(described_state: ['not_held', + 'rejected', + 'successful', + 'partially_successful', + 'user_withdrawn'], + awaiting_description: false) + end + end + end +end diff --git a/app/models/info_request/state/other_query.rb b/app/models/info_request/state/other_query.rb new file mode 100644 index 0000000000..12f1a06f69 --- /dev/null +++ b/app/models/info_request/state/other_query.rb @@ -0,0 +1,21 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module State + class OtherQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.where(described_state: ['gone_postal', + 'internal_review', + 'error_message', + 'requires_admin', + 'attention_requested', + 'vexatious', + 'not_foi'], + awaiting_description: false) + end + end + end +end diff --git a/app/models/info_request/state/response_received_query.rb b/app/models/info_request/state/response_received_query.rb new file mode 100644 index 0000000000..a9d8a2aaf8 --- /dev/null +++ b/app/models/info_request/state/response_received_query.rb @@ -0,0 +1,14 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module State + class ResponseReceivedQuery + def initialize(relation = InfoRequest) + @relation = relation + end + + def call + @relation.where(awaiting_description: true) + end + end + end +end diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb index 473fdea50a..9965af8b9c 100644 --- a/app/models/info_request_event.rb +++ b/app/models/info_request_event.rb @@ -71,7 +71,7 @@ class InfoRequestEvent < ActiveRecord::Base validate :must_be_valid_state def must_be_valid_state - if described_state and !InfoRequest.enumerate_states.include?(described_state) + if described_state and !InfoRequest::State.all.include?(described_state) errors.add(:described_state, "is not a valid state") end end diff --git a/app/views/admin_request/edit.html.erb b/app/views/admin_request/edit.html.erb index 51d344bfef..63a696ca3b 100644 --- a/app/views/admin_request/edit.html.erb +++ b/app/views/admin_request/edit.html.erb @@ -44,7 +44,7 @@

- <%= select( 'info_request', "described_state", InfoRequest.enumerate_states) %> + <%= select( 'info_request', "described_state", InfoRequest::State.all) %> <%= select('info_request', "awaiting_description", [["Yes – needs state updating",true],["No – state is up to date",false]]) %>
(don't forget to change 'awaiting description' when you set described state)
diff --git a/app/views/alaveteli_pro/dashboard/_projects.html.erb b/app/views/alaveteli_pro/dashboard/_projects.html.erb new file mode 100644 index 0000000000..0285f19fae --- /dev/null +++ b/app/views/alaveteli_pro/dashboard/_projects.html.erb @@ -0,0 +1,30 @@ +

diff --git a/app/views/alaveteli_pro/info_requests/_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_info_request.html.erb new file mode 100644 index 0000000000..93a557caec --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/_info_request.html.erb @@ -0,0 +1,41 @@ +
+

+ <%= info_request.title %> +

+ +
+ +
+ <%= _('Request to') %> + <%= info_request.public_body.name %> +
+ +
+ <%= _('Created') %> + <%= info_request.created_at.strftime('%d-%m-%Y') %> +
+ +
+ <%= _('Updated') %> + <%= info_request.updated_at.strftime('%d-%m-%Y') %> +
+ +
+ <%= _('Status') %> + <%= InfoRequest.get_status_description(info_request.described_state) %> +
+ +
+ +
+ +
+ +
diff --git a/app/views/alaveteli_pro/info_requests/index.html.erb b/app/views/alaveteli_pro/info_requests/index.html.erb new file mode 100644 index 0000000000..ee4f413fec --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/index.html.erb @@ -0,0 +1,48 @@ +
+ +
+

<%= _('Requests') %>

+
+ +
+ +
+ + <%= render partial: 'alaveteli_pro/dashboard/projects' %> + +
+ +
+ +
+
+

<%= _('All requests') %>

+
+
+ +
+ + +
+
+ +
+ + <%= render partial: 'info_request', :collection => @info_requests %> + + +
+ +
+ +
+ +
+ +
diff --git a/config/routes.rb b/config/routes.rb index cc1030945e..841f54c676 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -599,7 +599,7 @@ namespace :alaveteli_pro do match '/' => 'dashboard#index', :as => 'dashboard', :via => :get resources :draft_info_requests, :only => [:create, :update] - resources :info_requests, :only => [:new, :create] do + resources :info_requests, :only => [:new, :create, :index] do get :preview, on: :new # /info_request/new/preview end end diff --git a/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb index 923a843aa5..a583815eef 100644 --- a/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb +++ b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb @@ -4,6 +4,25 @@ describe AlaveteliPro::InfoRequestsController do let(:pro_user) { FactoryGirl.create(:pro_user) } + describe "GET #index" do + let(:info_request){ FactoryGirl.create(:info_request, :user => pro_user) } + + before do + session[:user_id] = info_request.user.id + end + + it "exists" do + get :index + expect(response.status).to be 200 + end + + it "assigns the user's InfoRequests" do + get :index + expect(assigns[:info_requests]).to eq [info_request] + end + + end + describe "#preview" do let(:draft) do FactoryGirl.create(:draft_info_request, body: nil, user: pro_user) diff --git a/spec/factories/info_requests.rb b/spec/factories/info_requests.rb index 7d506d46a0..004eb6e9d6 100644 --- a/spec/factories/info_requests.rb +++ b/spec/factories/info_requests.rb @@ -43,6 +43,18 @@ incoming_message = create(:incoming_message, :info_request => info_request) info_request.log_event("response", {:incoming_message_id => incoming_message.id}) end + + factory :waiting_clarification_info_request do + after(:create) do |info_request, evaluator| + info_request.set_described_state('waiting_clarification') + end + end + + factory :successful_request do + after(:create) do |info_request, evaluator| + info_request.set_described_state('successful') + end + end end factory :info_request_with_plain_incoming do diff --git a/spec/models/info_request/state/awaiting_response_query_spec.rb b/spec/models/info_request/state/awaiting_response_query_spec.rb new file mode 100644 index 0000000000..5489117961 --- /dev/null +++ b/spec/models/info_request/state/awaiting_response_query_spec.rb @@ -0,0 +1,21 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::State::AwaitingResponseQuery do + + describe '#call' do + + it 'includes those that are waiting for a response, + and not waiting for description' do + info_request = FactoryGirl.create(:info_request) + expect(described_class.new.call.include?(info_request)).to be true + end + + it 'excludes those that are waiting for description' do + old_unclassified_request = FactoryGirl.create(:old_unclassified_request) + expect(described_class.new.call.include?(old_unclassified_request)) + .to be false + end + + end +end diff --git a/spec/models/info_request/state/clarification_needed_query_spec.rb b/spec/models/info_request/state/clarification_needed_query_spec.rb new file mode 100644 index 0000000000..425df7421e --- /dev/null +++ b/spec/models/info_request/state/clarification_needed_query_spec.rb @@ -0,0 +1,22 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::State::ClarificationNeededQuery do + + describe '#call' do + let(:info_request){ FactoryGirl.create(:waiting_clarification_info_request) } + + it 'includes those that are waiting for a clarification, + and not waiting for description' do + expect(described_class.new.call.include?(info_request)).to be true + end + + it 'excludes those that are waiting for description' do + info_request.awaiting_description = true + info_request.save! + expect(described_class.new.call.include?(info_request)) + .to be false + end + + end +end diff --git a/spec/models/info_request/state/complete_query_spec.rb b/spec/models/info_request/state/complete_query_spec.rb new file mode 100644 index 0000000000..884236c133 --- /dev/null +++ b/spec/models/info_request/state/complete_query_spec.rb @@ -0,0 +1,22 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::State::CompleteQuery do + + describe '#call' do + let(:info_request){ FactoryGirl.create(:successful_request) } + + it 'includes those that are successful, + and not waiting for description' do + expect(described_class.new.call.include?(info_request)).to be true + end + + it 'excludes those that are waiting for description' do + info_request.awaiting_description = true + info_request.save! + expect(described_class.new.call.include?(info_request)) + .to be false + end + + end +end diff --git a/spec/models/info_request/state/other_query_spec.rb b/spec/models/info_request/state/other_query_spec.rb new file mode 100644 index 0000000000..3b84b16e9f --- /dev/null +++ b/spec/models/info_request/state/other_query_spec.rb @@ -0,0 +1,22 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::State::OtherQuery do + + describe '#call' do + let(:info_request){ FactoryGirl.create(:info_request_with_internal_review_request) } + + it 'includes those that are in internal review, + and not waiting for description' do + expect(described_class.new.call.include?(info_request)).to be true + end + + it 'excludes those that are waiting for description' do + info_request.awaiting_description = true + info_request.save! + expect(described_class.new.call.include?(info_request)) + .to be false + end + + end +end diff --git a/spec/models/info_request/state/response_received_query_spec.rb b/spec/models/info_request/state/response_received_query_spec.rb new file mode 100644 index 0000000000..8493581197 --- /dev/null +++ b/spec/models/info_request/state/response_received_query_spec.rb @@ -0,0 +1,21 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper') + +describe InfoRequest::State::ResponseReceivedQuery do + + describe '#call' do + let(:info_request){ FactoryGirl.create(:info_request) } + + it 'includes those that are waiting for description' do + info_request.awaiting_description = true + info_request.save! + expect(described_class.new.call.include?(info_request)).to be true + end + + it 'excludes those that are not waiting for description' do + expect(described_class.new.call.include?(info_request)) + .to be false + end + + end +end diff --git a/spec/models/info_request/state_spec.rb b/spec/models/info_request/state_spec.rb new file mode 100644 index 0000000000..43b7cc4b71 --- /dev/null +++ b/spec/models/info_request/state_spec.rb @@ -0,0 +1,27 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') + +describe InfoRequest::State do + + describe :all do + + it 'includes "waiting_response"' do + expect(InfoRequest::State.all.include?("waiting_response")) + .to be true + end + + end + + describe :phases do + + it 'returns an array' do + expect(InfoRequest::State.phases).to be_a Array + end + + it 'includes a hash with name "Complete" and scope :complete' do + expect(InfoRequest::State.phases.include?({ name: _('Complete'), + scope: :complete })) + end + + end +end From baf04f57574d8d125ceaf3d0d5df79f25f2e5c66 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Mon, 28 Nov 2016 14:43:04 +0000 Subject: [PATCH 090/311] Add request_filter to handle request params. --- .../alaveteli_pro/request-index.js | 8 ++ .../alaveteli_pro/_requests_layout.scss | 4 + .../alaveteli_pro/info_requests_controller.rb | 11 +- app/models/request_filter.rb | 98 +++++++++++++++++ .../dashboard/_projects.html.erb | 4 +- .../info_requests/index.html.erb | 26 ++--- config/application.rb | 1 + .../info_requests_controller_spec.rb | 15 ++- spec/models/request_filter_spec.rb | 100 ++++++++++++++++++ 9 files changed, 248 insertions(+), 19 deletions(-) create mode 100644 app/assets/javascripts/alaveteli_pro/request-index.js create mode 100644 app/models/request_filter.rb create mode 100644 spec/models/request_filter_spec.rb diff --git a/app/assets/javascripts/alaveteli_pro/request-index.js b/app/assets/javascripts/alaveteli_pro/request-index.js new file mode 100644 index 0000000000..cf0a755100 --- /dev/null +++ b/app/assets/javascripts/alaveteli_pro/request-index.js @@ -0,0 +1,8 @@ +$(function() { + $('#request_filter_order').change(function() { + this.form.submit(); + }); + $('#request_filter_filter').change(function() { + this.form.submit(); + }); +}); diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss index d63054ccad..ae289c2340 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss @@ -10,6 +10,10 @@ padding: 0.5em; } +.requests__filter_form { + @include flexbox; +} + .requests__search, .requests__sort { @include flex(0 0 33%); diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index dc0244a581..4193aac373 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -10,7 +10,13 @@ class AlaveteliPro::InfoRequestsController < AlaveteliPro::BaseController before_filter :load_data_from_draft, only: [:preview, :create] def index - @info_requests = current_user.info_requests + @request_filter = RequestFilter.new + if params[:request_filter] + @request_filter.update_attributes(params[:request_filter]) + end + info_requests = @request_filter.results(current_user.info_requests) + @info_requests = info_requests.paginate :page => params[:page], + :per_page => 10 end def new @@ -97,4 +103,5 @@ def send_initial_message(outgoing_message) ) end end -end \ No newline at end of file + +end diff --git a/app/models/request_filter.rb b/app/models/request_filter.rb new file mode 100644 index 0000000000..c97ee6f833 --- /dev/null +++ b/app/models/request_filter.rb @@ -0,0 +1,98 @@ +class RequestFilter + + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_accessor :filter, :order, :search + + def update_attributes(attributes = {}) + self.attributes = attributes + end + + def attributes=(attributes) + self.filter = attributes[:filter] + self.order = attributes[:order] + self.search = attributes[:search] + end + + def order_options + order_attributes.map {|atts| [atts[:label], atts[:param]] } + end + + def results(info_requests) + info_requests = info_requests.send(filter_value) if filter_value + if search + info_requests = info_requests.where("title ILIKE :q", q: "%#{ search }%") + end + info_requests.reorder(order_value) + end + + def persisted? + false + end + + private + + def order_attributes + [ + { :param => 'updated_at_desc', + :value => 'updated_at DESC', + :label => _('Last updated') }, + { :param => 'created_at_asc', + :value => 'created_at ASC', + :label => _('First created') }, + { :param => 'title_asc', + :value => 'title ASC', + :label => _('Title (A-Z)') } + ] + end + + def order_params + order_attributes.map{ |atts| atts[:param] } + end + + def order_values + Hash[order_attributes.map{ |atts| [ atts[:param], atts[:value] ] }] + end + + def order_value + order_params.include?(@order) ? order_values[@order] : default_order + end + + def default_order + 'updated_at DESC' + end + + def default_filters + [ { :param => '', + :value => nil, + :label => 'All Requests' }, + { :param => '', + :value => nil, + :label => 'Drafts' }, + ] + end + + def phase_filters + InfoRequest::State.phases.map{ |phase| { :param => phase[:scope].to_s, + :value => phase[:scope], + :label => phase[:name] } } + end + + def filter_attributes + default_filters + phase_filters + end + + def filter_params + phase_filters.map{ |atts| atts[:param] } + end + + def filter_values + Hash[ filter_attributes.map{ |atts| [ atts[:param], atts[:value] ] } ] + end + + def filter_value + filter_params.include?(@filter) ? filter_values[@filter] : nil + end + +end diff --git a/app/views/alaveteli_pro/dashboard/_projects.html.erb b/app/views/alaveteli_pro/dashboard/_projects.html.erb index 0285f19fae..d4b6e2a303 100644 --- a/app/views/alaveteli_pro/dashboard/_projects.html.erb +++ b/app/views/alaveteli_pro/dashboard/_projects.html.erb @@ -8,7 +8,7 @@
  • - + <%= _('Drafts') %> <%= @user.draft_info_requests.count %> @@ -18,7 +18,7 @@ <% InfoRequest::State.phases.each do |phase| %>
  • - + <%= phase[:name] %> <%= @user.info_requests.send(phase[:scope]).count %> diff --git a/app/views/alaveteli_pro/info_requests/index.html.erb b/app/views/alaveteli_pro/info_requests/index.html.erb index ee4f413fec..efa7062695 100644 --- a/app/views/alaveteli_pro/info_requests/index.html.erb +++ b/app/views/alaveteli_pro/info_requests/index.html.erb @@ -19,23 +19,22 @@

    <%= _('All requests') %>

    - -
    - - -
    + <%= form_for( @request_filter, :url => '', :method => 'get', :html => { :class => 'requests__filter_form' }) do |f| %> + +
    + + <%= f.select :order, @request_filter.order_options %> +
    + <% end %> +
    <%= render partial: 'info_request', :collection => @info_requests %> - + <%= will_paginate(@info_requests, :class => "paginator") %>
    @@ -46,3 +45,6 @@ +<% content_for :javascript do %> + <%= javascript_include_tag 'alaveteli_pro/request-index.js' %> +<% end %> diff --git a/config/application.rb b/config/application.rb index e4aa68b3b2..f203e1cea5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -126,6 +126,7 @@ class Application < Rails::Application 'ie7.css', 'bootstrap-dropdown.js', 'widget.css', + 'responsive/alaveteli_pro/request-index.js', 'responsive/print.css', 'responsive/application-lte-ie7.css', 'responsive/application-ie8.css'] diff --git a/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb index a583815eef..272c2e240f 100644 --- a/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb +++ b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb @@ -1,5 +1,5 @@ # -*- encoding : utf-8 -*- -require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') +require 'spec_helper' describe AlaveteliPro::InfoRequestsController do let(:pro_user) { FactoryGirl.create(:pro_user) } @@ -16,9 +16,18 @@ expect(response.status).to be 200 end - it "assigns the user's InfoRequests" do + it "assigns a request filter" do get :index - expect(assigns[:info_requests]).to eq [info_request] + expect(assigns[:request_filter]).to be_a RequestFilter + end + + context 'when no filters, searches or sort params are passed' do + + it "assigns the user's info requests" do + get :index + expect(assigns[:info_requests].size).to eq 1 + expect(assigns[:info_requests].first).to eq info_request + end end end diff --git a/spec/models/request_filter_spec.rb b/spec/models/request_filter_spec.rb new file mode 100644 index 0000000000..6eb7645403 --- /dev/null +++ b/spec/models/request_filter_spec.rb @@ -0,0 +1,100 @@ +# -*- encoding : utf-8 -*- +require 'spec_helper' + +describe RequestFilter do + + describe '#update_attributes' do + + it 'assigns the filter' do + request_filter = described_class.new + request_filter.update_attributes(:filter => 'awaiting_response') + expect(request_filter.filter).to eq 'awaiting_response' + end + + it 'assigns the search' do + request_filter = described_class.new + request_filter.update_attributes(:search => 'lazy dog') + expect(request_filter.search).to eq 'lazy dog' + end + + it 'assigns the order' do + request_filter = described_class.new + request_filter.update_attributes(:order => 'created_at_asc') + expect(request_filter.order).to eq 'created_at_asc' + end + end + + describe '#order_options' do + + it 'returns a list of sort order options in label, parameter form' do + expected = [['Last updated', 'updated_at_desc'], + ['First created', 'created_at_asc'], + ['Title (A-Z)', 'title_asc']] + expect(described_class.new.order_options).to eq expected + end + end + + describe '#persisted?' do + + it 'returns false' do + expect(described_class.new.persisted?).to be false + end + + end + + describe '#results' do + + context 'when no attributes are supplied' do + + it 'sorts the requests by most recently updated' do + user = FactoryGirl.create(:user) + first_request = FactoryGirl.create(:info_request, :user => user) + second_request = FactoryGirl.create(:info_request, :user => user) + + request_filter = described_class.new + expect(request_filter.results(user.info_requests(true))) + .to eq([second_request, first_request]) + end + end + + it 'applies a sort order' do + user = FactoryGirl.create(:user) + first_request = FactoryGirl.create(:info_request, :user => user) + second_request = FactoryGirl.create(:info_request, :user => user) + + request_filter = described_class.new + request_filter.update_attributes(:order => 'created_at_asc') + expect(request_filter.results(user.info_requests(true))) + .to eq([first_request, second_request]) + end + + it 'applies a filter ' do + user = FactoryGirl.create(:user) + complete_request = FactoryGirl.create(:successful_request, + :user => user) + incomplete_request = FactoryGirl.create(:info_request, + :user => user) + + request_filter = described_class.new + request_filter.update_attributes(:filter => 'complete') + expect(request_filter.results(user.info_requests(true))) + .to eq([complete_request]) + end + + it 'applies a search to the request titles' do + user = FactoryGirl.create(:user) + dog_request = FactoryGirl.create(:info_request, + :title => 'Where is my dog?', + :user => user) + cat_request = FactoryGirl.create(:info_request, + :title => 'Where is my cat?', + :user => user) + request_filter = described_class.new + request_filter.update_attributes(:search => 'CAT') + expect(request_filter.results(user.info_requests(true))) + .to eq([cat_request]) + end + + end + +end From 2320c17ebe8a10d50efd5cf4360f667353d2dca5 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 29 Nov 2016 17:00:57 +0000 Subject: [PATCH 091/311] Split request list into partial - create partial for blank slate. --- .../info_requests/_no_requests.html.erb | 1 + .../info_requests/_request_list.html.erb | 30 ++++++++++++++++ .../info_requests/index.html.erb | 34 +++---------------- 3 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 app/views/alaveteli_pro/info_requests/_no_requests.html.erb create mode 100644 app/views/alaveteli_pro/info_requests/_request_list.html.erb diff --git a/app/views/alaveteli_pro/info_requests/_no_requests.html.erb b/app/views/alaveteli_pro/info_requests/_no_requests.html.erb new file mode 100644 index 0000000000..245cb67a47 --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/_no_requests.html.erb @@ -0,0 +1 @@ +No Requests! diff --git a/app/views/alaveteli_pro/info_requests/_request_list.html.erb b/app/views/alaveteli_pro/info_requests/_request_list.html.erb new file mode 100644 index 0000000000..19519d960c --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/_request_list.html.erb @@ -0,0 +1,30 @@ +
    + +
    +
    +

    <%= _('All requests') %>

    +
    +
    + + <%= form_for( @request_filter, :url => '', :method => 'get', :html => { :class => 'requests__filter_form' }) do |f| %> + +
    + + <%= f.select :order, @request_filter.order_options %> +
    + <% end %> + +
    + +
    + + <%= render partial: 'info_request', :collection => @info_requests %> + <%= will_paginate(@info_requests, :class => "paginator") %> + +
    + +
    + +
    diff --git a/app/views/alaveteli_pro/info_requests/index.html.erb b/app/views/alaveteli_pro/info_requests/index.html.erb index efa7062695..c990415a19 100644 --- a/app/views/alaveteli_pro/info_requests/index.html.erb +++ b/app/views/alaveteli_pro/info_requests/index.html.erb @@ -12,35 +12,11 @@ -
    - -
    -
    -

    <%= _('All requests') %>

    -
    -
    - <%= form_for( @request_filter, :url => '', :method => 'get', :html => { :class => 'requests__filter_form' }) do |f| %> - -
    - - <%= f.select :order, @request_filter.order_options %> -
    - <% end %> - -
    - -
    - - <%= render partial: 'info_request', :collection => @info_requests %> - <%= will_paginate(@info_requests, :class => "paginator") %> - -
    - -
    - -
    + <% if @user.info_requests.empty? %> + <%= render partial: 'no_requests' %> + <% else %> + <%= render partial: 'request_list' %> + <% end %> From 11bdf11bca633b8f1d248a9668b59185ef47a4d9 Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Thu, 1 Dec 2016 15:38:56 +0000 Subject: [PATCH 092/311] add a body class when we're in alaveteli pro --- app/controllers/alaveteli_pro/base_controller.rb | 7 +++++++ app/views/layouts/default.html.erb | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/controllers/alaveteli_pro/base_controller.rb b/app/controllers/alaveteli_pro/base_controller.rb index 1a6d8e584d..ad319622cd 100644 --- a/app/controllers/alaveteli_pro/base_controller.rb +++ b/app/controllers/alaveteli_pro/base_controller.rb @@ -6,7 +6,9 @@ # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class AlaveteliPro::BaseController < ApplicationController + before_filter :pro_user_authenticated? + before_filter :set_pro_flag # A pro-specific version of user_authenticated? that pro controller actions # can use to check for (or force a login for) an authenticated pro user @@ -31,4 +33,9 @@ def pro_user_authenticated?(reason_params = nil) end return false end + + def set_pro_flag + @in_pro_area = true + end + end diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb index ca2182fe80..e5c8094e44 100644 --- a/app/views/layouts/default.html.erb +++ b/app/views/layouts/default.html.erb @@ -39,7 +39,7 @@ <%= render :partial => 'general/opengraph_tags' %> <%= render :partial => 'general/before_head_end' %> - + <% if is_admin? %> <%= render :partial => 'admin_general/admin_navbar' %> <% end %> From 19f8aa9ef6e01f1ef1fd5484192a6068d1cf2a88 Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Fri, 2 Dec 2016 10:44:43 +0000 Subject: [PATCH 093/311] New dashboard layout - Use Foundation's grid system - Make mobile look ok - Make the pro dashboard wide like the mockups - Add basics of grid in to places we'll probably need it in the future --- .../responsive/_global_layout.scss | 13 ++++++ .../alaveteli_pro/_dashboard_layout.scss | 42 +++++++++++-------- .../info_requests/_no_requests.html.erb | 12 +++++- .../info_requests/index.html.erb | 23 +++++----- 4 files changed, 62 insertions(+), 28 deletions(-) diff --git a/app/assets/stylesheets/responsive/_global_layout.scss b/app/assets/stylesheets/responsive/_global_layout.scss index 2b19a24b64..23c20183ac 100644 --- a/app/assets/stylesheets/responsive/_global_layout.scss +++ b/app/assets/stylesheets/responsive/_global_layout.scss @@ -6,11 +6,24 @@ } } +.wrapper--wide { + // used in alaveteli-pro for wide outside gutters of the layout container + // the rest of the grid remains the same + @extend .wrapper; + max-width: 72em; +} + /* Legacy ID-based wrapper */ #wrapper { @extend .wrapper; } +.alaveteli-pro { + #wrapper { + @extend .wrapper--wide; + } +} + /* A global 12 column element provides padding for all pages */ .content { @include grid-column(12); diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss index fb6cde49a3..afac5346ec 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss @@ -1,34 +1,47 @@ /* Layout for the dashboard */ -.dashboard-header, -.dashboard-body { - @include flexbox; - padding: 1em; -} +$dashboard-collapse: 50em; //where the dashboard layout needs to change .dashboard-left-column { - width: 30%; - padding-left: 2em; - padding-right: 1.5em; + @include grid-column(12); + @include ie8{ + padding-left: 0.9375em; + padding-right: 0.9375em; + } + @include respond-min( $dashboard-collapse ){ + @include grid-column(3); + } } .dashboard-right-column { - width: 70%; - padding-right: 2em; + @include grid-column(12); + @include ie8{ + padding-left: 0.9375em; + padding-right: 0.9375em; + } + @include respond-min( $dashboard-collapse ){ + @include grid-column(9); + } } .dashboard-header { margin-top: 2em; - padding-left: 2em; + padding-left: 0.9375em; + padding-right: 0.9375em; h1 { + @include grid-column(12); margin-top: 0.5em; - margin-bottom: 0.5em; + margin-bottom: 1em; display:block; width:100%; font-weight: bold; } } +.dashboard-body { + padding-top: 2em; +} + .dashboard-profile__image { float:left; margin-right: 1em; @@ -51,11 +64,6 @@ float: right; } -h1 { - margin-left: 1em; - margin-bottom: 1em; -} - .dashboard-folders, .dashboard-sub-folders { margin: 0; diff --git a/app/views/alaveteli_pro/info_requests/_no_requests.html.erb b/app/views/alaveteli_pro/info_requests/_no_requests.html.erb index 245cb67a47..01f36ed4b9 100644 --- a/app/views/alaveteli_pro/info_requests/_no_requests.html.erb +++ b/app/views/alaveteli_pro/info_requests/_no_requests.html.erb @@ -1 +1,11 @@ -No Requests! +
    diff --git a/app/views/alaveteli_pro/info_requests/index.html.erb b/app/views/alaveteli_pro/info_requests/index.html.erb index c990415a19..ea66f86302 100644 --- a/app/views/alaveteli_pro/info_requests/index.html.erb +++ b/app/views/alaveteli_pro/info_requests/index.html.erb @@ -1,22 +1,25 @@
    -

    <%= _('Requests') %>

    +
    +

    <%= _('Requests') %>

    +
    +
    +
    -
    + <%= render partial: 'alaveteli_pro/dashboard/projects' %> - <%= render partial: 'alaveteli_pro/dashboard/projects' %> +
    -
    - - <% if @user.info_requests.empty? %> - <%= render partial: 'no_requests' %> - <% else %> - <%= render partial: 'request_list' %> - <% end %> + <% if @user.info_requests.empty? %> + <%= render partial: 'no_requests' %> + <% else %> + <%= render partial: 'request_list' %> + <% end %> +
    From 4d66255c535509dfc6bf6778f0ec3eb7a0f4545d Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Fri, 2 Dec 2016 11:59:23 +0000 Subject: [PATCH 094/311] added blank slate styles to empty request list --- .../alaveteli-pro/blank-slate-requests.png | Bin 0 -> 10441 bytes .../alaveteli_pro/_dashboard_layout.scss | 9 +-- .../alaveteli_pro/_dashboard_style.scss | 5 +- .../alaveteli_pro/_requests_layout.scss | 40 +++++++++++-- .../alaveteli_pro/_requests_style.scss | 55 ++++++++++++++++++ .../info_requests/_request_list.html.erb | 2 +- 6 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 app/assets/images/alaveteli-pro/blank-slate-requests.png diff --git a/app/assets/images/alaveteli-pro/blank-slate-requests.png b/app/assets/images/alaveteli-pro/blank-slate-requests.png new file mode 100644 index 0000000000000000000000000000000000000000..4a2cf783f9b18b2e900ffd844b9a84ee5889ec61 GIT binary patch literal 10441 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 hBhBNxu6OqVniQZ{t^o2+@BO*(#uc;6B?hw5qI literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss index afac5346ec..25d10c84f2 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss @@ -30,10 +30,11 @@ $dashboard-collapse: 50em; //where the dashboard layout needs to change padding-right: 0.9375em; h1 { @include grid-column(12); - margin-top: 0.5em; - margin-bottom: 1em; - display:block; - width:100%; + font-size: 3em; + margin-top: 0.4em; + margin-bottom: 0.5em; + display: block; + width: 100%; font-weight: bold; } } diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss index 6e80a995d7..0590542091 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss @@ -6,17 +6,16 @@ .dashboard-header { background-color: #e6e4df; - border: 1px solid #e1dfda; } .dashboard-nav { list-style-type: none; list-style: none; - border-bottom: 1px solid #DDDDDD; + border-bottom: 1px solid #ccc; } .dashboard-nav__item { - border-bottom: 1px solid #DDDDDD; + border-bottom: 1px solid #ccc; // To overlap the bottom border with that of the containing list, making it // appear continuous margin-bottom: -2px; diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss index ae289c2340..1e7860d737 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss @@ -2,29 +2,61 @@ .requests__folder-title { h2 { + font-size: 2.375em; margin-top: 0; + font-weight: bold; + margin-bottom: 0.75em; } } .requests__header { - @include flexbox; padding: 0.5em; + width: 100%; + form { + margin-bottom: 0; + } } .requests__filter_form { @include flexbox; + flex-wrap: wrap; + width: 100%; +} + +.requests__search { + @include flex(1 0 100%); + margin-bottom: 1em; + @include respond-min( $dashboard-collapse ){ + @include flex(7 0 0); + margin-bottom: 0; + } + input[type=text]{ + width: 100%; + max-width: none; + border: 1px solid #ccc; + } } -.requests__search, .requests__sort { - @include flex(0 0 33%); - margin-right: 1em; + @include flex(1 0 100%); + max-width: 12em; + @include respond-min( 24em ){ + @include flex(1 2 50%); + } + @include respond-min( $dashboard-collapse ){ + @include flex(4 2 0); + text-align: right; + } +} +.requests__search, +.requests__sort { input, select, label { display: inline-block; margin-bottom: 0; } select { max-width: 60%; + padding-right: 2em; } } diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss index fd8833898a..cb459778a6 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss @@ -2,6 +2,7 @@ .requests__header { background-color: #f3f1eb; + border-bottom: 1px solid #ccc; } .request { @@ -24,3 +25,57 @@ font-size: 0.8em; color: #999; } + +.requests__sort, +.requests__filters { + label { + color: #777; + } + select { + border-color: transparent; + background-color: transparent; + cursor: pointer; + &:hover, + &:active, + &:focus { + border-color: #ccc; + background-color: #fff; + } + } +} + +/* Blank slate - when there are no requests to show */ +.blank-slate { + +} + +.blank-slate__requests { + @include respond-min( $dashboard-collapse ){ + background-image: image-url('/assets/alaveteli-pro/blank-slate-requests.png'); + background-repeat: no-repeat; + background-size: 676px 344px; + background-position: left bottom; + min-height: 344px; + padding: 2em 0; + } +} + +.blank-slate__message { + background-color: #333; + color: #fff; + padding: 1.5em 2em 2em; + margin-bottom: 2em; + @include respond-min( $dashboard-collapse ){ + max-width: 32em; + margin-left: 5em; + } + h2 { + color: #fff; + font-size: 1.3125em; + } + + p { + font-size: 0.875em; + line-height: 1.4em; + } +} diff --git a/app/views/alaveteli_pro/info_requests/_request_list.html.erb b/app/views/alaveteli_pro/info_requests/_request_list.html.erb index 19519d960c..3e006ad7c3 100644 --- a/app/views/alaveteli_pro/info_requests/_request_list.html.erb +++ b/app/views/alaveteli_pro/info_requests/_request_list.html.erb @@ -11,7 +11,7 @@ <%= f.text_field :search, :placeholder => _('Search') %>
    - + <%= f.select :order, @request_filter.order_options %>
    <% end %> From a6b189844be01db9d1712d51e904731edcc64773 Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Fri, 2 Dec 2016 15:01:35 +0000 Subject: [PATCH 095/311] improve layout of requests in lists on pro dashboard - Add mobile styles - Let flexbox do its thing - Adjust type sizes and spacing --- .../alaveteli_pro/_requests_layout.scss | 62 ++++++++++++++++--- .../alaveteli_pro/_requests_style.scss | 19 ++++-- .../info_requests/_info_request.html.erb | 8 +-- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss index 1e7860d737..2e2984be49 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss @@ -33,6 +33,7 @@ width: 100%; max-width: none; border: 1px solid #ccc; + font-size: 0.875em; } } @@ -67,15 +68,23 @@ .request { padding-top: 1em; padding-bottom: 1em; - margin-bottom: 1em; } .request__title { margin-top: 0; + margin-bottom: 0.75em; } .request__meta { @include flexbox; + flex-wrap: wrap; +} + +.request__meta__label { + margin-top: 1em; + @include respond-min( $dashboard-collapse ){ + margin-top: 0; + } } .request__recipient, @@ -83,24 +92,59 @@ .request__updated, .request__status, .request__actions { - margin-right: 1em; + @include respond-min( $dashboard-collapse ){ + margin-right: 1em; + } + p { + margin-bottom: 0; + } } // TODO - the flex() rules for these add up to way less than 100%, yet the // elements still overflow their container. Not sure why. I've hacked it by // reducing the width of the select in the last element and floating it right -.request__recipient, -.request__status { - @include flex(0 0 15%); +.request__recipient { + @include flex(1 0 100%); + @include respond-min( 24em ){ + @include flex(1 0 75%); + } + @include respond-min( $dashboard-collapse ){ + @include flex(8 0 0); + } } .request__created, .request__updated { - @include flex(0 0 15%); + @include flex(1 0 50%); + @include respond-min( 24em ){ + @include flex(1 0 25%); + } + @include respond-min( $dashboard-collapse ){ + @include flex(3 0 0); + } +} + + +.request__status { + @include flex(1 0 60%); + @include respond-min( 24em ){ + @include flex(1 0 40%); + } + @include respond-min( $dashboard-collapse ){ + @include flex(5 0 0); + } } .request__actions { - @include flex(0 0 31%); // No idea why this has to be 31% in particular + align-self: flex-end; + @include flex(1 0 40%); + @include respond-min( 24em ){ + @include flex(1 0 35%); + } + @include respond-min( $dashboard-collapse ){ + @include flex(3 0 0); + align-self: auto; + } @include clearfix; margin-right: 0; .actions { @@ -108,6 +152,10 @@ float: right; } + select { + margin-bottom: 0; + } + } .request__meta__label { diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss index cb459778a6..66cb6a3a6e 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss @@ -2,15 +2,23 @@ .requests__header { background-color: #f3f1eb; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid #ddd; } .request { - border-bottom: 1px solid #ccc; + border-bottom: 1px solid #ddd; } .request__title { - font-size: 1.25em; + font-size: 1.1875em; // 19px + a { + text-decoration: none; + &:hover, + &:active, + &:focus { + text-decoration: underline; + } + } } .request__recipient, @@ -18,11 +26,12 @@ .request__updated, .request__status, .request__actions { - font-size: 0.9em; + font-size: 0.875em; // 14px } .request__meta__label { - font-size: 0.8em; + font-size: 0.75em; // 12px + line-height: 1em; color: #999; } diff --git a/app/views/alaveteli_pro/info_requests/_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_info_request.html.erb index 93a557caec..af32bafa88 100644 --- a/app/views/alaveteli_pro/info_requests/_info_request.html.erb +++ b/app/views/alaveteli_pro/info_requests/_info_request.html.erb @@ -7,22 +7,22 @@
    <%= _('Request to') %> - <%= info_request.public_body.name %> +

    <%= info_request.public_body.name %>

    <%= _('Created') %> - <%= info_request.created_at.strftime('%d-%m-%Y') %> +

    <%= info_request.created_at.strftime('%d-%m-%Y') %>

    <%= _('Updated') %> - <%= info_request.updated_at.strftime('%d-%m-%Y') %> +

    <%= info_request.updated_at.strftime('%d-%m-%Y') %>

    <%= _('Status') %> - <%= InfoRequest.get_status_description(info_request.described_state) %> +

    <%= InfoRequest.get_status_description(info_request.described_state) %>

    From 965fc172d7b51d68350a1cf3ae6f40f51dab37b4 Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Mon, 5 Dec 2016 10:28:03 +0000 Subject: [PATCH 096/311] Improvements to project navigation in pro dashboard - Don't render a link to empty request types - Styling improvments - Accessibility improvements --- .../alaveteli_pro/_dashboard_layout.scss | 12 +++--- .../alaveteli_pro/_dashboard_style.scss | 29 +++++++++---- .../dashboard/_projects.html.erb | 43 ++++++++++++++----- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss index 25d10c84f2..74c0833ef4 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss @@ -72,19 +72,17 @@ $dashboard-collapse: 50em; //where the dashboard layout needs to change } .dashboard-sub-folders { - margin: 1em 0 2em 0; + margin: 0 0 2em 0; } .dashboard-folder { - margin-bottom: 0.5em; - font-size: 0.9em; + margin-bottom: 0; + font-size: 0.875em; } -.dashboard-folder a, -.dashboard-folder a:hover, -.dashboard-folder--disabled .fake-link { +.dashboard-folder__name { display: block; - padding: 0.5em 1em 0.5em 1em; + padding: 0.6em 1em 0.6em 1em; width: 100%; } diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss index 0590542091..ac6ffdb36a 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss @@ -39,16 +39,24 @@ list-style-type: none; } -.dashboard-folder a, -.dashboard-folder a:hover, -.dashboard-folder--disabled .fake-link { - color: black; - border-bottom: 1px solid #ccc; +.dashboard-folder__name { + color: #999; + border-bottom: 1px solid #ddd; text-decoration: none; } + +a.dashboard-folder__name, +a.dashboard-folder__name:visited { + color: #333; + &:hover, + &:active, + &:focus { + + } +} + .dashboard-folder a:hover { - color: black; - border-bottom-color: orange; + background-color: #e6e4df; } .dashboard-folder--disabled { @@ -56,14 +64,17 @@ } .dashboard-folder--top-level { - background-color: #E9E9E9; - margin-bottom: 1em; + background-color: #f3f1eb; border-bottom: transparent; } .dashboard-folder--selected { font-weight: bold; font-size: 1em; + background-color: transparent; + a { + border-color: #333; + } } .dashboard-todo__list { diff --git a/app/views/alaveteli_pro/dashboard/_projects.html.erb b/app/views/alaveteli_pro/dashboard/_projects.html.erb index d4b6e2a303..52506f9bae 100644 --- a/app/views/alaveteli_pro/dashboard/_projects.html.erb +++ b/app/views/alaveteli_pro/dashboard/_projects.html.erb @@ -1,30 +1,51 @@ + + From f3b96868854f5549f71ba35745ee6afc532985a1 Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Mon, 5 Dec 2016 11:23:10 +0000 Subject: [PATCH 097/311] Move 'drafts' item into own dashboard navigation section --- .../responsive/alaveteli_pro/_dashboard_layout.scss | 8 ++++++-- app/views/alaveteli_pro/dashboard/_projects.html.erb | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss index 74c0833ef4..f645cf070f 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss @@ -71,18 +71,22 @@ $dashboard-collapse: 50em; //where the dashboard layout needs to change padding: 0; } +.dashboard-folders { + margin-bottom: 2em; +} + .dashboard-sub-folders { margin: 0 0 2em 0; } .dashboard-folder { margin-bottom: 0; - font-size: 0.875em; + font-size: 0.9375em; //15px } .dashboard-folder__name { display: block; - padding: 0.6em 1em 0.6em 1em; + padding: 0.666em 1em 0.666em 1em; width: 100%; } diff --git a/app/views/alaveteli_pro/dashboard/_projects.html.erb b/app/views/alaveteli_pro/dashboard/_projects.html.erb index 52506f9bae..49871409a6 100644 --- a/app/views/alaveteli_pro/dashboard/_projects.html.erb +++ b/app/views/alaveteli_pro/dashboard/_projects.html.erb @@ -31,7 +31,7 @@
    From 908653b24a4055a8607b30d793dd47ebc4c737bd Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 6 Dec 2016 14:27:50 +0000 Subject: [PATCH 100/311] Add draft handling. --- .../alaveteli_pro/info_requests_controller.rb | 3 +- app/models/request_filter.rb | 17 ++++-- .../_draft_info_request.html.erb | 38 +++++++++++++ .../info_requests/_request_list.html.erb | 6 ++- .../info_requests/index.html.erb | 2 +- spec/models/request_filter_spec.rb | 53 +++++++++++++++---- 6 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 app/views/alaveteli_pro/info_requests/_draft_info_request.html.erb diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index 4193aac373..3509645984 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -14,9 +14,10 @@ def index if params[:request_filter] @request_filter.update_attributes(params[:request_filter]) end - info_requests = @request_filter.results(current_user.info_requests) + info_requests = @request_filter.results(current_user) @info_requests = info_requests.paginate :page => params[:page], :per_page => 10 + end def new diff --git a/app/models/request_filter.rb b/app/models/request_filter.rb index c97ee6f833..7386df4d36 100644 --- a/app/models/request_filter.rb +++ b/app/models/request_filter.rb @@ -16,11 +16,18 @@ def attributes=(attributes) end def order_options - order_attributes.map {|atts| [atts[:label], atts[:param]] } + order_attributes.map { |atts| [atts[:label], atts[:param]] } end - def results(info_requests) - info_requests = info_requests.send(filter_value) if filter_value + def results(user) + if filter == 'draft' + info_requests = user.draft_info_requests + else + info_requests = user.info_requests + end + if filter_value + info_requests = info_requests.send(filter_value) + end if search info_requests = info_requests.where("title ILIKE :q", q: "%#{ search }%") end @@ -67,8 +74,8 @@ def default_filters [ { :param => '', :value => nil, :label => 'All Requests' }, - { :param => '', - :value => nil, + { :param => 'drafts', + :value => '', :label => 'Drafts' }, ] end diff --git a/app/views/alaveteli_pro/info_requests/_draft_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_draft_info_request.html.erb new file mode 100644 index 0000000000..9b0eb85b14 --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/_draft_info_request.html.erb @@ -0,0 +1,38 @@ +
    +

    + <% if draft_info_request.embargo_duration %> + Embargoed + <% end %> + <%= link_to draft_info_request.title, + new_alaveteli_pro_info_request_path(:draft_id => draft_info_request.id) %> +

    + +
    + +
    + <%= _('Request to') %> +

    + <% if draft_info_request.public_body %> + <%= draft_info_request.public_body.name %> + <% end %> +

    +
    + +
    + <%= _('Created') %> +

    <%= draft_info_request.created_at.strftime('%d-%m-%Y') %>

    +
    + +
    + <%= _('Updated') %> +

    <%= draft_info_request.updated_at.strftime('%d-%m-%Y') %>

    +
    + +
    + <%= _('Status') %> +

    <%= _('Draft') %>

    +
    + +
    + +
    \ No newline at end of file diff --git a/app/views/alaveteli_pro/info_requests/_request_list.html.erb b/app/views/alaveteli_pro/info_requests/_request_list.html.erb index 3e006ad7c3..a5ebbe7e1d 100644 --- a/app/views/alaveteli_pro/info_requests/_request_list.html.erb +++ b/app/views/alaveteli_pro/info_requests/_request_list.html.erb @@ -20,7 +20,11 @@
    - <%= render partial: 'info_request', :collection => @info_requests %> + <% if @request_filter.filter == 'draft' %> + <%= render partial: 'draft_info_request', :collection => @info_requests %> + <% else %> + <%= render partial: 'info_request', :collection => @info_requests %> + <% end %> <%= will_paginate(@info_requests, :class => "paginator") %>
    diff --git a/app/views/alaveteli_pro/info_requests/index.html.erb b/app/views/alaveteli_pro/info_requests/index.html.erb index ea66f86302..d145018dd7 100644 --- a/app/views/alaveteli_pro/info_requests/index.html.erb +++ b/app/views/alaveteli_pro/info_requests/index.html.erb @@ -14,7 +14,7 @@ - <% if @user.info_requests.empty? %> + <% if @user.info_requests.empty? && @user.draft_info_requests.empty? %> <%= render partial: 'no_requests' %> <% else %> <%= render partial: 'request_list' %> diff --git a/spec/models/request_filter_spec.rb b/spec/models/request_filter_spec.rb index 6eb7645403..365d3c1d81 100644 --- a/spec/models/request_filter_spec.rb +++ b/spec/models/request_filter_spec.rb @@ -43,46 +43,42 @@ end describe '#results' do + let(:user){ FactoryGirl.create(:user) } context 'when no attributes are supplied' do it 'sorts the requests by most recently updated' do - user = FactoryGirl.create(:user) first_request = FactoryGirl.create(:info_request, :user => user) second_request = FactoryGirl.create(:info_request, :user => user) request_filter = described_class.new - expect(request_filter.results(user.info_requests(true))) + expect(request_filter.results(user)) .to eq([second_request, first_request]) end end it 'applies a sort order' do - user = FactoryGirl.create(:user) first_request = FactoryGirl.create(:info_request, :user => user) second_request = FactoryGirl.create(:info_request, :user => user) request_filter = described_class.new request_filter.update_attributes(:order => 'created_at_asc') - expect(request_filter.results(user.info_requests(true))) + expect(request_filter.results(user)) .to eq([first_request, second_request]) end - it 'applies a filter ' do - user = FactoryGirl.create(:user) + it 'applies a filter' do complete_request = FactoryGirl.create(:successful_request, :user => user) incomplete_request = FactoryGirl.create(:info_request, :user => user) - request_filter = described_class.new request_filter.update_attributes(:filter => 'complete') - expect(request_filter.results(user.info_requests(true))) + expect(request_filter.results(user)) .to eq([complete_request]) end it 'applies a search to the request titles' do - user = FactoryGirl.create(:user) dog_request = FactoryGirl.create(:info_request, :title => 'Where is my dog?', :user => user) @@ -91,10 +87,45 @@ :user => user) request_filter = described_class.new request_filter.update_attributes(:search => 'CAT') - expect(request_filter.results(user.info_requests(true))) + expect(request_filter.results(user)) .to eq([cat_request]) end - end + context 'when the filter is "draft"' do + it 'returns draft requests' do + draft_request = FactoryGirl.create(:draft_info_request, + :user => user) + request_filter = described_class.new + request_filter.update_attributes(:filter => 'draft') + expect(request_filter.results(user)) + .to eq([draft_request]) + end + + it 'applies a search to the request titles' do + dog_request = FactoryGirl.create(:draft_info_request, + :title => 'Where is my dog?', + :user => user) + cat_request = FactoryGirl.create(:draft_info_request, + :title => 'Where is my cat?', + :user => user) + request_filter = described_class.new + request_filter.update_attributes(:search => 'CAT', + :filter => 'draft') + expect(request_filter.results(user)) + .to eq([cat_request]) + end + + it 'applies a sort order' do + first_request = FactoryGirl.create(:draft_info_request, :user => user) + second_request = FactoryGirl.create(:draft_info_request, :user => user) + + request_filter = described_class.new + request_filter.update_attributes(:order => 'created_at_asc', + :filter => 'draft') + expect(request_filter.results(user)) + .to eq([first_request, second_request]) + end + end + end end From 5ac80bffe57a4c48da6de3f7e346088a7f32e5f1 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 6 Dec 2016 14:28:21 +0000 Subject: [PATCH 101/311] Link to published request pages. --- .../alaveteli_pro/info_requests/_info_request.html.erb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/alaveteli_pro/info_requests/_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_info_request.html.erb index 45afe187d2..c52e139fdb 100644 --- a/app/views/alaveteli_pro/info_requests/_info_request.html.erb +++ b/app/views/alaveteli_pro/info_requests/_info_request.html.erb @@ -2,8 +2,12 @@

    <% if info_request.embargo %> Embargoed + <% # TODO: insert embargoed request in progress url here %> + <%= info_request.title %> + <% else %> + <%= link_to info_request.title, show_request_path(info_request) %> <% end %> - <%= info_request.title %> +

    From 5d107dc9dea60f959882b0a19641c17a6017145e Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 6 Dec 2016 15:06:02 +0000 Subject: [PATCH 102/311] Use standard pagination. --- app/controllers/alaveteli_pro/info_requests_controller.rb | 6 ++++-- .../alaveteli_pro/info_requests/_request_list.html.erb | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index 3509645984..5079b3963a 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -15,8 +15,10 @@ def index @request_filter.update_attributes(params[:request_filter]) end info_requests = @request_filter.results(current_user) - @info_requests = info_requests.paginate :page => params[:page], - :per_page => 10 + @page = params[:page] || 1 + @per_page = 10 + @info_requests = info_requests.paginate :page => @page, + :per_page => @per_page end diff --git a/app/views/alaveteli_pro/info_requests/_request_list.html.erb b/app/views/alaveteli_pro/info_requests/_request_list.html.erb index a5ebbe7e1d..b5874c532b 100644 --- a/app/views/alaveteli_pro/info_requests/_request_list.html.erb +++ b/app/views/alaveteli_pro/info_requests/_request_list.html.erb @@ -25,8 +25,7 @@ <% else %> <%= render partial: 'info_request', :collection => @info_requests %> <% end %> - <%= will_paginate(@info_requests, :class => "paginator") %> - + <%= will_paginate WillPaginate::Collection.new(@page, @per_page, @info_requests.count) %>
    From 407619bcb1124b9246b5e6301968094e0da88532 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 6 Dec 2016 15:26:52 +0000 Subject: [PATCH 103/311] Show currently applied filter in header --- app/models/request_filter.rb | 28 ++++++++---- .../info_requests/_request_list.html.erb | 2 +- spec/models/request_filter_spec.rb | 44 +++++++++++++++++++ 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/app/models/request_filter.rb b/app/models/request_filter.rb index 7386df4d36..c8632a5073 100644 --- a/app/models/request_filter.rb +++ b/app/models/request_filter.rb @@ -10,7 +10,7 @@ def update_attributes(attributes = {}) end def attributes=(attributes) - self.filter = attributes[:filter] + self.filter = attributes[:filter] unless attributes[:filter].blank? self.order = attributes[:order] self.search = attributes[:search] end @@ -19,13 +19,17 @@ def order_options order_attributes.map { |atts| [atts[:label], atts[:param]] } end + def filter_label + filter_params.include?(@filter) ? filter_labels[@filter] : nil + end + def results(user) if filter == 'draft' info_requests = user.draft_info_requests else info_requests = user.info_requests end - if filter_value + if !filter_value.blank? info_requests = info_requests.send(filter_value) end if search @@ -71,12 +75,12 @@ def default_order end def default_filters - [ { :param => '', + [ { :param => nil, :value => nil, - :label => 'All Requests' }, - { :param => 'drafts', - :value => '', - :label => 'Drafts' }, + :label => _('All requests') }, + { :param => 'draft', + :value => nil, + :label => _('Drafts') }, ] end @@ -90,8 +94,16 @@ def filter_attributes default_filters + phase_filters end + def filter_options + filter_attributes.map {|atts| [atts[:label], atts[:param]] } + end + + def filter_labels + Hash[ filter_options ].invert + end + def filter_params - phase_filters.map{ |atts| atts[:param] } + filter_attributes.map{ |atts| atts[:param] } end def filter_values diff --git a/app/views/alaveteli_pro/info_requests/_request_list.html.erb b/app/views/alaveteli_pro/info_requests/_request_list.html.erb index b5874c532b..63f633b298 100644 --- a/app/views/alaveteli_pro/info_requests/_request_list.html.erb +++ b/app/views/alaveteli_pro/info_requests/_request_list.html.erb @@ -2,7 +2,7 @@
    -

    <%= _('All requests') %>

    +

    <%= @request_filter.filter_label %>

    diff --git a/spec/models/request_filter_spec.rb b/spec/models/request_filter_spec.rb index 365d3c1d81..5550207e63 100644 --- a/spec/models/request_filter_spec.rb +++ b/spec/models/request_filter_spec.rb @@ -22,6 +22,50 @@ request_filter.update_attributes(:order => 'created_at_asc') expect(request_filter.order).to eq 'created_at_asc' end + + it 'does not assign an empty filter' do + request_filter = described_class.new + request_filter.update_attributes(:filter => '') + expect(request_filter.filter).to be nil + end + + end + + describe '#filter_label' do + + def expect_label(label, filter) + request_filter = described_class.new + request_filter.update_attributes(:filter => filter) + expect(request_filter.filter_label).to eq label + end + + it 'is "All requests" when the filter is empty' do + expect_label('All requests', '') + end + + it 'is "Drafts" when the filter is "draft"' do + expect_label('Drafts', 'draft') + end + + it 'is "Awaiting response" when the filter is "awaiting_response"' do + expect_label('Awaiting response', 'awaiting_response') + end + + it 'is "Complete" when the filter is "complete"' do + expect_label('Complete', 'complete') + end + + it 'is "Clarification needed" when the filter is "clarification_needed"' do + expect_label('Clarification needed', 'clarification_needed') + end + + it 'is "Status unknown" when the filter is "other"' do + expect_label('Status unknown', 'other') + end + + it 'is "Response received" when the filter is "response_received"' do + expect_label('Response received', 'response_received') + end end describe '#order_options' do From 97ce4e805459d58488cf38027786bc301cbd8d5a Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 6 Dec 2016 15:27:06 +0000 Subject: [PATCH 104/311] Make order and search operate within filter. --- app/views/alaveteli_pro/info_requests/_request_list.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/alaveteli_pro/info_requests/_request_list.html.erb b/app/views/alaveteli_pro/info_requests/_request_list.html.erb index 63f633b298..84b5c64ad9 100644 --- a/app/views/alaveteli_pro/info_requests/_request_list.html.erb +++ b/app/views/alaveteli_pro/info_requests/_request_list.html.erb @@ -14,6 +14,7 @@ <%= f.select :order, @request_filter.order_options %>
    + <%= f.hidden_field :filter %> <% end %>
    From 2c15dc4da16841022ec369cbb52611c27544140f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 6 Dec 2016 15:29:02 +0000 Subject: [PATCH 105/311] Change "Status unknown" to "Other" --- app/models/info_request/state.rb | 2 +- spec/models/request_filter_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/info_request/state.rb b/app/models/info_request/state.rb index 5ce432c4dc..10eccc1065 100644 --- a/app/models/info_request/state.rb +++ b/app/models/info_request/state.rb @@ -34,7 +34,7 @@ def self.phases scope: :clarification_needed }, { name: _('Complete'), scope: :complete }, - { name: _('Status unknown'), + { name: _('Other'), scope: :other } ] end diff --git a/spec/models/request_filter_spec.rb b/spec/models/request_filter_spec.rb index 5550207e63..4c8e79b1e9 100644 --- a/spec/models/request_filter_spec.rb +++ b/spec/models/request_filter_spec.rb @@ -59,8 +59,8 @@ def expect_label(label, filter) expect_label('Clarification needed', 'clarification_needed') end - it 'is "Status unknown" when the filter is "other"' do - expect_label('Status unknown', 'other') + it 'is "Other" when the filter is "other"' do + expect_label('Other', 'other') end it 'is "Response received" when the filter is "response_received"' do From 1f80be76ae1ba9294b6155ef278a03c3a42bb19f Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Tue, 6 Dec 2016 15:48:03 +0000 Subject: [PATCH 106/311] Extend search to request body and authority name Actually includes the text of any outgoing message. --- app/models/request_filter.rb | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/app/models/request_filter.rb b/app/models/request_filter.rb index c8632a5073..1ff53232ba 100644 --- a/app/models/request_filter.rb +++ b/app/models/request_filter.rb @@ -25,17 +25,36 @@ def filter_label def results(user) if filter == 'draft' - info_requests = user.draft_info_requests + draft_info_request_results(user) else - info_requests = user.info_requests + info_request_results(user) end + end + + def draft_info_request_results(user) + info_requests = user.draft_info_requests + info_requests = info_requests + .includes(:public_body) + .where("title ILIKE :q + OR body ILIKE :q + OR public_bodies.name ILIKE :q", + q: "%#{ search }%") + info_requests.reorder("draft_info_requests.#{order_value}") + end + + def info_request_results(user) + info_requests = user.info_requests if !filter_value.blank? info_requests = info_requests.send(filter_value) end - if search - info_requests = info_requests.where("title ILIKE :q", q: "%#{ search }%") - end - info_requests.reorder(order_value) + info_requests = info_requests + .includes(:public_body) + .includes(:outgoing_messages) + .where("title ILIKE :q + OR outgoing_messages.body ILIKE :q + OR public_bodies.name ILIKE :q", + q: "%#{ search }%") + info_requests.reorder("info_requests.#{order_value}") end def persisted? From e0109b3d27c994fd6027941666a76fa2f5cb1a40 Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Wed, 7 Dec 2016 10:00:18 +0000 Subject: [PATCH 107/311] Add a sumbit button to the request list search box --- .../images/alaveteli-pro/search--light.png | Bin 0 -> 493 bytes .../alaveteli_pro/_requests_layout.scss | 22 ++++++++++++++++++ .../info_requests/_request_list.html.erb | 5 ++++ 3 files changed, 27 insertions(+) create mode 100644 app/assets/images/alaveteli-pro/search--light.png diff --git a/app/assets/images/alaveteli-pro/search--light.png b/app/assets/images/alaveteli-pro/search--light.png new file mode 100644 index 0000000000000000000000000000000000000000..668484bf705ab0f5184567d78cce96f98bc43dad GIT binary patch literal 493 zcmVKnd7EY#>##p_UM1o4+WVOionE(EFQB~(;Jv@HJt*iV_2C1=T?KHW~w{`{WM%Ns4bx2 zO}ApPS>?d&nxp-Gzc$Rj10K-}i6 '', :method => 'get', :html => { :class => 'requests__filter_form' }) do |f| %>
    From 00baa72ec27f4e673b12b619307c6f6f160d9b8b Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Thu, 8 Dec 2016 09:55:21 +0000 Subject: [PATCH 108/311] Add phase icons to dashboard request list --- .../images/alaveteli-pro/pro-status-icons.png | Bin 0 -> 8198 bytes .../alaveteli_pro/_dashboard_layout.scss | 3 + .../alaveteli_pro/_dashboard_style.scss | 71 +++++++++++++++++- .../alaveteli_pro/_requests_layout.scss | 10 +++ .../dashboard/_projects.html.erb | 3 +- .../info_requests/_info_request.html.erb | 2 +- 6 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 app/assets/images/alaveteli-pro/pro-status-icons.png diff --git a/app/assets/images/alaveteli-pro/pro-status-icons.png b/app/assets/images/alaveteli-pro/pro-status-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..dd7ecd269ebd9dc39230b58213a2e9018fa25cf1 GIT binary patch literal 8198 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 XBU~Imon*{lAM2%>&Wm3v*5UsLkSLD< literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss index f645cf070f..d2a5b5c9db 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_layout.scss @@ -77,6 +77,9 @@ $dashboard-collapse: 50em; //where the dashboard layout needs to change .dashboard-sub-folders { margin: 0 0 2em 0; + .dashboard-folder__name { + padding-left: 0.5em; + } } .dashboard-folder { diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss index ac6ffdb36a..3401f94e8c 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss @@ -20,7 +20,9 @@ // appear continuous margin-bottom: -2px; a, - a:hover { + a:hover, + a:active, + a:focus { font-weight: normal; color: white; text-decoration: none; @@ -55,8 +57,14 @@ a.dashboard-folder__name:visited { } } -.dashboard-folder a:hover { - background-color: #e6e4df; +.dashboard-folder { + a { + &:hover, + &:active, + &:focus { + background-color: #e6e4df; + } + } } .dashboard-folder--disabled { @@ -98,3 +106,60 @@ a.dashboard-folder__name:visited { .dashboard-activity__item__time { color: #999; } + + +.status-icon { + background-image: image-url('/assets/alaveteli-pro/pro-status-icons.png'); + background-position: 0 0; + background-size: 44px 283px; + width: 22px; + height: 22px; + display: inline-block; +} + +.status-icon--awaiting-response { + background-position: 0 -63px; +} + +.status-icon--response-received { + background-position: 0 -127px; +} + +.status-icon--clarification-needed { + background-position: 0 -192px; +} + +.status-icon--complete { + background-position: 0 1px; +} + +.status-icon--other { + background-position: 0 -256px; +} + +.dashboard-folder { + .status-icon { + position: relative; + top: 4px; + } +} + +.dashboard-folder { + a { + &:hover, + &:active, + &:focus { + .status-icon { + //changes to darker style for rollover + background-position-x: -23px; + } + } + } +} + +.dashboard-folder__name--disabled { + .status-icon { + //make it less prominent when the folder name isn't clickable + opacity: 0.5; + } +} diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss index 92243c38f1..7ab2280588 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss @@ -148,6 +148,8 @@ .request__status { + position: relative; + padding-left: 25px; @include flex(1 0 60%); @include respond-min( 24em ){ @include flex(1 0 40%); @@ -155,6 +157,14 @@ @include respond-min( $dashboard-collapse ){ @include flex(5 0 0); } + p { + + } + .status-icon { + position: absolute; + bottom: 8px; + left: 0; + } } .request__actions { diff --git a/app/views/alaveteli_pro/dashboard/_projects.html.erb b/app/views/alaveteli_pro/dashboard/_projects.html.erb index 49871409a6..2630567dd3 100644 --- a/app/views/alaveteli_pro/dashboard/_projects.html.erb +++ b/app/views/alaveteli_pro/dashboard/_projects.html.erb @@ -13,8 +13,9 @@ <% if @user.info_requests.send(phase[:scope]).count != 0 %> <% else %> - + <% end %> + <%= phase[:name] %> <%= @user.info_requests.send(phase[:scope]).count %> diff --git a/app/views/alaveteli_pro/info_requests/_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_info_request.html.erb index c52e139fdb..561edf342f 100644 --- a/app/views/alaveteli_pro/info_requests/_info_request.html.erb +++ b/app/views/alaveteli_pro/info_requests/_info_request.html.erb @@ -29,7 +29,7 @@
    <%= _('Status') %> -

    <%= InfoRequest.get_status_description(info_request.described_state) %>

    +

    <%= InfoRequest.get_status_description(info_request.described_state) %>

    From 2b05a2b20894fb5acb46dbd42c16089543d632c0 Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Thu, 8 Dec 2016 13:03:23 +0000 Subject: [PATCH 109/311] add HTML stub of search information, make it clearable --- .../images/alaveteli-pro/clear-icon.png | Bin 0 -> 576 bytes .../alaveteli_pro/_requests_layout.scss | 4 +++ .../alaveteli_pro/_requests_style.scss | 29 ++++++++++++++++++ .../info_requests/_request_list.html.erb | 4 ++- 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/alaveteli-pro/clear-icon.png diff --git a/app/assets/images/alaveteli-pro/clear-icon.png b/app/assets/images/alaveteli-pro/clear-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1adc6057776be77f587e8ca4cd0cb5ad701715ad GIT binary patch literal 576 zcmV-G0>AxVdjTWrm>r_XTSHyVv|HAtunh+koS;oHW56m1X^pUq~&#bU8Vc|wT0dcA(? zYosJV6K8)cL>K2I72*L5;v63dF^O{$Lqw<3xy1Hb>@ds$h)rD4xFNtB0DFqj^wWdD zi6j3#b5pQye_3-Jr!=3>2Y_r^g7c`90k-~)SnonD@uOTWci`PYx7*!a5@OMmZW1q+ z%jGXwMJt3zal$#wtX8WZ*m?m+@X5A6%mH!AmpfoFl5^x@dGBrpR zr#K0qg$9Wgr+6AVL=`8006+%xp%YAp2t3k{h5S*&hdjd9$3Kc|HRcD=Q$|jhYQseU O0000
    - +
    <% if @request_filter.filter == 'draft' %> From 3cc86ea6d368fc37fe01a086d356f899fc318ee5 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 9 Dec 2016 15:35:20 +0000 Subject: [PATCH 110/311] Wire up search information. Note that no text is shown for the default filter and sort without a search term, mainly because the clear option would not make sense in that context. --- app/models/info_request/state.rb | 15 ++++-- app/models/request_filter.rb | 52 ++++++++++++++----- .../dashboard/_projects.html.erb | 4 +- .../info_requests/_request_list.html.erb | 27 +++++++++- spec/models/request_filter_spec.rb | 41 ++++++++++++++- 5 files changed, 115 insertions(+), 24 deletions(-) diff --git a/app/models/info_request/state.rb b/app/models/info_request/state.rb index 10eccc1065..4c8de96c52 100644 --- a/app/models/info_request/state.rb +++ b/app/models/info_request/state.rb @@ -26,15 +26,20 @@ def self.all end def self.phases - [ { name: _('Awaiting response'), + [ { capital_label: _('Awaiting response'), + label: _('awaiting response'), scope: :awaiting_response }, - { name: _('Response received'), + { capital_label: _('Response received'), + label: _('response received'), scope: :response_received }, - { name: _('Clarification needed'), + { capital_label: _('Clarification needed'), + label: _('clarification needed'), scope: :clarification_needed }, - { name: _('Complete'), + { capital_label: _('Complete'), + label: _('complete'), scope: :complete }, - { name: _('Other'), + { capital_label: _('Other'), + label: _('other'), scope: :other } ] end diff --git a/app/models/request_filter.rb b/app/models/request_filter.rb index 1ff53232ba..eb447ab139 100644 --- a/app/models/request_filter.rb +++ b/app/models/request_filter.rb @@ -16,11 +16,19 @@ def attributes=(attributes) end def order_options - order_attributes.map { |atts| [atts[:label], atts[:param]] } + order_attributes.map { |atts| [atts[:capital_label], atts[:param]] } + end + + def order_label + order_params.include?(@order) ? order_labels[@order] : order_labels[default_order] end def filter_label - filter_params.include?(@filter) ? filter_labels[@filter] : nil + filter_params.include?(@filter) ? filter_labels[@filter] : filter_labels[nil] + end + + def filter_capital_label + filter_params.include?(@filter) ? filter_capital_labels[@filter] : filter_capital_labels[nil] end def results(user) @@ -67,13 +75,16 @@ def order_attributes [ { :param => 'updated_at_desc', :value => 'updated_at DESC', - :label => _('Last updated') }, + :label => _('last updated'), + :capital_label => _('Last updated') }, { :param => 'created_at_asc', :value => 'created_at ASC', - :label => _('First created') }, + :label => _('first created'), + :capital_label => _('First created') }, { :param => 'title_asc', :value => 'title ASC', - :label => _('Title (A-Z)') } + :label => _('title (A-Z)'), + :capital_label => _('Title (A-Z)') } ] end @@ -85,28 +96,39 @@ def order_values Hash[order_attributes.map{ |atts| [ atts[:param], atts[:value] ] }] end + def order_capital_labels + Hash[ order_options ].invert + end + + def order_labels + Hash[ order_attributes.map { |atts| [atts[:param], atts[:label]]} ] + end + def order_value - order_params.include?(@order) ? order_values[@order] : default_order + order_params.include?(@order) ? order_values[@order] : order_values[default_order] end def default_order - 'updated_at DESC' + 'updated_at_desc' end def default_filters [ { :param => nil, :value => nil, - :label => _('All requests') }, + :label => _('all requests'), + :capital_label => _('All requests') }, { :param => 'draft', :value => nil, - :label => _('Drafts') }, + :label => _('drafts'), + :capital_label => _('Drafts') }, ] end def phase_filters InfoRequest::State.phases.map{ |phase| { :param => phase[:scope].to_s, :value => phase[:scope], - :label => phase[:name] } } + :label => phase[:label], + :capital_label => phase[:capital_label] } } end def filter_attributes @@ -114,10 +136,10 @@ def filter_attributes end def filter_options - filter_attributes.map {|atts| [atts[:label], atts[:param]] } + filter_attributes.map {|atts| [atts[:capital_label], atts[:param]] } end - def filter_labels + def filter_capital_labels Hash[ filter_options ].invert end @@ -129,8 +151,12 @@ def filter_values Hash[ filter_attributes.map{ |atts| [ atts[:param], atts[:value] ] } ] end + def filter_labels + Hash[ filter_attributes.map { |atts| [atts[:param], atts[:label]]} ] + end + def filter_value - filter_params.include?(@filter) ? filter_values[@filter] : nil + filter_params.include?(@filter) ? filter_values[@filter] : filter_values[nil] end end diff --git a/app/views/alaveteli_pro/dashboard/_projects.html.erb b/app/views/alaveteli_pro/dashboard/_projects.html.erb index 2630567dd3..29a71c93bd 100644 --- a/app/views/alaveteli_pro/dashboard/_projects.html.erb +++ b/app/views/alaveteli_pro/dashboard/_projects.html.erb @@ -15,8 +15,8 @@ <% else %> <% end %> - - <%= phase[:name] %> + + <%= phase[:capital_label] %> <%= @user.info_requests.send(phase[:scope]).count %> diff --git a/app/views/alaveteli_pro/info_requests/_request_list.html.erb b/app/views/alaveteli_pro/info_requests/_request_list.html.erb index 29e57df325..9198cc4177 100644 --- a/app/views/alaveteli_pro/info_requests/_request_list.html.erb +++ b/app/views/alaveteli_pro/info_requests/_request_list.html.erb @@ -2,7 +2,7 @@
    -

    <%= @request_filter.filter_label %>

    +

    <%= @request_filter.filter_capital_label %>

    @@ -24,7 +24,30 @@
    -

    Searching for search term in filter sorted by sort choice clear

    +

    + <% if @request_filter.search.present? %> + <%= _("Searching for {{search_term}} in " \ + "{{filter_description}} sorted by " \ + "{{order_description}} clear", + :reset_url => alaveteli_pro_info_requests_path, + :search_term => @request_filter.search, + :filter_description => @request_filter.filter_label, + :order_description => @request_filter.order_label) %> + <% elsif @request_filter.filter.present? %> + <%= _("{{filter_description}} sorted by " \ + "{{order_description}} clear", + :reset_url => alaveteli_pro_info_requests_path, + :filter_description => @request_filter.filter_capital_label, + :order_description => @request_filter.order_label) %> + <% elsif @request_filter.order && @request_filter.order != @request_filter.default_order %> + <%= _("Sorted by {{order_description}}" \ + " clear", + :reset_url => alaveteli_pro_info_requests_path, + :order_description => @request_filter.order_label) %> + <% end %> +

    diff --git a/spec/models/request_filter_spec.rb b/spec/models/request_filter_spec.rb index 4c8e79b1e9..ca301b34ee 100644 --- a/spec/models/request_filter_spec.rb +++ b/spec/models/request_filter_spec.rb @@ -31,12 +31,12 @@ end - describe '#filter_label' do + describe '#filter_capital_label' do def expect_label(label, filter) request_filter = described_class.new request_filter.update_attributes(:filter => filter) - expect(request_filter.filter_label).to eq label + expect(request_filter.filter_capital_label).to eq label end it 'is "All requests" when the filter is empty' do @@ -68,6 +68,43 @@ def expect_label(label, filter) end end + describe '#filter_label' do + + def expect_label(label, filter) + request_filter = described_class.new + request_filter.update_attributes(:filter => filter) + expect(request_filter.filter_label).to eq label + end + + it 'is "all requests" when the filter is empty' do + expect_label('all requests', '') + end + + it 'is "drafts" when the filter is "draft"' do + expect_label('drafts', 'draft') + end + + it 'is "awaiting response" when the filter is "awaiting_response"' do + expect_label('awaiting response', 'awaiting_response') + end + + it 'is "complete" when the filter is "complete"' do + expect_label('complete', 'complete') + end + + it 'is "clarification needed" when the filter is "clarification_needed"' do + expect_label('clarification needed', 'clarification_needed') + end + + it 'is "other" when the filter is "other"' do + expect_label('other', 'other') + end + + it 'is "response received" when the filter is "response_received"' do + expect_label('response received', 'response_received') + end + end + describe '#order_options' do it 'returns a list of sort order options in label, parameter form' do From 056afc7a28c6638cbd622f7dbf88a847e5a63918 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 9 Dec 2016 16:41:50 +0000 Subject: [PATCH 111/311] Add shorter description of states. --- app/models/info_request/state.rb | 29 +++++++++++++++ .../info_requests/_info_request.html.erb | 2 +- spec/models/customstates.rb | 12 ++++++- spec/models/info_request/state_spec.rb | 35 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/app/models/info_request/state.rb b/app/models/info_request/state.rb index 4c8de96c52..46354ae93f 100644 --- a/app/models/info_request/state.rb +++ b/app/models/info_request/state.rb @@ -25,6 +25,35 @@ def self.all states end + def self.short_description(state) + descriptions = { + 'waiting_classification' => _("Awaiting classification"), + 'waiting_response' => _("Awaiting response"), + 'waiting_response_overdue' => _("Delayed"), + 'waiting_response_very_overdue' => _("Long overdue"), + 'not_held' => _("Information not held"), + 'rejected' => _("Refused"), + 'partially_successful' => _("Partially successful"), + 'successful' => _("Successful"), + 'waiting_clarification' => _("Waiting clarification"), + 'gone_postal' => _("Handled by post"), + 'internal_review' => _("Awaiting internal review"), + 'error_message' => _("Delivery error"), + 'requires_admin' => _("Unusual response"), + 'attention_requested' => _("Reported"), + 'user_withdrawn' => _("Withdrawn"), + 'vexatious' => _("Vexatious"), + 'not_foi' => _("Not an FOI request"), + } + if descriptions[state] + descriptions[state] + elsif InfoRequest.respond_to?(:theme_short_description) + InfoRequest.theme_short_description(state) + else + raise _("unknown status {{state}}", :state => state) + end + end + def self.phases [ { capital_label: _('Awaiting response'), label: _('awaiting response'), diff --git a/app/views/alaveteli_pro/info_requests/_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_info_request.html.erb index 561edf342f..c3e89f66c7 100644 --- a/app/views/alaveteli_pro/info_requests/_info_request.html.erb +++ b/app/views/alaveteli_pro/info_requests/_info_request.html.erb @@ -29,7 +29,7 @@
    <%= _('Status') %> -

    <%= InfoRequest.get_status_description(info_request.described_state) %>

    +

    <%= InfoRequest::State.short_description(info_request.described_state) %>

    diff --git a/spec/models/customstates.rb b/spec/models/customstates.rb index 7ba49aa39d..afae0bbfcb 100644 --- a/spec/models/customstates.rb +++ b/spec/models/customstates.rb @@ -38,7 +38,17 @@ def theme_display_status(status) elsif status == 'wrong_response' _("Wrong Response.") else - raise _("unknown status ") + status + raise _("unknown status {{status}}", :status => status) + end + end + + def theme_short_description(status) + if status == 'deadline_extended' + _("Deadline extended") + elsif status == 'wrong_response' + _("Wrong Response") + else + raise _("unknown status {{status}}", :status => status) end end diff --git a/spec/models/info_request/state_spec.rb b/spec/models/info_request/state_spec.rb index 43b7cc4b71..bc1f8e40d1 100644 --- a/spec/models/info_request/state_spec.rb +++ b/spec/models/info_request/state_spec.rb @@ -24,4 +24,39 @@ end end + + describe :short_description do + + it 'returns a short description for a valid state' do + expect(InfoRequest::State.short_description('attention_requested')) + .to eq 'Reported' + end + + it 'raises an error for an unknown state' do + expect{ InfoRequest::State.short_description('meow') } + .to raise_error 'unknown status meow' + end + + context 'when a theme is in use' do + + before do + InfoRequest.send(:require, File.expand_path(File.dirname(__FILE__) + '/../customstates')) + InfoRequest.send(:include, InfoRequestCustomStates) + InfoRequest.class_eval('@@custom_states_loaded = true') + end + + it 'returns a short description for a theme state' do + expect(InfoRequest::State.short_description('deadline_extended')) + .to eq 'Deadline extended' + end + + it 'raises an error for an unknown state' do + expect{ InfoRequest::State.short_description('meow') } + .to raise_error 'unknown status meow' + end + + end + + end + end From 09e938bb4c9a869429c7c2b27a59ea62d61c7c49 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Fri, 9 Dec 2016 16:43:10 +0000 Subject: [PATCH 112/311] Add easier way to get hyphenated version of phase. We can't use labels here as they will be translated. --- app/models/info_request.rb | 6 +++ app/models/info_request/state.rb | 18 +++++--- app/models/info_request/state/calculator.rb | 39 ++++++++++++++++++ .../dashboard/_projects.html.erb | 2 +- .../info_requests/_info_request.html.erb | 2 +- .../info_request/state/calculator_spec.rb | 41 +++++++++++++++++++ spec/models/info_request/state_spec.rb | 12 ++++++ spec/models/info_request_spec.rb | 11 +++++ 8 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 app/models/info_request/state/calculator.rb create mode 100644 spec/models/info_request/state/calculator_spec.rb diff --git a/app/models/info_request.rb b/app/models/info_request.rb index c2ed61e6ee..7c037fe90e 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -180,6 +180,12 @@ def prominence(opts = {}) end end + # opts = Hash of options (default: {}) + # Returns a StateCalculator + def state(opts = {}) + State::Calculator.new(self) + end + def must_be_valid_state unless State.all.include?(described_state) errors.add(:described_state, "is not a valid state") diff --git a/app/models/info_request/state.rb b/app/models/info_request/state.rb index 46354ae93f..117917ae4d 100644 --- a/app/models/info_request/state.rb +++ b/app/models/info_request/state.rb @@ -57,21 +57,29 @@ def self.short_description(state) def self.phases [ { capital_label: _('Awaiting response'), label: _('awaiting response'), - scope: :awaiting_response }, + scope: :awaiting_response, + param: 'awaiting-response' }, { capital_label: _('Response received'), label: _('response received'), - scope: :response_received }, + scope: :response_received, + param: 'response-received' }, { capital_label: _('Clarification needed'), label: _('clarification needed'), - scope: :clarification_needed }, + scope: :clarification_needed, + param: 'clarification-needed' }, { capital_label: _('Complete'), label: _('complete'), - scope: :complete }, + scope: :complete, + param: 'complete' }, { capital_label: _('Other'), label: _('other'), - scope: :other } + scope: :other, + param: 'other' } ] end + def self.phase_params + Hash[phases.map{ |atts| [ atts[:scope], atts[:param] ]}] + end end end diff --git a/app/models/info_request/state/calculator.rb b/app/models/info_request/state/calculator.rb new file mode 100644 index 0000000000..1c03ce9f85 --- /dev/null +++ b/app/models/info_request/state/calculator.rb @@ -0,0 +1,39 @@ +# -*- encoding : utf-8 -*- +class InfoRequest + module State + class Calculator + + def initialize(info_request) + @info_request = info_request + end + + def phase(cached_value_ok=false) + if @info_request.awaiting_description? + :response_received + else + state = @info_request.calculate_status(cached_value_ok) + case state + when 'not_held', + 'rejected', + 'successful', + 'partially_successful', + 'user_withdrawn' + :complete + when 'waiting_clarification' + :clarification_needed + when 'waiting_response' + :awaiting_response + when 'gone_postal', + 'internal_review', + 'error_message', + 'requires_admin', + 'attention_requested', + 'vexatious', + 'not_foi' + :other + end + end + end + end + end +end diff --git a/app/views/alaveteli_pro/dashboard/_projects.html.erb b/app/views/alaveteli_pro/dashboard/_projects.html.erb index 29a71c93bd..2ad98f07c1 100644 --- a/app/views/alaveteli_pro/dashboard/_projects.html.erb +++ b/app/views/alaveteli_pro/dashboard/_projects.html.erb @@ -15,7 +15,7 @@ <% else %> <% end %> - + <%= phase[:capital_label] %> <%= @user.info_requests.send(phase[:scope]).count %> diff --git a/app/views/alaveteli_pro/info_requests/_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_info_request.html.erb index c3e89f66c7..c7e99208e6 100644 --- a/app/views/alaveteli_pro/info_requests/_info_request.html.erb +++ b/app/views/alaveteli_pro/info_requests/_info_request.html.erb @@ -29,7 +29,7 @@
    <%= _('Status') %> -

    <%= InfoRequest::State.short_description(info_request.described_state) %>

    +

    <%= InfoRequest::State.short_description(info_request.described_state) %>

    diff --git a/spec/models/info_request/state/calculator_spec.rb b/spec/models/info_request/state/calculator_spec.rb new file mode 100644 index 0000000000..d5ddcfbf97 --- /dev/null +++ b/spec/models/info_request/state/calculator_spec.rb @@ -0,0 +1,41 @@ +# -*- encoding : utf-8 -*- +require 'spec_helper' + +describe InfoRequest::State::Calculator do + + describe '#phase' do + let(:info_request){ FactoryGirl.create(:info_request) } + + it 'returns :awaiting_response when the request is in state "waiting_response"' do + expect(described_class.new(info_request).phase) + .to eq(:awaiting_response) + end + + it 'returns :clarification_needed when the request is in state "waiting_clarification"' do + info_request.set_described_state('waiting_clarification') + expect(described_class.new(info_request).phase) + .to eq(:clarification_needed) + end + + it 'returns :complete when the request is in state "not_held"' do + info_request.set_described_state('not_held') + expect(described_class.new(info_request).phase) + .to eq(:complete) + end + + it 'returns :other when the request is in state "gone_postal"' do + info_request.set_described_state('gone_postal') + expect(described_class.new(info_request).phase) + .to eq(:other) + end + + it 'returns :response_received when the request is awaiting description' do + info_request.awaiting_description = true + info_request.save + expect(described_class.new(info_request).phase) + .to eq(:response_received) + end + + end + +end diff --git a/spec/models/info_request/state_spec.rb b/spec/models/info_request/state_spec.rb index bc1f8e40d1..7e2f3fa049 100644 --- a/spec/models/info_request/state_spec.rb +++ b/spec/models/info_request/state_spec.rb @@ -59,4 +59,16 @@ end + describe :phase_params do + + it 'returns hyphenised versions of the phases' do + expect(InfoRequest::State.phase_params) + .to eq({ :awaiting_response => "awaiting-response", + :response_received => "response-received", + :clarification_needed => "clarification-needed", + :complete => "complete", + :other => "other" }) + end + end + end diff --git a/spec/models/info_request_spec.rb b/spec/models/info_request_spec.rb index 9472a9dadf..d7eb21952d 100644 --- a/spec/models/info_request_spec.rb +++ b/spec/models/info_request_spec.rb @@ -2813,3 +2813,14 @@ def email_and_raw_email(opts = {}) end + +describe InfoRequest do + + describe '#state' do + + it 'returns a State::Calculator' do + expect(InfoRequest.new.state).to be_a InfoRequest::State::Calculator + end + end + +end From 5e67509a7cb98d514bb13325ab2333a75c2d020b Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Thu, 15 Dec 2016 14:32:48 +0000 Subject: [PATCH 113/311] Improve icons - Retina styles - Fixed spacing for various screen sizes --- .../alaveteli-pro/blank-slate-requests.png | Bin 10441 -> 1471 bytes .../alaveteli-pro/blank-slate-requests@2.png | Bin 0 -> 3598 bytes .../images/alaveteli-pro/clear-icon.png | Bin 576 -> 142 bytes .../images/alaveteli-pro/clear-icon@2.png | Bin 0 -> 202 bytes .../alaveteli-pro/embargo-lock--small.png | Bin 475 -> 271 bytes .../alaveteli-pro/embargo-lock--small@2.png | Bin 0 -> 432 bytes .../images/alaveteli-pro/pro-status-icons.png | Bin 8198 -> 5242 bytes .../alaveteli-pro/pro-status-icons@.png | Bin 0 -> 2527 bytes .../alaveteli-pro/pro-status-icons@2.png | Bin 0 -> 5242 bytes .../images/alaveteli-pro/search--light.png | Bin 493 -> 282 bytes .../images/alaveteli-pro/search--light@2.png | Bin 0 -> 474 bytes .../alaveteli_pro/_dashboard_style.scss | 3 +++ .../alaveteli_pro/_requests_layout.scss | 8 +++++++- .../alaveteli_pro/_requests_style.scss | 13 +++++++++++-- 14 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 app/assets/images/alaveteli-pro/blank-slate-requests@2.png create mode 100644 app/assets/images/alaveteli-pro/clear-icon@2.png create mode 100644 app/assets/images/alaveteli-pro/embargo-lock--small@2.png create mode 100644 app/assets/images/alaveteli-pro/pro-status-icons@.png create mode 100644 app/assets/images/alaveteli-pro/pro-status-icons@2.png create mode 100644 app/assets/images/alaveteli-pro/search--light@2.png diff --git a/app/assets/images/alaveteli-pro/blank-slate-requests.png b/app/assets/images/alaveteli-pro/blank-slate-requests.png index 4a2cf783f9b18b2e900ffd844b9a84ee5889ec61..ec6d860cbcc2dbcb7ae11e47767c2c00497838c3 100644 GIT binary patch literal 1471 zcmZvce^8Ql9LFF0(KemBbyP>0&Uv1h7OfOzCaqsrU?4qcsj0|WIbzwF1tKD3SEQ9W zz>vn2U5^rG_QQ!LN=;jCmuMx^!l{)%P)xy;QIxU=y8h|9_ucpNem?j8d_T{9U-$VI zkx4teJeGR^0N{n+xeWvW7YG0liJ!Z|nOooZ?T14uc~|^)IP&>-sQJYy$BR?h#mUT~ z$;rvFu`zg^n3&kdEJ|UW86O`{$uBY*jSh4CbYiZcrly9)VsSVeHk-}o^Ai#hA|oR) z7)(S&M0j{OY_wXf-EKGc{>^|J_#XpF6u>Poh5({)B+xfDEEpFTv)dI-1YKNL?*M<; z4I&iu@Dl*w_6omkb9^ph>fc9Mms^&)rwi_rg(`Y59wC%kd7YO}-KQz-t0VGOmSFulw(4b#nk$WS9h`$G6Foz(xq+Ey z{uMS_sv5nFei<0^vP4?t%c4_AT$*C;;r@#~5OLyctWTwMG`~7{)da3y#?31f796-Z zTjr98zbVv<^rF>tQAl?`H$Wq*8;Mp5ZNnoMDDy!U)8L)Awpb%lOd(q&IyPv)Uqy$k z3PFgGVlhDQAQTG=g^T0xoAJ9rGN_8fXJg%TTrEi=U+{b&e|6RW>`7I$<&>-$v&7}@ zh1pTtg|!Bgr5jbb#9x(cU?2=1yo7;}IQ`OUh)@IhSsfaBVCo+>oa-+3ali$DGaJtJ z_@*xxxa2JAL`vZ!NbQHckFGHOGK;yF`KRF8V3YtAD$8*c>}#caZ%BwTARV3Ztk}-h zHmvElq@QVfz))qGuEMGQ*2cHCHzZ>;ecpNg$4f!4S3y+`;^VaA`)(ckY38?6)Jv&) zxr#I8$)3{B^*t{yVr_)9?8&ovvAh<;0dobWddid0b41XB^rr8_+1hZA+o#JVZP^mc zsBRgP;6qIFChEnSra7}z(w+%JnVvoRL;*9}o90ao;g=ypl`*h{nA^yffn|nhr3r#D z_$zGos1PO3YO$;Lf(p5DlY14^CFm49mOD?0Q}M`i3Q%>KXOHt5ZCiYMuFYK?z`x zrO67*o>nmA7nPa2CqHqc^Md#^*%;6h<<^;$Ud^w4cEhqFE2p%dJS1xj*cgiYY5qR8 zGx51B11@-CBj%JV+pr3=qm#OuHoc^a?V~OJvR;^szXtv1)ZldMKwmct9Kuh2_9+17 z`YdzkC(+mstH%yh2Hxb+ne>zT=?3&j1EYs3$SCQUDa9DBp^a^lPWVo8c{D5-RbO0uDgcG>0NxO z%dNup-Xl%g%~@NyIfJL23|1J{Wt_?>aTH8OYH8IT23ec46Hah@ZLg+o9V5RpW0zwEFAPFR)6A%j=6~O{XN0cHUO?p=l zkPZnDNPvh+O9)5}BqZG6cRcT{ckg+3t#|);S!?q9?VXv~vuDqqJu^FxEX=MQ9%lDEWG&gTAHwJ>4rptHCpyRt2?1R4K)6GsZou5UAXX4J zj}ZSZh%NwdKn7~#fOar7(FP-63T}Hi3c)ZxHZ=gCs~_y=2KIrVrQ9K2P&nxHazo2$ zDX0hNw7r_CqN$%Do>pkTOIdAyR4z>hfSkbqy&EEd?bt4GmR|3sTC8N@_qw z6`+cuyt0b6s-m{4veaLn)9lfZ9-i7(m#_Xc7P|#HeFu&9(*^>Af`Sx+R1^?MFQAf^ zmKIP^8K|r*&&H5Pg}~8n!SZm_nco~PLr`EO)DI0sz@_#$y163)(4f<7N&h_snBU)Q z;i$jV#8wzE*v${9q@cJrrQe9ArvGFs6xs^%zvTTd8KZ1M{2)Lp2nrE^1hW^; z^UU5*e%gjeh#MM#v_T+ze{0d=4g!rp-9h+C85;i5nv{&G8yE`T>y-UPWooK@1CBzw z!NHIlmqDl5d=#Ki4{g;eDh66w>Q@aEFR3XhT~XJ%tfYBaLrKv<^YW!D8cJHfX)hzd z0Wb(0{hQX~Z(0q-f27?r2$&yxn&74`SZp+=%hUaAh z<-1;OIKq-X7u!*{zRzf38Ca0Jv>0 zVQ~-u;AC&`J&<~~{}+M`|l-PRuSoETvKf_th0XA?l0Y?i8L zgp9(m6DfbE*UVofbq~BlvIcId*m;t6CtYn&8be`Fbht+Ytjg*hw z-)ltdj682TkQ+g8k*w8-)jJz|GUN|a64B1f*08~A9YsEJJ1Nr)hd*&(!ca(_)_&rL zWU8~2vyRt^yW4b6a;c;h&(9GPSixL*SjCo6j;Z9#E4!eS;YeK@3BN` z5eF9Z&tJVe?t>^=O?3w>^<1Qr&jZDCVl+;i{rH|~O)k~9;Q5FVzHT3pp6BzJ|8~-p zG0t4zfS)S|cD<+pWXVEc*Go1FA871XK(5v47W(yxFnWbG?D}DQ?E?e!?$jl=_ zh_m#ZZ->SnRf7@0+jLW|H!b#JgDXDNuV3}^VmJ~nmrJ@d2CwvuAI1jArdJD0@V17V zHSJ4J7KPS($IEc$?3ZP)sN6Hp(2FVi0cFPQCg3;x)9U-RJbrvp>JVTo*XQx=1W`8n zMRAVDug6AIF?)tGyb}M(1#7kIgFuwh3wYT-mtB{2m^9(jH^DU+fd`9Kna*+*_!_0P za~;rAHBb@TgMf5zwvlAr;4?p{Ls5tF;aXB!=ZoJ;=gnS=Isv$Q!Y65o(~F zw1vzJlk1T%n59oK?))4 zy3bjvMJWjo-I4@h?q9z3S+yf5WdZKXjQ0If8cxTR__!5B1*6B3z zd-oljAfNPd`sri{Lx=yCkm8)4Rb(z-6hq+|>ynaOj5xjNbh4|~d)_FPWmRuA;!aGh z|I&|1;#G3PwBC^%{UbfixJzNI`BJRlPkpGF&I*m2FPFOgw&ezH*|XMH-w5Z#U6MwG z(nH9iE?LBTxe#w+U!*aP!CjKId2d)U|APZk_@l3l=q6jm#>Sw_L0%(u6@~=TdrBV2 zqv>-|Eh4}EQT}bB$@SgMuI;98n}$*sDIN&$1~@?s&g?&#NEf=OW-s;J&SZCV+RUAHStYm-Ol z%;DO+S|^E3FP87yXduoXe)qOpGpE2X>>P6q_;6_aV60?*98xK0z=*nBu>KR{hK_)p z8|kyhwgo*H8e%)2c&o|Aiih=3j_0hv)aH{(A>Z$bOgWAWTzh;kH)WE|%9`jS`Cw=~ zSjIFmkv#M0;e(;Tf&LbOt8;iy>`rrnM6-0P+3NLF)}%Mk6Gk$@;U5m&$TY#Z=F`iA zgagH|zeLD(IF3>@1V5CJRv+Fh?h6NV2ehH7@l3DtyAGNaTyITi4yl=TJY|fF^c?r} zTyrP$K-;7uRJWvcP6cd^I^`F(L0$oIs&M95D)nQh4MwdQX3M8IjgucTIST7jy@{*jLX5rCKpEU_${c6fu1%!xi$Xuh%3p|klb*QwWY(VB5$~F_ zP%T%A^^9-~$<*iG6dM_I`NSlqs@ytycEkFLQ@ay1)LH4HYE?AGyA%zNnGCu0d}%@_ zs0C5d*peV%MlA(JQaGlULi`haXdWyN(LJ@c`_YUT*96vNIEw1)s&ira;vbe9KFJ7x zD_t{7a~CApmTx6?0e;|}?xJ<##rn~@Dpzej4tvmYA*wM|yQ9ch{_Ze~a0=P)6S?N2 zQUI55+Bo_{>()qL75}?5&nlHmGJSXnfgQz>c$~zs>vKZC?8mt76$k*(Is4bSv*%X+ zK70N<_w?WTuYW}Nc-1u4AIrnG(bX&MoPaWi$T1R0KVA8Lg}}*g!W#f~48J?4cNGIJG3@APa3yw zk=ifgd6yrhV~WSE2c}MiZzR52lKIzske}br;Lu<>)A(ObmXT0{U1zXQ;o7Hscihv} zNqNw?dv_S|rFjdY1~MNnOqk#XtYt8!mKVK*m^VYWl3sklvId!wvI$d}sgm(UtLJYE zT#bB|*K+LYKF{yBQk%2r^DP!9%9LUw?1bc37gRA`qADd-PISujQVAR|J2!s9s3k|} ztAwlN!S+nEORP`hhp@w48RmS{rKK^Yhw>o7P(2hzA33o)T&0W2l2jh8Kx7!qDj)6B ziG6?Hlp@4M&huz$`xD=BB*5}RxSL1Vkraxka<|lp(e32GTXdF1c_WhG{Z6q3yP4S# zo9(XC594-TW(1N;2j~h9&4a^RX_VxM@HCRjsS1P;F}_%zHnzAQx;mq(28xV#3Wd-A zc$mDBDpH@6zq|(S2;D5U?!A?l9n$$c;8^JXOAGW^dD4sR#NjJltL@LJXFW?0y)yS5 z9Ol3T{{;rPeYG1}RNlH{L=0O}?s!bqtdU1Im4d>Tub&LZjvpbFg6S);h=I?&&2GN> zb~<~Z#ayM=U28Y<;{cS+nP+-$g!Y*|Ko|USTxQ z7yZYi$2p3p)We`YFXr~S?@X;8Yl>PY9>#oibqUFr*tgOGqes3kE5WoR7*Qg-mDLVW zectsO6Fe6vM%`+0;fO`qmx^y<)8Zh)Yn0$#hEYwy1Tu}jlMDlSRx&+}iQPjej?(7U z7oZ%!(g~_wek5GEhGt62FZWC9Uud8w)h%?L%ANx`jTh;L@IlF635OUA)k%WIJ1s%S zb+u8^WK8juZg(A_jJe%RI#f{gQ>A3HuVvM#i*_;hB9il5V;by{ zNfS#z+N-WX`R#gaWdEA~SNop9)wzi!l?O{YH9U#9L3-j;gxZ5fC)?=Q0L}v~B4D8@ zpI7V%YQ6fy*$u5ci4GRIx4860poR^H+v|+k#Ssaj9%_^8$Ap|lPzn-Xj5c2#l(&l6 z3{K!BB~~2WkOw#1|0)Qjmli}elPSC(S{KHg?ghdqb&hZT=;6V1SnS%IvA4m0)`U%& zqL!Tvvm&QwHoG>{v736dr3@ZFBs_n?n=5g`N|Tf z0@wfw{qw!BQO!v`jq(_F;Fs~Q>-+BJk{jqvlq#*YbUFbht}LlDyFN-w(DvX1aOUFR z@5_tvOUeneNjC&G*{6h}+AtS8Vd1}T+fTiOD(wIY--?)z7OT?`I&vV3M59g2)pVQ+ z16-7$@sBTT+Nc^Ii{SS4r2nl+26Md)Tu+h2D?)qJ=-XZS>i_X_sA#W zyUTPZ)Z$ud9AvCgukYikdgpAKllortn=LB(VbJ2MNSKUHT>9LwAsNiJ8|^F8fz-!- zUnBqyg*5_~%AKI}vP}~z`>aSJ@8&FUL$y_Y)=#uiDKR(=5Il4sF9eOT`{K}Vn)PZe zvw6eOwS=@trHXer(7rT*asVGs=iUwgfp-3w0HI0JwEi1fv|>qmWLHLJeG;Vm^^S84 zg%7t*wDBy3y;VppIKqyrwJ~|*^%zB>xbAA zemu;ZW7~hK3B7>*zA)|XFe?k7mPYH}Y7XCyo~;utV1lZ>BfU=6(y3iDr2|#W-1-n@ zP`LU9RD9c0+sTR4m1-PZ!~8;J1Uv3pwq}Zvuej09EuX@yv(5oa1P(7pLnPgyF{G`AVU}L1>b1W0pSch50}Lp=+=6Rn zHrNtZLFvJ-ESF7ho6<1Uuz|d=-i|1I0LHV|A@kTOeBUT*_f~4=kZ#bo@t>vk`{IPT z6+A2NKU3OG4-=s^DQt$jh;8jupnM`_7b`J6_FUbO?|7oCXI}Vk8j*A=Z(*1?LuhDd zwA=dh`cIpV`X8I)xwN@{=x*mLY0us>p1%5&_-of_cg2g?9`DvCK@+%F7XLZ-PhLFC zNHPW*-yEzN!Uw6Yvv4Rqdw>V;4Ypni+WFZ_o{6&EwIXnQK0;hg_>^UAxG;!XaS+W& zi{T!Hm`FN)044FPf~r6;IxAQcP_czgCOfyk6APRdzoOWJtPS5rhKYQNDTGvG+Bn>} zgKm~n0S!DkE!S})gONh-m!EYFQq0}Cgd>;3`YxrivSus!TW%f7N;eGmH(+q3P*rU1 zL}vSCB61gY`^(@FnRz~khz`B;=kt6APJtP`iR8sQp&-+iET{GyNuD5V+d%8&lW*?r z5gH~kMd^l1U~*-VJVrOqUG51D{kAO3HX2ipUa$(|B|R+bw|M?jjEUOxT4k&(O4 z*MGitpG*#w|kVMCw88Mch5d@V)aDrADJvU`mw4LOH39mYD{>Y z;niI$`mPeDvzi=)^C3Bn^liw)bh6C(T%D=uex@qe(iJ``MUsW2 z5e<_-jIBn^d4x`uEAtsRm4;jAD~NdW=Ax8%X~(NA6-rOd=2(E_%HUdfZ@9u7)Bh{m zHInP&F{B;$fe$==h;C%D4=r$K+@SiAmmNC{DRIA~6_Xh_VN1gd(5kY=DkeYQQy&X* zUDhMsQE}~`Sc>j0_Yi$_fOu;u@FW*W^4fcQp{?Da7GhJ6xh?NozV{>MLXIwm0mrT; z(8faLz@hgd933l0ryKDGohaaBn$I}%Lf z%&iO12uvZ;L#%V|u87SxMyieSW4NnDzpB0~kEsMj&iY@`(pd(2fQV1@GHoW;yj7^% zaSR57dc{ic1m!}_Ts3@YYuciZ<1rwwU`UvWdWU+bv>{ePh8(GE35Cs8$qygJRSBvz zxuBvKINH?7VFd&?+>`}R+4qa7+SNh0HvJOqzGHOY>^OS*QvVfE!%}J*Ld+pa5jk}V zoPks*ld1djY`!nie}xrP_^QbG&POPS2;y%s!4+Es9w!Wv46YJ+KmlVKrN>qkB1G!1 zm*rq+m{~i5Nh--{VOnE)DYNzkOwVS_CAvCM^9?Vu`@{PF&)J)V=!`5FxUold0B@JQ zuJ|KM#}#|dP^MvCf#%-y%rYNb{Uq5E(Yd?HDva2XiGEZVYe*oFNC*yFINzk`PG}GT zSWHVA(<)JT6Jo$fb4|MufC?PZNVlE1LYU6f8{6DWC(lh3Mu;^J1gR>rVjkaXhaW_V z#;nWy%CzF91(p7Prd`O0jRDO7$GAQ)bJuydWW&+6r_yT*L#d@~2&LlFlLCkVNe%C% zClxf9m^;F8nUqwTI;E;>UNm|XI*&m??Q?sSAaxDh7d$FEC-51Uw}Sl?#Jdr` z#W!(z3HW4)?jxG2&u*N6KDamBywI%m0jxM@8MM^FVzF{QXgXyBGM`iS0Zvlz(0UbF zQ}anfg6dOm0Knl{!S;&g99vCg*1t2*z2`p_$^J3P{C~@1dk4m0z3bb8E_w~AL88HF zjIxPuvNubExhS{E%FFH<#(LI_x)$+7$6L%uLD-6-X^jhJCReBMK(x4+SY^n}C)JFu zF;9HW3T%UcqJ-%$S#NG`!V#1MsY2f?g;o1PkAboz*d-3FKGEBA>H3J-;qew22pPTH#Q3|p*9V@2)=UY___UqXw? z#7pePTGTF{0yEQ&wv>JQ6DhQ|Gd9pIUn(Mx#x|T;1Xso$$3Hj_+f&J${TaBn^~q1JZm>zHX-6i!-=IM zZ>cUr!1pkHFk`woU3p#H5j&^OkF>-4q(SBZG@ zo#~91*3tCc7kWWP{f`gSNqI&f^fK>-Z!~qiJOCyCkZ^c7lx?Qrd9Wv&7IP2ZkqOL5 z9+!$1xd#7EE}GE%S@WVpM5To%X8+9vFJCTx!%r}il=dho%MX_Vn>2#P4LYu7h+4#u zrD|@WbXq5nT*Ac57cS&$eyCVF|0Fw5R3%Bv6Ruy!w00FYAD;qWO(w0UP+xVvENEei z_fkTRC&vG^x!ic&OMV6LRsEL?u)0RfQTgSe?P}YR+II^>e#=V}Sgx_FGJWG~*7Y17 zPT7^~#@>tC3DGgw0EO{2)pH5F4~MX>wf-I;bJ@}t17g-C*2J~sdPz~E&Ll%K;e_{2 z3xUGds^2<5!z9Bh3z++%1BnfC_djOoDy}p{n6#+RGIBbCuHE46Ig&ea-o0?`%zi=w za75&gWb042^@8luAC>oClpU9TAC~Trrxo^xk62(y_PRyHQzV%opskKJTac=;jb1rO zmbY^l)pU18wm5jd>Pv1K1=Am`t#X{;=q@Ma`+p9UI2Uf#rJR(`3OwC!m)bdo5)TqL z%HP@l$SHXM%9C4lpqqKjC_nmoHaaC6?d$6B!G-VeYhW@=Ct-ApRV-kHVtj!`+D9%% zrSu-SOI3M)RMA*%^V!sKqkU$F4$QPQy*WMBBywpr>EE{iQnCG3PeD0Ufwa!$-eHX` z^3WRia2@Z%Qc01|jp(m5&2Lqh8D)s#nicr)v_z=u!bRpO9zGe@4|>^eK~Eh^zOSWY zE!CWY5%|9S$X6&)qSFGy2w^c4fQo9ql#vgAWnI($BF%Yy67a?o^L}?XwG=cKDkCqB zR}C86ktHFd6D0#Bu1j(UE$?dE?8}uN7_Co+_8v30PHNh`;w;_t$yEG$1W`JeZ^ZR! z_T9jdx_0?Qr-^UK&LgF1p`YBk0&N&MEJb%mBZbs=S{H*TvqhziXy?tlVb$@T+-vVQ z=mi>IQoy%KcUDg^hCfwt)6HZ=%K{b!2zBaBaS*A^lP}YUvNv_2z>6L-FDk!QNq0nu5*u<;X+{y5R>ONw9@!Qe7nTRHdd-yh%*^o(EWd?E<6qgof* z8ju$WsFzbF)>K1-@^6eziJP%1j<1xm)HcfCx}RpAN)H}x*24N(96$kE~l&# zHx@)2c;8TEZ4sPr6^p2=s*pznl1fG)^^`MhhZ|Tk_p+K61CyjG9nu^J#uK6S&+pA= zf4d7W0_7l$>S}zJPfZ%J>w=nxqR>E15B;lQUs#@f!kM;tfRFctpP&!#$nDkD7x$7y zxw+_5*Gh?K@DtNDEmo#!)=LyPc5rPzG>n z!_Fn|dudnGqs~W@A3OK*-LD>dRr5D4{}(fVbsBB1OL~50`MpEV6mlpX^ws_8bi^_tTRuUSBI+rc$-o z1%cPmC*H&JWBA&nJI8u6T0ic_WDYPH)%h<5TAt^{S6d32#^Ma%IMth~06%Z`!Iyp{ zh;LFS(Z6C-k=b|RD?$RKq^7@DA26|E-y&|G^ghEhLTqBCeF>()(oit_+dO~8#w%Sv&LmNJ6})ZXw{HMWx1l_vQTtP7pE}sir83^ zdPI2%fi&Gq&SO6RIbW_UT9}2UqHq>}^!*9b&hKZ;PFA^8P(`hny6F-MJiZ)YLf8#) zI6bzUXeG2Vi8?dVcWs{xA2^ZrY?SS?TM5xE(ChaJ(u>h?a|U1Z3p5|pOP{C-6P6fd zr^l_)biNd-^m=8!=3qaP2yrEHr%=)FFnWqLo3C2?^Xj__*n_L^=$e5F^ga3;e466= zjnkZjV1|V4Sn6$)FksWz85=@UM>s9riRrgL@cmUO&|9IzHd#a)!4>ngJ|#eO#8s9% zIZ>2o8J)Tac9b{ID@zEObuo> znONsX4Hb)SY@4;NZkX8q(Tu=_8+bYSPj-^{Y*J*%xRe2HA;pl$$vdq2?Q8 zfp2b0#+34%+AZPK3epoI50?0t=~!yNTd_zJ(kWXB746How&1K&FVTw$93z{lqVQhWTgMDssQlYTpQA!9jAhJ*^9SAixK zupgbsrtz9dXrPgp z0r3`RwoQe9d#f@l%BA1XmF#+9YteM=mg{CVWD3naY`W$GObdKHCu&T-V9oI9na)&UhQ9ao=xTUBx{^_%edNq&`SNDw*?`K0z!ET7 zD=2M=Oyso>K%tz7sKP82pAFoIBn>7FX7oI>TsFkOT#2&V67vlsXi^xFURPBp3BA{z zGWl_907Sdh;P3L25^6_?TP6I=8CFPMzkJz9VWnBg+E6jDTHcphc1%ramR_0W)F{iiSz0JQycA= zx@F{D6k~3zVo~Y)#;LaxF=V%So!1`DM$M8)Li9y0fmRRGaxBR^8vEj_#lMVHm25K@S3niJyCJ<5U zqtUL3S}`uI(fF9CmJM-&a2RKqk8m_AsX|&MCz&&+7Bd@yEVOl zUCDIbGen=I>XRHv{>*IxY}T%$enu>iFNGM|4?U9GbDW$0=GjnQH2qb{M&V73B1xVa zLzdfWI+F8D_c>Y_W=Xem7y-v@nAWr(VXus-IFV{A!>yhT zeoy2pjS~+t(`hw^TN9232V9EhiWS4-AYI#aXZMx=Nbko285U*XhS~7>!G%5o>|EdS zXt_ly%EfIdz8OpBI`itxgAdl1q6s!;*Fmhzz_6(fiP~yX^UI-rRL`k6nY3r+)P!D? z33GPT9?vKdeR(3UNSPPv6YwG&|7g+~Ol{9>kpyKi4z)mUwU~bI4tN@c*Jv@#BChYW zAl~<6DAB5#rdjI_zTkFVEIL`j^`M-&^(FZg7-K) zc8(`LOGKq+dtzWGc;KDVb;rW-r-q>?eqC&K`T@iK$wmAwjs3nt|4%;Te}r&%8|ohW z($jCIJ25ZAJoOs^JU0hvhU3DbXL0YDpv`c*ip|vQ0m{w(iI5*9b9n=cPMyh)dTzHt zaRU_#Av{j+D$;2W;^^Ed5G9*)XJP&(;_Ttpefor@+nF3kIMz^@rXk{L|?Yre){M(yBG5{gm! zeL3R;GjW}Gma3+q&(jyytB$qtb;_1DPFUx|kcvfXh)gE4fC>v`d)T|OVjnpEv!DH~ hBhBNxu6OqVniQZ{t^o2+@BO*(#uc;6B?hw5qI diff --git a/app/assets/images/alaveteli-pro/blank-slate-requests@2.png b/app/assets/images/alaveteli-pro/blank-slate-requests@2.png new file mode 100644 index 0000000000000000000000000000000000000000..e27bd13a376c69650d9e360c36bf6bf380dfbeed GIT binary patch literal 3598 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 literal 576 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 L8$N2#bOQeu;HV}n literal 8198 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 XBU~Imon*{lAM2%>&Wm3v*5UsLkSLD< diff --git a/app/assets/images/alaveteli-pro/pro-status-icons@.png b/app/assets/images/alaveteli-pro/pro-status-icons@.png new file mode 100644 index 0000000000000000000000000000000000000000..7390355c5ff9ef49eb5e5230e932efbd172eb696 GIT binary patch literal 2527 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 B&^`bF literal 0 HcmV?d00001 diff --git a/app/assets/images/alaveteli-pro/pro-status-icons@2.png b/app/assets/images/alaveteli-pro/pro-status-icons@2.png new file mode 100644 index 0000000000000000000000000000000000000000..6dc4eedbefc41f4a79e2773d2cb4e8d86430cf05 GIT binary patch literal 5242 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 L8$N2#bOQeu;HV}n literal 0 HcmV?d00001 diff --git a/app/assets/images/alaveteli-pro/search--light.png b/app/assets/images/alaveteli-pro/search--light.png index 668484bf705ab0f5184567d78cce96f98bc43dad..0e3bbea15a02662769e8a3baa5ba0d79e9fca253 100644 GIT binary patch delta 266 zcmV+l0rmdv1DXPm8Gi%-001s)b^HJT0O3hQK~yNuV_*P-V}^&8A9{M|ClD_`X2^gF z{<9peKlJP1e;7RU>u~*lW+VlN>%qc*51l-;2#EiJc!%rZ(m=z3x(_`#Y6;;VwLJ6y zWD4A(L(4(BVbV|~KvNDahbedpv8OQ3PatOznty^tK(U`hn?T471bu-N z4kv)&Z~`M7u!Q8lgU>H=qXjooTwDMvn#X_(TxC0S18hkWP9=w3z*c=fYJgM8!IfZZ z?wsMrq2QSG!OvhN>v5Y=0`bw|JRAznupYV!G-1yXDcnwVI8^qZk%0jKC^g(=+F06i Q00000NkvXXt^-0~g3iHm(f|Me delta 478 zcmV<40U`dH0__8k8Gi-<004wyy>|cr00d`2O+f$vv5yPUYQX=zr1=1(_9! zz{H3w9?d&nxp-Gzc$Rj z10K-}i6#GKOF{?FhC^iC zTu(4OWcdAmZ^76vS!UygztWfKD5FEXM8Yf@I&iAdwO%~~BW2NYSiF}14PI`& Uxpn+~{{R3007*qoM6N<$f+;}RLjV8( diff --git a/app/assets/images/alaveteli-pro/search--light@2.png b/app/assets/images/alaveteli-pro/search--light@2.png new file mode 100644 index 0000000000000000000000000000000000000000..20a62f77b96bc83df8b1cbccb296438b94955bf4 GIT binary patch literal 474 zcmV<00VV#4P)S5DvB=o}|zyK7b8)5(379-hG0+fCoKz@M7o_c=nKEpuU0NHMFoZTPaw?dJB5? zCejAV4c!1yCa`z!Fm~Lv}KU%wYho z2;luEsF?*oO$OkJ0{M)Ak;%YdD)8oZ=hTE3|+TN~|`v~>nCdDzIccdEsIEabk(q+|r_z?9>0S_a~h1Na{9O9KQA zm~d^pjW?ws=#en3cV*hR70%;I8eq6ZJ~@aVdMwZ3Lg4T|+Lo4*JjHHCLsrT|aoFKn zx{b&gm+k-uk^rH-e6aE_I4^OWGw@b){^ejwjLAFZakjxH8r$ QQvd(}07*qoM6N<$f=Bn&>i_@% literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss index 3401f94e8c..b4eafe2a68 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss @@ -115,6 +115,9 @@ a.dashboard-folder__name:visited { width: 22px; height: 22px; display: inline-block; + @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + background-image: image-url('alaveteli-pro/pro-status-icons@2.png'); + } } .status-icon--awaiting-response { diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss index a1ed887a93..5ef63bcac2 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss @@ -52,6 +52,9 @@ width: 16px; height: 20px; margin-top: -8px; //vertically center in the search box + @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + background-image: image-url('alaveteli-pro/search--light@2.png'); + } &:hover, &:active, &:focus { @@ -162,8 +165,11 @@ } .status-icon { position: absolute; - bottom: 8px; + top: 1.45em; left: 0; + @include respond-min( $dashboard-collapse ){ + top: 0.7em; + } } } diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss index e56ebff026..703f248d91 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss @@ -62,10 +62,13 @@ @include respond-min( $dashboard-collapse ){ background-image: image-url('/assets/alaveteli-pro/blank-slate-requests.png'); background-repeat: no-repeat; - background-size: 676px 344px; + background-size: 683px 348px; background-position: left bottom; - min-height: 344px; + min-height: 348px; padding: 2em 0; + @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + background-image: image-url('alaveteli-pro/blank-slate-requests@2.png'); + } } } @@ -103,6 +106,9 @@ background-size: 14px 19px; height: 19px; width: 14px; + @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + background-image: image-url('alaveteli-pro/embargo-lock--small@2.png'); + } } .search-and-filter-overview { @@ -127,6 +133,9 @@ top: 3px; @extend .image-replacement; opacity: 0.5; + @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { + background-image: image-url('alaveteli-pro/clear-icon@2.png'); + } &:hover, &:active, &:focus { From 893bce85bd0f73203c89fc0f313b4a5d8edb006d Mon Sep 17 00:00:00 2001 From: Martin Wright Date: Thu, 15 Dec 2016 14:59:06 +0000 Subject: [PATCH 114/311] Remove opinionated button style from core alaveteli --- .../responsive/alaveteli_pro/_requests_layout.scss | 2 +- .../responsive/alaveteli_pro/_requests_style.scss | 8 ++++++++ .../alaveteli_pro/info_requests/_no_requests.html.erb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss index 5ef63bcac2..ec58c750e5 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_layout.scss @@ -64,7 +64,7 @@ .requests__sort { @include flex(1 0 100%); - max-width: 12em; + max-width: 15em; @include respond-min( 24em ){ @include flex(1 2 50%); } diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss index 703f248d91..11ffc044c4 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss @@ -37,10 +37,13 @@ .requests__sort, .requests__filters { + font-size: 0.8125em; label { color: #777; + font-size: 1em; } select { + font-size: 1em; border-color: transparent; background-color: transparent; cursor: pointer; @@ -92,6 +95,11 @@ } } +.new-request-button, +.new-request-button:visited { + color: #fff; +} + .embargo-indicator { //doesn't exist yet //background-image: image-url('/assets/alaveteli-pro/embargo-indicator.png'); diff --git a/app/views/alaveteli_pro/info_requests/_no_requests.html.erb b/app/views/alaveteli_pro/info_requests/_no_requests.html.erb index 8fbf827a9a..2a1852c812 100644 --- a/app/views/alaveteli_pro/info_requests/_no_requests.html.erb +++ b/app/views/alaveteli_pro/info_requests/_no_requests.html.erb @@ -4,7 +4,7 @@

    <%= _("Get started with your first request") %>

    <%= _("From this screen you can get a helpful overview of your requests, and use quick actions to keep them up to date. Why not start your first one now?") %>

    - <%= link_to _("Start new request"), new_alaveteli_pro_info_request_path, :class => 'button' %> + <%= link_to _("Start new request"), new_alaveteli_pro_info_request_path, :class => 'new-request-button' %>
    From 97832cb942480e8ff56cccf9a13e690e6c458216 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 15 Dec 2016 18:39:15 +0000 Subject: [PATCH 115/311] Rename with .erb extension. --- .../{_embargo_info.html => _embargo_info.html.erb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/views/alaveteli_pro/info_requests/{_embargo_info.html => _embargo_info.html.erb} (96%) diff --git a/app/views/alaveteli_pro/info_requests/_embargo_info.html b/app/views/alaveteli_pro/info_requests/_embargo_info.html.erb similarity index 96% rename from app/views/alaveteli_pro/info_requests/_embargo_info.html rename to app/views/alaveteli_pro/info_requests/_embargo_info.html.erb index f5c8f72fa8..82599bd5a0 100644 --- a/app/views/alaveteli_pro/info_requests/_embargo_info.html +++ b/app/views/alaveteli_pro/info_requests/_embargo_info.html.erb @@ -6,4 +6,4 @@

    Embargo

    <% else %>

    <%= _("This request will be published immediately") %>

    -<% end %> \ No newline at end of file +<% end %> From d335badf2b8efc333c53504551446026e3e5ea85 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 15 Dec 2016 18:40:30 +0000 Subject: [PATCH 116/311] Always link to request page and use version with url_title. --- .../alaveteli_pro/info_requests/_info_request.html.erb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/views/alaveteli_pro/info_requests/_info_request.html.erb b/app/views/alaveteli_pro/info_requests/_info_request.html.erb index c7e99208e6..9522ef8587 100644 --- a/app/views/alaveteli_pro/info_requests/_info_request.html.erb +++ b/app/views/alaveteli_pro/info_requests/_info_request.html.erb @@ -2,12 +2,8 @@

    <% if info_request.embargo %> Embargoed - <% # TODO: insert embargoed request in progress url here %> - <%= info_request.title %> - <% else %> - <%= link_to info_request.title, show_request_path(info_request) %> <% end %> - + <%= link_to info_request.title, show_request_path(info_request.url_title) %>

    From 889bb1f7b692084b56f9bf8ab6f6094cc17685f6 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 15 Dec 2016 18:43:33 +0000 Subject: [PATCH 117/311] Remove unneeded asset segments of path. --- .../responsive/alaveteli_pro/_dashboard_style.scss | 2 +- .../responsive/alaveteli_pro/_requests_style.scss | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss index b4eafe2a68..a966085178 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss @@ -109,7 +109,7 @@ a.dashboard-folder__name:visited { .status-icon { - background-image: image-url('/assets/alaveteli-pro/pro-status-icons.png'); + background-image: image-url('alaveteli-pro/pro-status-icons.png'); background-position: 0 0; background-size: 44px 283px; width: 22px; diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss index 11ffc044c4..fc4d9c3030 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_requests_style.scss @@ -63,7 +63,7 @@ .blank-slate__requests { @include respond-min( $dashboard-collapse ){ - background-image: image-url('/assets/alaveteli-pro/blank-slate-requests.png'); + background-image: image-url('alaveteli-pro/blank-slate-requests.png'); background-repeat: no-repeat; background-size: 683px 348px; background-position: left bottom; @@ -110,7 +110,7 @@ } .embargo-indicator--small { - background-image: image-url('/assets/alaveteli-pro/embargo-lock--small.png'); + background-image: image-url('alaveteli-pro/embargo-lock--small.png'); background-size: 14px 19px; height: 19px; width: 14px; @@ -128,7 +128,7 @@ } .clear-icon { - background-image: image-url('/assets/alaveteli-pro/clear-icon.png'); + background-image: image-url('alaveteli-pro/clear-icon.png'); background-size: 8px 8px; background-position: center; background-repeat: no-repeat; From 25dba28f5ef6f5f6191c43f9420c80e942566628 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 15 Dec 2016 19:02:00 +0000 Subject: [PATCH 118/311] Use strong parameters on request filter params. --- app/controllers/alaveteli_pro/info_requests_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index 5079b3963a..08da36ab21 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -12,7 +12,7 @@ class AlaveteliPro::InfoRequestsController < AlaveteliPro::BaseController def index @request_filter = RequestFilter.new if params[:request_filter] - @request_filter.update_attributes(params[:request_filter]) + @request_filter.update_attributes(request_filter_params) end info_requests = @request_filter.results(current_user) @page = params[:page] || 1 @@ -107,4 +107,9 @@ def send_initial_message(outgoing_message) end end + + def request_filter_params + params.require(:request_filter).permit(:filter, :order, :search) + end + end From e2dc4db3c7ca41349ba203f3adddc3ef1ddee6de Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 1 Dec 2016 13:02:38 +0000 Subject: [PATCH 119/311] Move search to upper nav level. Local switcher moves to the right of the logo, as it is some themes. --- .../responsive/_header_layout.scss | 12 +++----- app/views/general/_responsive_header.html.erb | 29 ++++++++---------- app/views/general/_responsive_topnav.html.erb | 30 +++++++++++-------- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/app/assets/stylesheets/responsive/_header_layout.scss b/app/assets/stylesheets/responsive/_header_layout.scss index 64adf859ca..019dde6219 100644 --- a/app/assets/stylesheets/responsive/_header_layout.scss +++ b/app/assets/stylesheets/responsive/_header_layout.scss @@ -179,7 +179,7 @@ #navigation_search { @include respond-min( $main_menu-mobile_menu_cutoff ){ position: absolute; - top: 0; + top: 2.2em; right: 0; } form{ @@ -216,11 +216,6 @@ } @include respond-min( $main_menu-mobile_menu_cutoff ){ - position: absolute; // relative to #banner_content - top: 2em; // leave space for the language switcher - left: 40%; // leave space for the logo - right: 0; - bottom: 0; #logged_in_links { position: absolute; @@ -246,8 +241,9 @@ @include respond-min( $main_menu-mobile_menu_cutoff ) { position: absolute; - right: 0; - top: 0; + left: ($logo-width + 40px); + top: 2.2em; + right: auto; border: 0; } diff --git a/app/views/general/_responsive_header.html.erb b/app/views/general/_responsive_header.html.erb index 52aeff6887..4cc0c803e8 100644 --- a/app/views/general/_responsive_header.html.erb +++ b/app/views/general/_responsive_header.html.erb @@ -15,22 +15,19 @@ <%= render :partial => 'general/locale_switcher' %> - <% if ! (controller.action_name == 'signin' or controller.action_name == 'signup') %> -
    - -
    - <% end %> +
    <%= render :partial => 'general/responsive_topnav' %> diff --git a/app/views/general/_responsive_topnav.html.erb b/app/views/general/_responsive_topnav.html.erb index 19ef6e6f40..974ca4e1f3 100644 --- a/app/views/general/_responsive_topnav.html.erb +++ b/app/views/general/_responsive_topnav.html.erb @@ -21,18 +21,22 @@ <%= link_to _("Help"), help_about_path %>
  • - + <% if ! (controller.action_name == 'signin' or controller.action_name == 'signup') %> +
  • + +
  • + <% end %> +
From 1f9ff5da90ee7a2ee1f34e62deaf25401020ed1a Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 1 Dec 2016 13:14:16 +0000 Subject: [PATCH 120/311] Move login bar to partial. --- app/views/general/_log_in_bar.html.erb | 15 +++++++++++++++ app/views/general/_responsive_topnav.html.erb | 15 +-------------- 2 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 app/views/general/_log_in_bar.html.erb diff --git a/app/views/general/_log_in_bar.html.erb b/app/views/general/_log_in_bar.html.erb new file mode 100644 index 0000000000..66686576ee --- /dev/null +++ b/app/views/general/_log_in_bar.html.erb @@ -0,0 +1,15 @@ +
  • + +
  • + diff --git a/app/views/general/_responsive_topnav.html.erb b/app/views/general/_responsive_topnav.html.erb index 974ca4e1f3..d84c7a8894 100644 --- a/app/views/general/_responsive_topnav.html.erb +++ b/app/views/general/_responsive_topnav.html.erb @@ -22,20 +22,7 @@ <% if ! (controller.action_name == 'signin' or controller.action_name == 'signup') %> -
  • - -
  • + <%= render :partial => 'general/log_in_bar' %> <% end %> From 9c8c72f8b53a21f45a4af8396fc28d8210ca9e55 Mon Sep 17 00:00:00 2001 From: Louise Crow Date: Thu, 1 Dec 2016 14:55:00 +0000 Subject: [PATCH 121/311] Add pro styling classes when in pro areas. --- app/views/general/_responsive_header.html.erb | 2 +- app/views/general/_responsive_topnav.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/general/_responsive_header.html.erb b/app/views/general/_responsive_header.html.erb index 4cc0c803e8..a7f62e5ad1 100644 --- a/app/views/general/_responsive_header.html.erb +++ b/app/views/general/_responsive_header.html.erb @@ -1,7 +1,7 @@
    -

    - <% if @info_request.awaiting_description %> - <% if @is_owning_user && !@info_request.is_external? && !@render_to_file %> - <%= n_('Please answer the question above so we know ' \ - 'whether the recent response contains useful information.', - 'Please answer the question above so we know ' \ - 'whether the recent responses contain useful information.', - @new_responses_count) %> - <% else %> - <%= _('This request has an unknown status.') %> - - <% if @old_unclassified %> - <%= n_("We're waiting for someone to read a recent response and " \ - "update the status accordingly. Perhaps " \ - "you might like to help out by doing that?", - "We're waiting for someone to read recent responses and " \ - "update the status accordingly. Perhaps " \ - "you might like to help out by doing that?", - @new_responses_count) %> - <% else %> - <%= n_("We're waiting for {{user}} to read a recent response and update the status.", - "We're waiting for {{user}} to read recent responses and update the status.", - @new_responses_count, - :user => user_link_for_request(@info_request)) %> - <% end %> - <% end %> - <% else %> - <%= status_text(@info_request.calculate_status, - :info_request => @info_request, - :is_owning_user => @is_owning_user, - :redirect_to => request.fullpath) %> - <% end %> -

    -
    + <%= render :partial => 'request_status' %> +

    + From d52b734f25f2e8cda518ac5681dc9ea09d655f02 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 5 Dec 2016 21:30:55 +0000 Subject: [PATCH 143/311] Refactor awaiting description text into a helper --- app/helpers/info_request_helper.rb | 53 ++++++++ app/views/request/_request_status.html.erb | 31 +---- spec/helpers/info_request_helper_spec.rb | 147 +++++++++++++++++++++ 3 files changed, 206 insertions(+), 25 deletions(-) diff --git a/app/helpers/info_request_helper.rb b/app/helpers/info_request_helper.rb index 406bfe8da0..f77d7215e5 100644 --- a/app/helpers/info_request_helper.rb +++ b/app/helpers/info_request_helper.rb @@ -13,6 +13,25 @@ def status_text(status, opts = {}) end end + def awaiting_description_text(info_request, + new_responses_count, + opts = {}) + is_owning_user = opts.fetch(:is_owning_user, false) + render_to_file = opts.fetch(:render_to_file, false) + old_unclassified = opts.fetch(:old_unclassified, false) + + if is_owning_user && !info_request.is_external? && !render_to_file + return awaiting_description_text_owner_please_answer(new_responses_count) + else + if old_unclassified + return awaiting_description_text_old_unclassified(new_responses_count) + else + return awaiting_description_text_other(info_request, + new_responses_count) + end + end + end + private def status_text_waiting_response(opts = {}) @@ -190,4 +209,38 @@ def custom_state_description(status) :locals => { :status => status } end + def awaiting_description_text_owner_please_answer(new_responses_count) + n_('Please answer the question above so we know ' \ + 'whether the recent response contains useful information.', + 'Please answer the question above so we know ' \ + 'whether the recent responses contain useful information.', + new_responses_count) + end + + def awaiting_description_text_unknown + _('This request has an unknown status.') + end + + def awaiting_description_text_old_unclassified(new_responses_count) + str = ''.html_safe + str += awaiting_description_text_unknown + str += ' ' + str += n_('We\'re waiting for someone to read a recent response and ' \ + 'update the status accordingly. Perhaps ' \ + 'you might like to help out by doing that?', + 'We\'re waiting for someone to read recent responses and ' \ + 'update the status accordingly. Perhaps ' \ + 'you might like to help out by doing that?', + new_responses_count) + end + + def awaiting_description_text_other(info_request, new_responses_count) + n_('We\'re waiting for {{user}} to read a recent response and update ' \ + 'the status.', + 'We\'re waiting for {{user}} to read recent responses and update ' \ + 'the status.', + new_responses_count, + user: user_link_for_request(info_request)) + end + end diff --git a/app/views/request/_request_status.html.erb b/app/views/request/_request_status.html.erb index 537f916468..c7091f1a84 100644 --- a/app/views/request/_request_status.html.erb +++ b/app/views/request/_request_status.html.erb @@ -1,33 +1,14 @@ <%# Partial view that displays a line of text displaying the current status of a request %> <% if @info_request.awaiting_description %> - <% if @is_owning_user && !@info_request.is_external? && !@render_to_file %> - <%= n_('Please answer the question above so we know ' \ - 'whether the recent response contains useful information.', - 'Please answer the question above so we know ' \ - 'whether the recent responses contain useful information.', - @new_responses_count) %> - <% else %> - <%= _('This request has an unknown status.') %> - - <% if @old_unclassified %> - <%= n_("We're waiting for someone to read a recent response and " \ - "update the status accordingly. Perhaps " \ - "you might like to help out by doing that?", - "We're waiting for someone to read recent responses and " \ - "update the status accordingly. Perhaps " \ - "you might like to help out by doing that?", - @new_responses_count) %> - <% else %> - <%= n_("We're waiting for {{user}} to read a recent response and update the status.", - "We're waiting for {{user}} to read recent responses and update the status.", - @new_responses_count, - :user => user_link_for_request(@info_request)) %> - <% end %> - <% end %> + <%= awaiting_description_text(@info_request, + @new_responses_count, + :is_owning_user => @is_owning_user, + :render_to_file => @render_to_file, + :old_unclassified => @old_unclassified) %> <% else %> <%= status_text(@info_request.calculate_status, :info_request => @info_request, :is_owning_user => @is_owning_user, :redirect_to => request.fullpath) %> -<% end %> \ No newline at end of file +<% end %> diff --git a/spec/helpers/info_request_helper_spec.rb b/spec/helpers/info_request_helper_spec.rb index fc7ca3798b..9aa7da4869 100644 --- a/spec/helpers/info_request_helper_spec.rb +++ b/spec/helpers/info_request_helper_spec.rb @@ -409,4 +409,151 @@ end + describe '#awaiting_description_text' do + let(:info_request) { FactoryGirl.create(:info_request) } + + shared_examples_for "when we can't ask the user to update the status" do + context "when there's one new reponse" do + it 'asks the user to answer the question' do + expected = "We're waiting for " \ + "#{user_link_for_request(info_request)} to read a " \ + "recent response and update the status." + expect(message).to eq(expected) + end + end + + context "when there's more than one new response" do + it 'asks the user to answer the question' do + expected = "We're waiting for " \ + "#{user_link_for_request(info_request)} to read " \ + "recent responses and update the status." + expect(plural_message).to eq(expected) + end + end + end + + context 'owning user' do + context "when there's one new reponse" do + it 'asks the user to answer the question' do + expected = 'Please answer the question above so ' \ + 'we know whether the recent response contains useful ' \ + 'information.' + actual = awaiting_description_text(info_request, + 1, + :is_owning_user => true, + :render_to_file => false, + :old_unclassified => false) + expect(actual).to eq(expected) + end + end + + context "when there's more than one new response" do + it 'asks the user to answer the question' do + expected = 'Please answer the question above so ' \ + 'we know whether the recent responses contain useful ' \ + 'information.' + actual = awaiting_description_text(info_request, + 3, + :is_owning_user => true, + :render_to_file => false, + :old_unclassified => true) + expect(actual).to eq(expected) + end + end + end + + context 'old, unclassified request' do + context "when there's one new reponse" do + it 'asks the user to answer the question' do + expected = "This request has an unknown status. " \ + "We're waiting for someone to read a recent response " \ + "and update the status accordingly. Perhaps " \ + "you might like to help out by doing " \ + "that?" + actual = awaiting_description_text(info_request, + 1, + :is_owning_user => false, + :render_to_file => false, + :old_unclassified => true) + expect(actual).to eq(expected) + end + end + + context "when there's more than one new response" do + it 'asks the user to answer the question' do + expected = "This request has an unknown status. " \ + "We're waiting for someone to read recent responses " \ + "and update the status accordingly. Perhaps " \ + "you might like to help out by doing " \ + "that?" + actual = awaiting_description_text(info_request, + 3, + :is_owning_user => false, + :render_to_file => false, + :old_unclassified => true) + expect(actual).to eq(expected) + end + end + end + + context 'external request' do + it_behaves_like "when we can't ask the user to update the status" do + let(:info_request) { FactoryGirl.create(:external_request) } + let(:message) do + awaiting_description_text(info_request, + 1, + :is_owning_user => true, + :render_to_file => false, + :old_unclassified => false) + end + let(:plural_message) do + awaiting_description_text(info_request, + 3, + :is_owning_user => true, + :render_to_file => false, + :old_unclassified => false) + end + end + end + + context 'rendering to a file' do + it_behaves_like "when we can't ask the user to update the status" do + let(:message) do + awaiting_description_text(info_request, + 1, + :is_owning_user => true, + :render_to_file => true, + :old_unclassified => false) + end + let(:plural_message) do + awaiting_description_text(info_request, + 3, + :is_owning_user => true, + :render_to_file => true, + :old_unclassified => false) + end + end + end + + context 'non-owner viewing a recent request' do + it_behaves_like "when we can't ask the user to update the status" do + let(:message) do + awaiting_description_text(info_request, + 1, + :is_owning_user => false, + :render_to_file => false, + :old_unclassified => false) + end + let(:plural_message) do + awaiting_description_text(info_request, + 3, + :is_owning_user => false, + :render_to_file => false, + :old_unclassified => false) + end + end + end + + end + end From 2040c41498f1de5822deea074d80670584da1b41 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 6 Dec 2016 11:18:55 +0000 Subject: [PATCH 144/311] Refactor request page view tracking into a partial --- app/views/request/_ga_events.html.erb | 9 +++++++++ app/views/request/show.html.erb | 10 +++------- 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 app/views/request/_ga_events.html.erb diff --git a/app/views/request/_ga_events.html.erb b/app/views/request/_ga_events.html.erb new file mode 100644 index 0000000000..88e226e3af --- /dev/null +++ b/app/views/request/_ga_events.html.erb @@ -0,0 +1,9 @@ +<% if flash[:request_sent] %> + <% content_for :ga_pageview do %> + ga('send', 'pageview', '<%= show_new_request_path(@info_request.url_title) %>'); + <% end %> +<% else %> + <% content_for :ga_pageview do %> + ga('send', 'pageview', '<%= show_request_path(@info_request.url_title) %>'); + <% end %> +<% end %> diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index 6c2f7e7097..aaf8f95ff9 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -9,18 +9,14 @@ <% end %> <% end %> + <% if flash[:request_sent] %> <%= render :partial => 'request_sent', :locals => { :info_request => @info_request } %> - <% content_for :ga_pageview do %> - ga('send', 'pageview', '<%= show_new_request_path(@info_request.url_title) %>'); - <% end %> -<% else %> - <% content_for :ga_pageview do %> - ga('send', 'pageview', '<%= show_request_path(@info_request.url_title) %>'); - <% end %> <% end %> +<%= render :partial => 'ga_events' %> + <% if @info_request.prominence(:decorate => true).is_hidden? %>

    <%= _("This request has prominence 'hidden'. You can only see it because " \ From 858a55f4992b1ec41d2695d7fb1cab1a9e60d107 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 6 Dec 2016 11:20:47 +0000 Subject: [PATCH 145/311] Refactor hidden request messages into a partial --- .../request/_hidden_request_messages.html.erb | 14 ++++++++++++++ app/views/request/show.html.erb | 16 +--------------- 2 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 app/views/request/_hidden_request_messages.html.erb diff --git a/app/views/request/_hidden_request_messages.html.erb b/app/views/request/_hidden_request_messages.html.erb new file mode 100644 index 0000000000..0dd6452580 --- /dev/null +++ b/app/views/request/_hidden_request_messages.html.erb @@ -0,0 +1,14 @@ +<% if @info_request.prominence(:decorate => true).is_hidden? %> +

    + <%= _("This request has prominence 'hidden'. You can only see it because " \ + "you are logged in as a super user.") %> +

    +<% end %> + +<% if @info_request.prominence(:decorate => true).is_requester_only? %> +

    + <%= _("This request is hidden, so that only you the requester can see it. " \ + "Please contact us if you are not sure why.", + :url => help_requesting_path.html_safe) %> +

    +<% end %> diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index aaf8f95ff9..e55bdc7c1f 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -17,21 +17,7 @@ <%= render :partial => 'ga_events' %> -<% if @info_request.prominence(:decorate => true).is_hidden? %> -

    - <%= _("This request has prominence 'hidden'. You can only see it because " \ - "you are logged in as a super user.") %> -

    -<% end %> - -<% if @info_request.prominence(:decorate => true).is_requester_only? %> -

    - <%= _("This request is hidden, so that only you the requester can see it. " \ - "Please contact us if you are not sure why.", - :url => help_requesting_path.html_safe) %> -

    -<% end %> - +<%= render :partial => 'hidden_request_messages' %> <% if ( @update_status || @info_request.awaiting_description ) && ! @info_request.is_external? %>
    From 407f1465cba56080e8ff008e8739bb68d4616ad4 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 6 Dec 2016 11:22:53 +0000 Subject: [PATCH 146/311] Refactor request meta tags into a partial --- app/views/request/_request_meta_tags.html.erb | 7 +++++++ app/views/request/show.html.erb | 8 +------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 app/views/request/_request_meta_tags.html.erb diff --git a/app/views/request/_request_meta_tags.html.erb b/app/views/request/_request_meta_tags.html.erb new file mode 100644 index 0000000000..5beab30ecb --- /dev/null +++ b/app/views/request/_request_meta_tags.html.erb @@ -0,0 +1,7 @@ +<% if @info_request.prominence(:decorate => true).is_public? %> + <% content_for :open_graph_tags do %> + + + + <% end %> +<% end %> diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index e55bdc7c1f..9143c37054 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -2,13 +2,7 @@ :title => h(@info_request.title), :public_body => (@info_request.public_body.name)) %> -<% if @info_request.prominence(:decorate => true).is_public? %> - <% content_for :open_graph_tags do %> - - - - <% end %> -<% end %> +<%= render :partial => 'request_meta_tags' %> <% if flash[:request_sent] %> <%= render :partial => 'request_sent', From 51b32f66b313cc2ab1db69aacafb56918cbe48b5 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 6 Dec 2016 11:23:25 +0000 Subject: [PATCH 147/311] Remove uneeded locals from request_sent partial usage --- app/views/request/show.html.erb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index 9143c37054..4a0b6349f7 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -4,10 +4,7 @@ <%= render :partial => 'request_meta_tags' %> -<% if flash[:request_sent] %> - <%= render :partial => 'request_sent', - :locals => { :info_request => @info_request } %> -<% end %> +<% if flash[:request_sent] %><%= render :partial => 'request_sent' %><% end %> <%= render :partial => 'ga_events' %> From b64d69cfa60f2d3e15c2b255d620ee8f2b0c28d8 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 6 Dec 2016 11:26:58 +0000 Subject: [PATCH 148/311] Refactor request subtitle into a partial --- app/views/request/_request_subtitle.html.erb | 15 +++++++++++++++ app/views/request/show.html.erb | 16 +--------------- 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 app/views/request/_request_subtitle.html.erb diff --git a/app/views/request/_request_subtitle.html.erb b/app/views/request/_request_subtitle.html.erb new file mode 100644 index 0000000000..398f828f8a --- /dev/null +++ b/app/views/request/_request_subtitle.html.erb @@ -0,0 +1,15 @@ +<% if !@user.nil? && @user.admin_page_links? %> + <%= _('{{user}} ({{user_admin_link}}) made this {{law_used_full}} request (admin) to {{public_body_link}} (admin)', + :user => request_user_link(@info_request, _('An anonymous user')), + :law_used_full => h(@info_request.law_used_human(:full)), + :user_admin_link => user_admin_link_for_request( + @info_request, _('external'), _('admin')), + :request_admin_url => admin_request_url(@info_request), + :public_body_link => public_body_link(@info_request.public_body), + :public_body_admin_url => admin_body_url(@info_request.public_body)) %> +<% else %> + <%= _('{{user}} made this {{law_used_full}} request', + :user=>request_user_link(@info_request, _('An anonymous user')), + :law_used_full=>h(@info_request.law_used_human(:full))) %> + <%= _('to {{public_body}}',:public_body=>public_body_link(@info_request.public_body)) %> +<% end %> \ No newline at end of file diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index 4a0b6349f7..64d7c6692d 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -29,21 +29,7 @@
    <% end %>

    - <% if !@user.nil? && @user.admin_page_links? %> - <%= _('{{user}} ({{user_admin_link}}) made this {{law_used_full}} request (admin) to {{public_body_link}} (admin)', - :user => request_user_link(@info_request, _('An anonymous user')), - :law_used_full => h(@info_request.law_used_human(:full)), - :user_admin_link => user_admin_link_for_request( - @info_request, _('external'), _('admin')), - :request_admin_url => admin_request_url(@info_request), - :public_body_link => public_body_link(@info_request.public_body), - :public_body_admin_url => admin_body_url(@info_request.public_body)) %> - <% else %> - <%= _('{{user}} made this {{law_used_full}} request', - :user=>request_user_link(@info_request, _('An anonymous user')), - :law_used_full=>h(@info_request.law_used_human(:full))) %> - <%= _('to {{public_body}}',:public_body=>public_body_link(@info_request.public_body)) %> - <% end %> + <%= render :partial => 'request_subtitle' %>

    <% unless @render_to_file %>
    From 2faa28872702108246028abfbcdbf0f845295351 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 6 Dec 2016 17:11:15 +0000 Subject: [PATCH 149/311] Add a @show_profile_photo variable to request_controller#show --- app/controllers/request_controller.rb | 3 +++ app/views/request/show.html.erb | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 01c14726d9..4d74ba3809 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -862,6 +862,9 @@ def assign_variables_for_show_template(info_request) # For send followup link at bottom @last_response = info_request.get_last_public_response @follower_count = @info_request.track_things.count + 1 + @show_profile_photo = !@info_request.is_external? && \ + @info_request.user.profile_photo && \ + !@render_to_file end def make_request_zip(info_request, file_path) diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index 64d7c6692d..a6873c7a84 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -23,16 +23,16 @@
    - <% if !@info_request.is_external? && @info_request.user.profile_photo && !@render_to_file %> + <% if @show_profile_photo %>
    <% end %> -

    +

    <%= render :partial => 'request_subtitle' %>

    <% unless @render_to_file %> -
    +
    <%= render :partial => 'after_actions' %> <%= render :partial => 'track/tracking_links_simple', :locals => { :track_thing => @track_thing, :own_request => @info_request.user && @info_request.user == @user, :location => 'toolbar ' } %>
    From dbe425b0a4f2f1ceb9f768a9b3e42e1fb82773d9 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 09:32:00 +0000 Subject: [PATCH 150/311] Add a pro flag variable in RequestController#show --- app/controllers/request_controller.rb | 5 +++++ spec/controllers/request_controller_spec.rb | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 4d74ba3809..3ab335426d 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -91,6 +91,11 @@ def show # assign variables from request parameters @collapse_quotes = !params[:unfold] + + # TODO: make this the same as whatever we use in the pro layout + # This should come from defaults: { pro: true } in routes.rb + @pro = params[:pro] == "1" + # Don't allow status update on external requests, otherwise accept param if @info_request.is_external? @update_status = false diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index ecc93cfc79..d8bbac5661 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -237,6 +237,20 @@ def make_request expect(response).to render_template "request/show" end end + + describe 'when params[:pro] is true' do + it "should set @pro to true" do + get :show, :url_title => 'why_do_you_have_such_a_fancy_dog', pro: "1" + expect(assigns[:pro]).to be true + end + end + + describe 'when params[:pro] is not set' do + it "should set @pro to false" do + get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' + expect(assigns[:pro]).to be false + end + end end describe RequestController do From 134a4900c45ac01992181a9980a742a09315f1f4 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 09:33:50 +0000 Subject: [PATCH 151/311] Switch the request sidebar template based on @pro --- app/controllers/request_controller.rb | 1 + .../info_requests/_sidebar.html.erb | 1 + app/views/request/show.html.erb | 2 +- spec/controllers/request_controller_spec.rb | 19 +++++++++++++++++-- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 app/views/alaveteli_pro/info_requests/_sidebar.html.erb diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 3ab335426d..24422c2d53 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -121,6 +121,7 @@ def show # Sidebar stuff @sidebar = true @similar_cache_key = cache_key_for_similar_requests(@info_request, @locale) + @sidebar_template = @pro ? "alaveteli_pro/info_requests/sidebar" : "sidebar" # Track corresponding to this page @track_thing = TrackThing.create_track_for_request(@info_request) diff --git a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb new file mode 100644 index 0000000000..3472234b80 --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb @@ -0,0 +1 @@ +This is the pro sidebar \ No newline at end of file diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index a6873c7a84..9b17443247 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -75,7 +75,7 @@
    -<%- if @sidebar %><%= render :partial => 'sidebar' %><% end %> +<%- if @sidebar %><%= render :partial => @sidebar_template %><% end %> <%= content_for :javascript do %> <%= javascript_include_tag 'request-attachments.js' %> diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index d8bbac5661..e36cd24f0a 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -239,17 +239,32 @@ def make_request end describe 'when params[:pro] is true' do - it "should set @pro to true" do + before :each do get :show, :url_title => 'why_do_you_have_such_a_fancy_dog', pro: "1" + end + + it "should set @pro to true" do expect(assigns[:pro]).to be true end + + it "should set @sidebar_template to the pro sidebar" do + expect(assigns[:sidebar_template]). + to eq ("alaveteli_pro/info_requests/sidebar") + end end describe 'when params[:pro] is not set' do - it "should set @pro to false" do + before :each do get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' + end + + it "should set @pro to false" do expect(assigns[:pro]).to be false end + + it "should set @sidebar_template to the normal sidebar" do + expect(assigns[:sidebar_template]).to eq ("sidebar") + end end end From b0b4ee16eaeaa84f539e60157261657845766e9b Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 10:28:21 +0000 Subject: [PATCH 152/311] Add a pro route to the show controller --- config/routes.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 841f54c676..56c1b2c610 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -603,6 +603,12 @@ get :preview, on: :new # /info_request/new/preview end end + # So that we can show a request using the existing controller from the + # pro context + match '/alaveteli_pro/info_requests/:url_title' => 'request#show', + :as => :show_alaveteli_pro_request, + :via => :get, + :defaults => { :pro => "1" } end #### From 30da36d985ffc0ae15f2852224ff81d64670ced2 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 8 Dec 2016 12:30:57 +0000 Subject: [PATCH 153/311] Redirect pro requests by ID to the pro show page --- app/controllers/request_controller.rb | 8 +++++++- spec/controllers/request_controller_spec.rb | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 24422c2d53..96b3586c68 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -1074,7 +1074,13 @@ def redirect_numeric_id_to_url_title # Look up by old style numeric identifiers if params[:url_title].match(/^[0-9]+$/) @info_request = InfoRequest.find(params[:url_title].to_i) - redirect_to request_url(@info_request, :format => params[:format]) + if params[:pro] == "1" + redirect_to show_alaveteli_pro_request_url( + :url_title => @info_request.url_title, + :format => params[:format]) + else + redirect_to request_url(@info_request, :format => params[:format]) + end end end end diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index e36cd24f0a..73da6231f2 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -157,6 +157,16 @@ expect(response).to redirect_to(:action => 'show', :url_title => info_requests(:naughty_chicken_request).url_title) end + context "when showing a request in the pro context" do + it "should redirect from a numeric URL to pretty one in the pro namespace" do + get :show, :url_title => info_requests(:naughty_chicken_request).id.to_s, + :pro => "1" + expect(response). + to redirect_to(show_alaveteli_pro_request_path( + :url_title => info_requests(:naughty_chicken_request).url_title)) + end + end + context 'when the request is embargoed' do it 'raises ActiveRecord::RecordNotFound' do embargoed_request = FactoryGirl.create(:embargoed_request) From 82a36334a0b8769e10e15743bc5f0c4529bac377 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 12:35:02 +0000 Subject: [PATCH 154/311] Refactor setting @update_status into a method --- app/controllers/request_controller.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 96b3586c68..e54c0a0d29 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -96,15 +96,11 @@ def show # This should come from defaults: { pro: true } in routes.rb @pro = params[:pro] == "1" - # Don't allow status update on external requests, otherwise accept param - if @info_request.is_external? - @update_status = false - else - @update_status = params[:update_status] - end + @update_status = can_update_status(@info_request) assign_variables_for_show_template(@info_request) + # Only owners (and people who own everything) can update status if @update_status return if !@is_owning_user && !authenticated_as_user?( @info_request.user, @@ -857,6 +853,11 @@ def outgoing_message_params params.require(:outgoing_message).permit(:body, :what_doing) end + def can_update_status(info_request) + # Don't allow status update on external requests, otherwise accept param + info_request.is_external? ? false : params[:update_status] == "1" + end + def assign_variables_for_show_template(info_request) @info_request = info_request @info_request_events = info_request.info_request_events From 8ad166f5e65d802709d537da76bd4dee149fc846 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 12:38:17 +0000 Subject: [PATCH 155/311] Add new variables to control setting of describe state forms --- app/controllers/request_controller.rb | 6 +++- app/views/request/show.html.erb | 4 +-- spec/views/request/show.html.erb_spec.rb | 46 +++--------------------- 3 files changed, 12 insertions(+), 44 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index e54c0a0d29..fa2aaddf16 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -93,7 +93,6 @@ def show @collapse_quotes = !params[:unfold] # TODO: make this the same as whatever we use in the pro layout - # This should come from defaults: { pro: true } in routes.rb @pro = params[:pro] == "1" @update_status = can_update_status(@info_request) @@ -872,6 +871,11 @@ def assign_variables_for_show_template(info_request) @show_profile_photo = !@info_request.is_external? && \ @info_request.user.profile_photo && \ !@render_to_file + @show_top_describe_state_form = (@update_status || \ + @info_request.awaiting_description ) && \ + !@info_request.is_external? + @show_bottom_describe_state_form = @info_request.awaiting_description && \ + !@info_request.is_external? end def make_request_zip(info_request, file_path) diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index 9b17443247..12aa3dab97 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -10,7 +10,7 @@ <%= render :partial => 'hidden_request_messages' %> -<% if ( @update_status || @info_request.awaiting_description ) && ! @info_request.is_external? %> +<% if @show_top_describe_state_form %>
    <%= render :partial => 'describe_state', :locals => { :id_suffix => "1" } %>
    @@ -55,7 +55,7 @@ <% end %> <% end %> - <% if @info_request.awaiting_description && ! @info_request.is_external? %> + <% if @show_bottom_describe_state_form %>
    <%= render :partial => 'describe_state', :locals => { :id_suffix => "2" } %>
    diff --git a/spec/views/request/show.html.erb_spec.rb b/spec/views/request/show.html.erb_spec.rb index 3e04468717..a5f05db113 100644 --- a/spec/views/request/show.html.erb_spec.rb +++ b/spec/views/request/show.html.erb_spec.rb @@ -37,9 +37,9 @@ def request_page expect(rendered).to have_css("h1",:text => "Test request") end - describe "when a status update has been requested" do + describe "when told to show the top describe state form" do before do - assign :update_status, true + assign :show_top_describe_state_form, true end it "should show the first form for describing the state of the request" do @@ -49,15 +49,9 @@ def request_page end end - describe "when it is awaiting a description" do - before :each do - allow(mock_request).to receive(:awaiting_description).and_return(true) - request_page - end - - it "should show the first form for describing the state of the request" do - expect(rendered). - to have_css("div.describe_state_form#describe_state_form_1") + describe "when told to show the bottom describe state form" do + before do + assign :show_bottom_describe_state_form, true end it "should show the second form for describing the state of the request" do @@ -116,23 +110,7 @@ def request_page assign :user, admin_user # Admins own every request assign :is_owning_user, true - end - context "and the request is awaiting description" do - before :each do - allow(mock_request).to receive(:awaiting_description).and_return(true) - request_page - end - - it "should show the describe state form" do - expect(rendered).to have_css("div.describe_state_form") - end - - it "should ask the user to use the describe state from" do - expect(response.body).to have_css( - "p#request_status", - :text => "answer the question above") - end end context "and the request is waiting for a response and very overdue" do @@ -183,20 +161,6 @@ def request_page assign :user, admin_user end - context 'and the request is awaiting description' do - it 'should not show the describe state form' do - request_page - expect(rendered).not_to have_css('div.describe_state_form') - end - - it 'should not ask the user to use the describe state form' do - request_page - expect(rendered).not_to have_css( - 'p#request_status', - :text => "answer the question above") - end - end - context 'and the request is waiting for a response and very overdue' do before do allow(mock_request). From 0cfb218d1dc3f8bfc7e42d255192846c6b569d9c Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 12:43:40 +0000 Subject: [PATCH 156/311] Don't show describe state forms when viewing in a pro context --- app/controllers/request_controller.rb | 10 +-- spec/controllers/request_controller_spec.rb | 72 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index fa2aaddf16..e8284e58f3 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -871,11 +871,11 @@ def assign_variables_for_show_template(info_request) @show_profile_photo = !@info_request.is_external? && \ @info_request.user.profile_photo && \ !@render_to_file - @show_top_describe_state_form = (@update_status || \ - @info_request.awaiting_description ) && \ - !@info_request.is_external? - @show_bottom_describe_state_form = @info_request.awaiting_description && \ - !@info_request.is_external? + @show_top_describe_state_form = !@pro && \ + (@update_status || \ + @info_request.awaiting_description ) + @show_bottom_describe_state_form = !@pro && \ + @info_request.awaiting_description end def make_request_zip(info_request, file_path) diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 73da6231f2..c46c0cffd7 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -276,6 +276,78 @@ def make_request expect(assigns[:sidebar_template]).to eq ("sidebar") end end + + describe "@show_top_describe_state_form" do + context "when @pro is true" do + it "is false" do + get :show, :url_title => 'why_do_you_have_such_a_fancy_dog', + :pro => "1", + :update_status => "1" + expect(assigns[:show_top_describe_state_form]).to be false + end + end + context "when @pro is false" do + context "and @update_status is false" do + it "is false" do + info_request = info_requests(:naughty_chicken_request) + expect(info_request.awaiting_description).to be false + get :show, :url_title => info_request.url_title + expect(assigns[:show_top_describe_state_form]).to be false + end + + context "but the request is awaiting_description" do + it "is true" do + get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' + expect(assigns[:show_top_describe_state_form]).to be true + end + end + end + context "and @update_status is true" do + it "is true" do + session[:user_id] = users(:bob_smith_user).id + info_request = info_requests(:naughty_chicken_request) + expect(info_request.awaiting_description).to be false + get :show, :url_title => info_request.url_title, + :update_status => "1" + expect(assigns[:show_top_describe_state_form]).to be true + end + + context "and the request is awaiting_description" do + it "is true" do + get :show, :url_title => 'why_do_you_have_such_a_fancy_dog', + :update_status => "1" + expect(assigns[:show_top_describe_state_form]).to be true + end + end + end + end + end + + describe "@show_bottom_describe_state_form" do + context "when @pro is true" do + it "is false" do + get :show, :url_title => 'why_do_you_have_such_a_fancy_dog', + :pro => "1" + expect(assigns[:show_bottom_describe_state_form]).to be false + end + end + context "when @pro is false" do + context "and the request is awaiting_description" do + it "is true" do + get :show, :url_title => 'why_do_you_have_such_a_fancy_dog' + expect(assigns[:show_bottom_describe_state_form]).to be true + end + end + context "and the request is not awaiting_description" do + it "is false" do + info_request = info_requests(:naughty_chicken_request) + expect(info_request.awaiting_description).to be false + get :show, :url_title => info_request.url_title + expect(assigns[:show_bottom_describe_state_form]).to be false + end + end + end + end end describe RequestController do From 6d6b62219c172fcbe4481d811947aaae3e46b606 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 21:32:16 +0000 Subject: [PATCH 157/311] Add an EmbargoExtension model --- app/models/embargo.rb | 1 + app/models/embargo_extension.rb | 7 ++++ ...0161207184708_create_embargo_extensions.rb | 10 ++++++ spec/factories/embargo_extensions.rb | 6 ++++ spec/models/embargo_extension_spec.rb | 32 +++++++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 app/models/embargo_extension.rb create mode 100644 db/migrate/20161207184708_create_embargo_extensions.rb create mode 100644 spec/factories/embargo_extensions.rb create mode 100644 spec/models/embargo_extension_spec.rb diff --git a/app/models/embargo.rb b/app/models/embargo.rb index dfc493c8ad..c2b79fbbbe 100644 --- a/app/models/embargo.rb +++ b/app/models/embargo.rb @@ -14,6 +14,7 @@ class Embargo < ActiveRecord::Base belongs_to :info_request + has_many :embargo_extensions validates_presence_of :info_request validates_presence_of :publish_at validates_inclusion_of :embargo_duration, diff --git a/app/models/embargo_extension.rb b/app/models/embargo_extension.rb new file mode 100644 index 0000000000..dc10399a31 --- /dev/null +++ b/app/models/embargo_extension.rb @@ -0,0 +1,7 @@ +class EmbargoExtension < ActiveRecord::Base + belongs_to :embargo + validates_presence_of :embargo_id + validates_presence_of :extension_duration + validates_inclusion_of :extension_duration, + in: lambda { |e| Embargo.new.allowed_durations } +end diff --git a/db/migrate/20161207184708_create_embargo_extensions.rb b/db/migrate/20161207184708_create_embargo_extensions.rb new file mode 100644 index 0000000000..42d826edb8 --- /dev/null +++ b/db/migrate/20161207184708_create_embargo_extensions.rb @@ -0,0 +1,10 @@ +class CreateEmbargoExtensions < ActiveRecord::Migration + def change + create_table :embargo_extensions do |t| + t.integer :embargo_id + t.string :extension_duration + + t.timestamps + end + end +end diff --git a/spec/factories/embargo_extensions.rb b/spec/factories/embargo_extensions.rb new file mode 100644 index 0000000000..6c13aafd81 --- /dev/null +++ b/spec/factories/embargo_extensions.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :embargo_extension do + embargo + extension_duration "3_months" + end +end diff --git a/spec/models/embargo_extension_spec.rb b/spec/models/embargo_extension_spec.rb new file mode 100644 index 0000000000..9fdc4034e1 --- /dev/null +++ b/spec/models/embargo_extension_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +RSpec.describe EmbargoExtension do + let(:embargo_extension) { FactoryGirl.create(:embargo_extension) } + + it "has an embargo" do + expect(embargo_extension.embargo).to be_a Embargo + end + + it "has an extension_duration" do + expect(embargo_extension.extension_duration).to be_a String + end + + it "requires an embargo" do + embargo_extension.embargo = nil + expect(embargo_extension).not_to be_valid + end + + it "requires an extension duration" do + embargo_extension.extension_duration = nil + expect(embargo_extension).not_to be_valid + end + + it 'validates extension_duration field is in list' do + embargo_extension.embargo.allowed_durations.each do |duration| + embargo_extension.extension_duration = duration + expect(embargo_extension).to be_valid + end + embargo_extension.extension_duration = "not_in_list" + expect(embargo_extension).not_to be_valid + end +end From 0fa5861a8bb91b5de40efb3e163b7b411c579c8e Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 22:08:29 +0000 Subject: [PATCH 158/311] Fix bug in Embargo factory --- spec/factories/embargos.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/factories/embargos.rb b/spec/factories/embargos.rb index 79e86e1063..9bae850014 100644 --- a/spec/factories/embargos.rb +++ b/spec/factories/embargos.rb @@ -14,7 +14,7 @@ FactoryGirl.define do factory :embargo do info_request - publish_at Time.now + 3.months + publish_at Time.zone.today + 3.months embargo_duration "3_months" end end From d0fe4ca6e31f43bd34ffa30edc770104279728ab Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 22:09:42 +0000 Subject: [PATCH 159/311] Add an #extend method to Embargo --- app/models/embargo.rb | 13 ++++++++++--- spec/models/embargo_spec.rb | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/models/embargo.rb b/app/models/embargo.rb index c2b79fbbbe..fac22c004f 100644 --- a/app/models/embargo.rb +++ b/app/models/embargo.rb @@ -38,18 +38,25 @@ def allowed_durations DURATIONS.keys end - def duration_as_duration - DURATIONS[self.embargo_duration].call + def duration_as_duration(duration = nil) + duration ||= self.embargo_duration + DURATIONS[duration].call end def duration_label DURATION_LABELS[self.embargo_duration] end + def extend(extension) + self.publish_at += duration_as_duration(extension.extension_duration) + save + end + + private + def set_publish_at_from_duration unless self.publish_at.present? || self.embargo_duration.blank? self.publish_at = Time.zone.today + duration_as_duration end end - end diff --git a/spec/models/embargo_spec.rb b/spec/models/embargo_spec.rb index 7d519d6f94..b5fb340fee 100644 --- a/spec/models/embargo_spec.rb +++ b/spec/models/embargo_spec.rb @@ -62,4 +62,17 @@ expect(embargo.publish_at).to eq Time.zone.today end end + + describe 'extending' do + let(:embargo_extension) { FactoryGirl.create(:embargo_extension) } + let(:embargo) { embargo_extension.embargo } + + it 'allows extending the embargo' do + old_publish_at = embargo.publish_at + expect(old_publish_at).to eq Time.zone.today + 3.months + embargo.extend(embargo_extension) + expected_publish_at = old_publish_at + 3.months + expect(embargo.publish_at).to eq expected_publish_at + end + end end From d62fe40b2c94a2fee0c5e98aff1c45e499655f2c Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 22:46:28 +0000 Subject: [PATCH 160/311] Add an ability for updating embargoes --- app/models/ability.rb | 7 ++++++- spec/models/ability_spec.rb | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 7597f73fe0..7b3e173f7e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -47,11 +47,16 @@ def initialize(user) self.class.can_view_with_prominence?(request.prominence, request, user) end - # Accessing alaveteli professional if feature_enabled? :alaveteli_pro + # Accessing alaveteli professional if user && (user.super? || user.pro?) can :access, :alaveteli_pro end + + # Extending embargoes + can :update, Embargo do |embargo| + user && (user == embargo.info_request.user || user.super?) + end end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index bac4098c1d..7f49e3770a 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -258,4 +258,38 @@ end end end + + describe "Updating Embargoes" do + let(:embargo) { FactoryGirl.create(:embargo) } + let(:admin_user) { FactoryGirl.create(:admin_user) } + + it "allows the info request owner to update it" do + with_feature_enabled(:alaveteli_pro) do + ability = Ability.new(embargo.info_request.user) + expect(ability).to be_able_to(:update, embargo) + end + end + + it "allows admins to update it" do + with_feature_enabled(:alaveteli_pro) do + ability = Ability.new(admin_user) + expect(ability).to be_able_to(:update, embargo) + end + end + + it "doesnt allow anonymous users to update it" do + with_feature_enabled(:alaveteli_pro) do + ability = Ability.new(nil) + expect(ability).not_to be_able_to(:update, embargo) + end + end + + it "doesnt allow other users to update it" do + other_user = FactoryGirl.create(:user) + with_feature_enabled(:alaveteli_pro) do + ability = Ability.new(other_user) + expect(ability).not_to be_able_to(:update, embargo) + end + end + end end From b02768fe3fd8146833488e51088516a413d1a638 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 22:46:50 +0000 Subject: [PATCH 161/311] Add a route for creating embargo extensions --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 56c1b2c610..72b1f26c81 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -602,6 +602,7 @@ resources :info_requests, :only => [:new, :create, :index] do get :preview, on: :new # /info_request/new/preview end + resources :embargo_extensions, :only => [:create] end # So that we can show a request using the existing controller from the # pro context From 0471e75eb50ab441bd883657cb07484ca6ff96f3 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 9 Dec 2016 16:09:50 +0000 Subject: [PATCH 162/311] Rescue from CanCan:AccessDenied errors --- app/controllers/application_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8fc0454ead..655250f547 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -17,6 +17,11 @@ class RouteNotFound < StandardError protect_from_forgery :if => :user? skip_before_filter :verify_authenticity_token, :unless => :user? + # Deal with access denied errors from CanCan + rescue_from CanCan::AccessDenied do |exception| + raise PermissionDenied + end + # assign our own handler method for non-local exceptions rescue_from Exception, :with => :render_exception From e5b27f4b0db3c197934c9eeb554417e9341512fa Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 22:47:23 +0000 Subject: [PATCH 163/311] Add a controller for creating embargo extensions --- .../embargo_extensions_controller.rb | 32 ++++++ .../embargo_extensions_controller_spec.rb | 99 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 app/controllers/alaveteli_pro/embargo_extensions_controller.rb create mode 100644 spec/controllers/alaveteli_pro/embargo_extensions_controller_spec.rb diff --git a/app/controllers/alaveteli_pro/embargo_extensions_controller.rb b/app/controllers/alaveteli_pro/embargo_extensions_controller.rb new file mode 100644 index 0000000000..1d85c4fbe7 --- /dev/null +++ b/app/controllers/alaveteli_pro/embargo_extensions_controller.rb @@ -0,0 +1,32 @@ +# -*- encoding : utf-8 -*- +# app/controllers/alaveteli_pro/embargo_extensions_controller.rb +# Controller for embargo extensions +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ + +class AlaveteliPro::EmbargoExtensionsController < AlaveteliPro::BaseController + def create + @embargo = Embargo.find(embargo_extension_params[:embargo_id]) + authorize! :update, @embargo + @info_request = @embargo.info_request + @embargo_extension = EmbargoExtension.new(embargo_extension_params) + if @embargo_extension.save + @embargo.extend(@embargo_extension) + flash[:notice] = _("Your Embargo has been extended! It will now " \ + "expire on {{expiry_date}}.", + expiry_date: @embargo.publish_at.to_date) + else + flash[:error] = _("Sorry, something went wrong extending your " \ + "embargo, please try again.") + end + redirect_to show_alaveteli_pro_request_path( + url_title: @info_request.url_title) + end + + private + + def embargo_extension_params + params.require(:embargo_extension).permit(:embargo_id, :extension_duration) + end +end diff --git a/spec/controllers/alaveteli_pro/embargo_extensions_controller_spec.rb b/spec/controllers/alaveteli_pro/embargo_extensions_controller_spec.rb new file mode 100644 index 0000000000..e3bd1c030f --- /dev/null +++ b/spec/controllers/alaveteli_pro/embargo_extensions_controller_spec.rb @@ -0,0 +1,99 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') + +describe AlaveteliPro::EmbargoExtensionsController do + let(:pro_user) { FactoryGirl.create(:pro_user) } + let(:admin) { FactoryGirl.create(:admin_user) } + let(:info_request) { FactoryGirl.create(:info_request, user: pro_user) } + let(:embargo) { FactoryGirl.create(:embargo, info_request: info_request) } + + describe "#create" do + context "when the user is allowed to update the embargo" do + context "because they are the owner" do + before do + with_feature_enabled(:alaveteli_pro) do + session[:user_id] = pro_user.id + post :create, + embargo_extension: { embargo_id: embargo.id, + extension_duration: "3_months" } + end + end + + it "updates the embargo" do + expect(embargo.reload.publish_at.to_date).to eq Time.zone.today + 6.months + end + + it "sets a flash message" do + expect(flash[:notice]).to eq "Your Embargo has been extended! It "\ + "will now expire on " \ + "#{Time.zone.today + 6.months}." + end + + it "redirects to the request show page" do + expect(response). + to redirect_to show_alaveteli_pro_request_path( + url_title: info_request.url_title) + end + end + + context "because they are an admin" do + before do + with_feature_enabled(:alaveteli_pro) do + session[:user_id] = pro_user.id + post :create, + embargo_extension: { embargo_id: embargo.id, + extension_duration: "3_months" } + end + end + + it "updates the embargo" do + expect(embargo.reload.publish_at.to_date).to eq Time.zone.today + 6.months + end + + it "sets a flash message" do + expect(flash[:notice]).to eq "Your Embargo has been extended! It "\ + "will now expire on " \ + "#{Time.zone.today + 6.months}." + end + + it "redirects to the request show page" do + expect(response). + to redirect_to show_alaveteli_pro_request_path( + url_title: info_request.url_title) + end + end + end + + context "when the user is not allowed to update the embargo" do + before do + other_user = FactoryGirl.create(:pro_user) + with_feature_enabled(:alaveteli_pro) do + session[:user_id] = other_user.id + end + end + + it "raises a CanCan::AccessDenied error" do + expect do + post :create, + embargo_extension: { embargo_id: embargo.id, + extension_duration: "3_months" } + end.to raise_error(CanCan::AccessDenied) + end + end + + context "when the extension is invalid" do + before do + with_feature_enabled(:alaveteli_pro) do + session[:user_id] = pro_user.id + post :create, + embargo_extension: { embargo_id: embargo.id } + end + end + + it "sets a flash error message" do + expect(flash[:error]).to eq "Sorry, something went wrong extending " \ + "your embargo, please try again." + end + end + end +end \ No newline at end of file From 8e73ca814dd961cf0c7ede605239f632663a033a Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 8 Dec 2016 10:19:36 +0000 Subject: [PATCH 164/311] Rework embargo_info partial template for different tenses --- .../info_requests/_embargo_info.html.erb | 17 +++++++++++++---- .../info_requests/_sidebar.html.erb | 2 +- .../alaveteli_pro/info_requests/new.html.erb | 5 +++-- .../info_requests/preview.html.erb | 5 +++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/views/alaveteli_pro/info_requests/_embargo_info.html.erb b/app/views/alaveteli_pro/info_requests/_embargo_info.html.erb index 82599bd5a0..6af38df77e 100644 --- a/app/views/alaveteli_pro/info_requests/_embargo_info.html.erb +++ b/app/views/alaveteli_pro/info_requests/_embargo_info.html.erb @@ -1,9 +1,18 @@

    Embargo

    -<% if @embargo && @embargo.publish_at %> +<% if embargo && embargo.publish_at %>

    - <%= _("This request will be embargoed until {{embargo_publish_at}}", - embargo_publish_at: @embargo.publish_at.to_date) %> + <% if tense == :future %> + <%= _("This request will be embargoed until {{embargo_publish_at}}", + embargo_publish_at: embargo.publish_at.to_date) %> + <% else %> + <%= _("This request is embargoed until {{embargo_publish_at}}", + embargo_publish_at: embargo.publish_at.to_date) %> + <% end %>

    <% else %> -

    <%= _("This request will be published immediately") %>

    + <% if tense == :future %> +

    <%= _("This request will be published immediately") %>

    + <% else %> +

    <%= _("This request is public") %>

    + <% end %> <% end %> diff --git a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb index 3472234b80..a2b5893547 100644 --- a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb +++ b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb @@ -1 +1 @@ -This is the pro sidebar \ No newline at end of file +This is the pro sidebar diff --git a/app/views/alaveteli_pro/info_requests/new.html.erb b/app/views/alaveteli_pro/info_requests/new.html.erb index ebea57d476..a864430da6 100644 --- a/app/views/alaveteli_pro/info_requests/new.html.erb +++ b/app/views/alaveteli_pro/info_requests/new.html.erb @@ -30,7 +30,8 @@
    - <%= render partial: "embargo_info" %> + <%= render partial: "embargo_info", + locals: { embargo: @embargo, tense: :future } %>

    <% end %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/alaveteli_pro/info_requests/preview.html.erb b/app/views/alaveteli_pro/info_requests/preview.html.erb index 52ee178c67..a0ca42d021 100644 --- a/app/views/alaveteli_pro/info_requests/preview.html.erb +++ b/app/views/alaveteli_pro/info_requests/preview.html.erb @@ -8,7 +8,8 @@
    - <%= render partial: "embargo_info" %> + <%= render partial: "embargo_info", + locals: { embargo: @embargo, tense: :future } %>
    @@ -39,4 +40,4 @@

    -
    \ No newline at end of file +
    From fd873303371e8a1ac44be31e5d80c7c75e4c4b4a Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 8 Dec 2016 12:31:39 +0000 Subject: [PATCH 165/311] Add a helper for embargo extension options --- app/helpers/alaveteli_pro/info_requests_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/alaveteli_pro/info_requests_helper.rb b/app/helpers/alaveteli_pro/info_requests_helper.rb index 4040797902..0a6a175357 100644 --- a/app/helpers/alaveteli_pro/info_requests_helper.rb +++ b/app/helpers/alaveteli_pro/info_requests_helper.rb @@ -4,4 +4,8 @@ def publish_at_options options = { _("Publish immediately") => '' } options.merge(Embargo::DURATION_LABELS.invert) end + + def embargo_extension_options + Embargo::DURATION_LABELS.invert + end end From 57af593a6af2aebe4840e4d359d9b53fc35eb1ec Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 8 Dec 2016 10:20:17 +0000 Subject: [PATCH 166/311] Add an embargo extension form to the pro request sidebar --- .../info_requests/_sidebar.html.erb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb index a2b5893547..b48b039891 100644 --- a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb +++ b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb @@ -1 +1,18 @@ -This is the pro sidebar + From c1ef6049ae37d1562ce0071db0a9a4d946119602 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 8 Dec 2016 21:20:15 +0000 Subject: [PATCH 167/311] Add a route for destroying Embargoes --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 72b1f26c81..77de7226ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -602,6 +602,7 @@ resources :info_requests, :only => [:new, :create, :index] do get :preview, on: :new # /info_request/new/preview end + resources :embargoes, :only => [:destroy] resources :embargo_extensions, :only => [:create] end # So that we can show a request using the existing controller from the From 58ca481d38b51de9c0a25868f7b50f2875d908d1 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 8 Dec 2016 21:20:48 +0000 Subject: [PATCH 168/311] Add a controller for embargoes --- .../alaveteli_pro/embargoes_controller.rb | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/controllers/alaveteli_pro/embargoes_controller.rb diff --git a/app/controllers/alaveteli_pro/embargoes_controller.rb b/app/controllers/alaveteli_pro/embargoes_controller.rb new file mode 100644 index 0000000000..c1fc49d918 --- /dev/null +++ b/app/controllers/alaveteli_pro/embargoes_controller.rb @@ -0,0 +1,21 @@ +# -*- encoding : utf-8 -*- +# app/controllers/alaveteli_pro/embargoes_controller.rb +# Controller for embargoes +# +# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ + +class AlaveteliPro::EmbargoesController < AlaveteliPro::BaseController + def destroy + @embargo = Embargo.find(params[:id]) + @info_request = @embargo.info_request + authorize! :update, @embargo + if @embargo.destroy + flash[:notice] = _("Your request is now public!") + else + flash[:error] = _("Sorry, something went wrong publishing your " \ + "request, please try again.") + end + return redirect_to request_url(@info_request) + end +end From c49eae7a59096e112ae9c1a05b6cffabe87051a6 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Thu, 8 Dec 2016 21:21:24 +0000 Subject: [PATCH 169/311] Add a 'publish' button to the pro request sidebar --- app/views/alaveteli_pro/info_requests/_sidebar.html.erb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb index b48b039891..993e5c845f 100644 --- a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb +++ b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb @@ -14,5 +14,9 @@ ">

    <% end %> + <%= button_to "Publish request", + alaveteli_pro_embargo_path(@info_request.embargo), + method: :delete, + confirm: "Are you sure you want to publish this request?" %> <% end %> From d3187db733046af91790073159b5109256015000 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 9 Dec 2016 11:06:19 +0000 Subject: [PATCH 170/311] Show a pro version of the action menu on requests --- .../info_requests/_after_actions.html.erb | 39 ++++++++ app/views/request/show.html.erb | 13 ++- .../_after_actions.html.erb_spec.rb | 94 +++++++++++++++++++ spec/views/request/show.html.erb_spec.rb | 21 +++++ 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 app/views/alaveteli_pro/info_requests/_after_actions.html.erb create mode 100644 spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb diff --git a/app/views/alaveteli_pro/info_requests/_after_actions.html.erb b/app/views/alaveteli_pro/info_requests/_after_actions.html.erb new file mode 100644 index 0000000000..29968ad199 --- /dev/null +++ b/app/views/alaveteli_pro/info_requests/_after_actions.html.erb @@ -0,0 +1,39 @@ +
    +
      +
    • + Actions + +
        +
      • +
          +
        • + <% if @last_response.nil? %> + <%= link_to _("Send a followup"), new_request_followup_path(:request_id => @info_request.id, :anchor => 'followup') %> + <% else %> + <%= link_to _("Write a reply"), new_request_incoming_followup_path(:request_id => @info_request.id, :incoming_message_id => @last_response.id, :anchor => 'followup') %> + <% end %> +
        • + +
        • + <%= link_to _("Update the status of this request"), request_path(@info_request, :update_status => 1) %> +
        • + +
        • + <%= link_to _("Request an internal review"), new_request_followup_path(:request_id => @info_request.id, :internal_review => '1', :anchor => 'followup') %> +
        • + +
        • + <%= link_to _("Download a zip file of all correspondence"), download_entire_request_path(:url_title => @info_request.url_title) %> +
        • + + <% if feature_enabled?(:annotations) && @info_request.comments_allowed? %> +
        • + <%= link_to _('Add an annotation'), new_comment_path(:url_title => @info_request.url_title) %> +
        • + <% end %> +
        +
      • +
      +
    • +
    +
    \ No newline at end of file diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index 12aa3dab97..8b3572a4f3 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -33,8 +33,17 @@

    <% unless @render_to_file %>
    - <%= render :partial => 'after_actions' %> - <%= render :partial => 'track/tracking_links_simple', :locals => { :track_thing => @track_thing, :own_request => @info_request.user && @info_request.user == @user, :location => 'toolbar ' } %> + <% if @pro %> + <%= render :partial => 'alaveteli_pro/info_requests/after_actions' %> + <% else %> + <%= render :partial => 'after_actions' %> + <%= render :partial => 'track/tracking_links_simple', + :locals => { + :track_thing => @track_thing, + :own_request => @info_request.user && \ + @info_request.user == @user, + :location => 'toolbar ' } %> + <% end %>
    <% end %> diff --git a/spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb b/spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb new file mode 100644 index 0000000000..f106af001e --- /dev/null +++ b/spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb @@ -0,0 +1,94 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.join('..', '..', '..', '..', 'spec_helper'), __FILE__) + +describe 'when displaying actions that can be taken with regard to a pro request' do + let(:info_request) { FactoryGirl.create(:info_request) } + let(:pro_user) { info_request.pro_user } + let(:admin_user) { FactoryGirl.create("admin_user") } + + before do + assign :info_request, info_request + end + + def render_view + render :partial => 'alaveteli_pro/info_requests/after_actions' + end + + it 'should display a link to update the status of the request' do + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).to have_css('a', :text => 'Update the status of this request') + end + end + + it 'should display a link to request a review' do + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).to have_css('a', :text => 'Request an internal review') + end + end + + it 'should display the link to download the entire request' do + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).to have_css('a', :text => 'Download a zip file of all correspondence') + end + end + + it "should display a link to annotate the request" do + with_feature_enabled(:annotations) do + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).to have_css('a', :text => 'Add an annotation (to help the requester or others)') + end + end + end + + it "should not display a link to annotate the request if comments are disabled on it" do + with_feature_enabled(:annotations) do + info_request.comments_allowed = false + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).not_to have_css('a', :text => 'Add an annotation') + end + end + end + + it "should not display a link to annotate the request if comments are disabled globally" do + with_feature_disabled(:annotations) do + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).not_to have_css('a', :text => 'Add an annotation') + end + end + end + + context "when there is a response" do + let(:info_request) { FactoryGirl.create(:info_request_with_incoming) } + + before do + assign :info_request, info_request + assign :last_response, info_request.get_last_public_response + end + + it "should display a link to reply" do + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).to have_css('a', :text => 'Write a reply') + end + end + end + + context "when there is no response" do + before do + assign :last_response, nil + end + + it "should display a link to send a follow up" do + render_view + expect(response.body).to have_css('.action-menu__menu__submenu') do |div| + expect(div).to have_css('a', :text => 'Send a follow up') + end + end + end +end diff --git a/spec/views/request/show.html.erb_spec.rb b/spec/views/request/show.html.erb_spec.rb index a5f05db113..78aa4a1c5d 100644 --- a/spec/views/request/show.html.erb_spec.rb +++ b/spec/views/request/show.html.erb_spec.rb @@ -213,6 +213,7 @@ def request_page assign :info_request, request_with_attachment assign :info_request_events, request_with_attachment.info_request_events assign :status, request_with_attachment.calculate_status + assign :track_thing, TrackThing.create_track_for_request(request_with_attachment) render expect(rendered).to have_css(".attachment .attachment__name") do |s| expect(s).to contain /interesting.pdf/m @@ -230,6 +231,7 @@ def request_page assign :info_request, request_with_attachment assign :info_request_events, request_with_attachment.info_request_events assign :status, request_with_attachment.calculate_status + assign :track_thing, TrackThing.create_track_for_request(request_with_attachment) # For cancancan allow(view).to receive(:current_user).and_return(nil) allow(controller).to receive(:current_user).and_return(nil) @@ -242,4 +244,23 @@ def request_page end end end + + describe "follow links" do + context "when the request is a normal request" do + it "should show a follow link" do + request_page + expect(rendered).to have_css("a", text: "Follow") + end + end + + context "when the request is a pro request" do + it "should not show a follow link" do + assign :pro, true + request_page + expect(rendered).not_to have_css("a", text: "Follow") + end + end + end + + describe "action" end From 115e07e41be7d16471eabcfff3496226d754f759 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Wed, 7 Dec 2016 12:20:33 +0000 Subject: [PATCH 171/311] Add integration spec for pro request viewing --- spec/integration/alaveteli_dsl.rb | 4 + .../alaveteli_pro/view_request_spec.rb | 124 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 spec/integration/alaveteli_pro/view_request_spec.rb diff --git a/spec/integration/alaveteli_dsl.rb b/spec/integration/alaveteli_dsl.rb index 64ae8b0856..443f2b805f 100644 --- a/spec/integration/alaveteli_dsl.rb +++ b/spec/integration/alaveteli_dsl.rb @@ -5,6 +5,10 @@ def browse_request(url_title) visit "/request/#{url_title}" end + def browse_pro_request(url_title) + visit "/alaveteli_pro/info_requests/#{url_title}" + end + def create_request visit select_authority_path within(:css, '#search_form') do diff --git a/spec/integration/alaveteli_pro/view_request_spec.rb b/spec/integration/alaveteli_pro/view_request_spec.rb new file mode 100644 index 0000000000..788e8dea30 --- /dev/null +++ b/spec/integration/alaveteli_pro/view_request_spec.rb @@ -0,0 +1,124 @@ +# -*- encoding : utf-8 -*- +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/../alaveteli_dsl') + +describe "viewing requests in alaveteli_pro" do + let(:pro_user) { FactoryGirl.create(:pro_user) } + let(:info_request) { FactoryGirl.create(:info_request, user: pro_user) } + let!(:embargo) { FactoryGirl.create(:embargo, info_request: info_request) } + let!(:pro_user_session) { login(pro_user) } + + it "allows us to view a pro request" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + expect(page).to have_content(info_request.title) + end + end + + it "allows the user to extend an embargo" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + old_publish_at = embargo.publish_at + expect(page).to have_content("This request is embargoed until " \ + "#{old_publish_at.to_date}") + select "3 Months", from: "Extend embargo:" + click_button("Extend") + expected_publish_at = old_publish_at + 3.months + expect(embargo.reload.publish_at).to eq(expected_publish_at) + expect(page).to have_content("This request is embargoed until " \ + "#{expected_publish_at.to_date} ") + end + end + + it "allows the user to publish a request" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + old_publish_at = embargo.publish_at + expect(page).to have_content("This request is embargoed until " \ + "#{old_publish_at.to_date}") + click_button("Publish request") + expect(info_request.reload.embargo).to be nil + expect(page).to have_content("Your request is now public!") + end + end + + xit "allows the user to add an annotation" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + click_link("Add an annotation") + # TODO - currently fails because the request is embargoed, and so the + # comment controller returns a 404 for it. + expect(page).to have_content "Add an annotation to #{info_request.title}" + fill_in("comment_body", with: "Testing annotations") + click_button("Preview your annotation") + click_button("Post annotation") + expect(page).to have_content("#{pro_user.name} left an annotation") + expect(page).to have_content("Testing annotations") + end + end + + xit "allows the user to send a follow up" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + click_link("Send follow up") + expect(page).to have_content "Send a public follow up message to the " \ + "main FOI contact at " \ + "#{info_request.public_body.name}" + fill_in("outgoing_message_body", with: "Testing follow ups") + choose("Anything else, such as clarifying, prompting, thanking") + click_button("Preview your message") + click_button("Send message") + expect(page).to have_content("Testing follow ups") + end + end + + xit "allows the user to write a reply" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + click_link("Write a reply") + expect(page).to have_content "Send a public reply to the " \ + "main FOI contact at " \ + "#{info_request.public_body.name}" + fill_in("outgoing_message_body", with: "Testing replies") + choose("Anything else, such as clarifying, prompting, thanking") + click_button("Preview your message") + click_button("Send message") + expect(page).to have_content("Testing replies") + end + end + + xit "allows the user to download the entire request" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + click_link("Download a zip file of all correspondence") + page.response_headers["Content-Disposition"].should == "attachment" + end + end + + xit "allows the user to request an internal review" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + click_link("Request an internal review") + expect(page).to have_content "Request an internal review from " \ + "the main FOI contact at " \ + "#{info_request.public_body.name}" + fill_in("outgoing_message_body", with: "Testing internal reviews") + click_button("Preview your message") + click_button("Send message") + expect(page).to have_content("Testing internal reviews") + end + end + + it "allows the user to update the request status" do + using_pro_session(pro_user_session) do + browse_pro_request(info_request.url_title) + expect(page).to have_content("Update status") + expect(find_field("Waiting response")).to be_checked + choose("Partially successful") + click_button("Update") + expect(info_request.reload.described_state).to eq ("partially_successful") + expect(page).to have_content("Your request has been updated!") + expect(find_field("Partially successful")).to be_checked + end + end +end From cfb4324940802b629dca889f1d29ea8437d7bf53 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 9 Dec 2016 12:36:47 +0000 Subject: [PATCH 172/311] Redirect pros to the pro request page for their requests --- app/controllers/request_controller.rb | 31 ++++++++++--- spec/controllers/request_controller_spec.rb | 50 ++++++++++++++++++--- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index e8284e58f3..150b457d1d 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -13,6 +13,8 @@ class RequestController < ApplicationController before_filter :check_batch_requests_and_user_allowed, :only => [ :select_authorities, :new_batch ] before_filter :set_render_recaptcha, :only => [ :new ] before_filter :redirect_numeric_id_to_url_title, :only => [:show] + before_filter :redirect_embargoed_requests_for_pro_users, :only => [:show] + before_filter :redirect_public_requests_from_pro_context, :only => [:show] MAX_RESULTS = 500 PER_PAGE = 25 @@ -1079,12 +1081,31 @@ def redirect_numeric_id_to_url_title # Look up by old style numeric identifiers if params[:url_title].match(/^[0-9]+$/) @info_request = InfoRequest.find(params[:url_title].to_i) - if params[:pro] == "1" + redirect_to request_url(@info_request, :format => params[:format]) + end + end + + def redirect_embargoed_requests_for_pro_users + # Pro users should see their embargoed requests in the pro page, so that + # if other site functions send them to a request page, they end up back in + # the pro area + if feature_enabled?(:alaveteli_pro) && params[:pro] != "1" && \ + current_user && current_user.pro? + @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + if @info_request.is_actual_owning_user?(current_user) && @info_request.embargo redirect_to show_alaveteli_pro_request_url( - :url_title => @info_request.url_title, - :format => params[:format]) - else - redirect_to request_url(@info_request, :format => params[:format]) + :url_title => @info_request.url_title) + end + end + end + + def redirect_public_requests_from_pro_context + # Requests which aren't embargoed should always go to the normal request + # page, so that pro's seem them in that context after they publish them + if feature_enabled?(:alaveteli_pro) && params[:pro] == "1" + @info_request = InfoRequest.find_by_url_title!(params[:url_title]) + unless @info_request.embargo + redirect_to request_url(@info_request) end end end diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index c46c0cffd7..3afaf9894e 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -157,13 +157,49 @@ expect(response).to redirect_to(:action => 'show', :url_title => info_requests(:naughty_chicken_request).url_title) end - context "when showing a request in the pro context" do - it "should redirect from a numeric URL to pretty one in the pro namespace" do - get :show, :url_title => info_requests(:naughty_chicken_request).id.to_s, - :pro => "1" - expect(response). - to redirect_to(show_alaveteli_pro_request_path( - :url_title => info_requests(:naughty_chicken_request).url_title)) + + describe "redirecting pro users to the pro context" do + let(:pro_user) { FactoryGirl.create(:pro_user) } + + context "when showing pros their own requests" do + context "when the request is embargoed" do + let(:info_request) do + FactoryGirl.create(:embargoed_request, user: pro_user) + end + + it "should always redirect to the pro version of the page" do + with_feature_enabled(:alaveteli_pro) do + session[:user_id] = pro_user + get :show, url_title: info_request.url_title + expect(response).to redirect_to show_alaveteli_pro_request_path( + url_title: info_request.url_title) + end + end + end + + context "when the request is not embargoed" do + let(:info_request) do + FactoryGirl.create(:info_request, user: pro_user) + end + + it "should not redirect to the pro version of the page" do + with_feature_enabled(:alaveteli_pro) do + session[:user_id] = pro_user + get :show, url_title: info_request.url_title + expect(response).to be_success + end + end + end + end + + context "when showing pros a someone else's request" do + it "should not redirect to the pro version of the page" do + with_feature_enabled(:alaveteli_pro) do + session[:user_id] = pro_user + get :show, url_title: 'why_do_you_have_such_a_fancy_dog' + expect(response).to be_success + end + end end end From 8050e7d17a82457157f76250711399d461594cf2 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 9 Dec 2016 16:09:04 +0000 Subject: [PATCH 173/311] Add route for updating pro info requests --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 77de7226ec..0ef5e4e4e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -599,7 +599,7 @@ namespace :alaveteli_pro do match '/' => 'dashboard#index', :as => 'dashboard', :via => :get resources :draft_info_requests, :only => [:create, :update] - resources :info_requests, :only => [:new, :create, :index] do + resources :info_requests, :only => [:new, :create, :update, :index] do get :preview, on: :new # /info_request/new/preview end resources :embargoes, :only => [:destroy] From 5e8ee71af7be3f869a9c4959480e98f1dbbdeadb Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 9 Dec 2016 16:09:27 +0000 Subject: [PATCH 174/311] Add a controller action for updating pro info requests' status --- .../alaveteli_pro/info_requests_controller.rb | 14 +++++++++++++- .../info_requests_controller_spec.rb | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index 08da36ab21..5c55feadfb 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -50,6 +50,16 @@ def create end end + def update + @info_request = InfoRequest.find(params[:id]) + authorize! :update_request_state, @info_request + new_status = info_request_params[:described_state] + @info_request.set_described_state(new_status, current_user) + flash[:notice] = _("Your request has been updated!") + redirect_to show_alaveteli_pro_request_path( + url_title: @info_request.url_title) + end + private def show_errors @@ -107,9 +117,11 @@ def send_initial_message(outgoing_message) end end - def request_filter_params params.require(:request_filter).permit(:filter, :order, :search) end + def info_request_params + params.require(:info_request).permit(:described_state) + end end diff --git a/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb index 272c2e240f..45f0d2cf1e 100644 --- a/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb +++ b/spec/controllers/alaveteli_pro/info_requests_controller_spec.rb @@ -65,4 +65,20 @@ end end end + + describe "#update" do + let(:pro_user) { FactoryGirl.create(:pro_user) } + let(:other_pro_user) { FactoryGirl.create(:pro_user) } + let(:info_request) { FactoryGirl.create(:info_request, user: pro_user) } + + context "when the user is not allowed to update the request" do + it "raises a CanCan::AccessDenied error" do + session[:user_id] = other_pro_user.id + expect do + put :update, id: info_request.id, + info_request: { described_state: "successful" } + end.to raise_error(CanCan::AccessDenied) + end + end + end end From 2011735da23bc3f924fbf2a0c3e450d34ad79ba8 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Fri, 9 Dec 2016 16:15:12 +0000 Subject: [PATCH 175/311] Remove status update from action menu and add it to pro sidebar --- .../info_requests/_after_actions.html.erb | 4 ---- .../info_requests/_sidebar.html.erb | 16 ++++++++++++++++ .../_after_actions.html.erb_spec.rb | 7 ------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/views/alaveteli_pro/info_requests/_after_actions.html.erb b/app/views/alaveteli_pro/info_requests/_after_actions.html.erb index 29968ad199..17187956fa 100644 --- a/app/views/alaveteli_pro/info_requests/_after_actions.html.erb +++ b/app/views/alaveteli_pro/info_requests/_after_actions.html.erb @@ -14,10 +14,6 @@ <% end %> -
  • - <%= link_to _("Update the status of this request"), request_path(@info_request, :update_status => 1) %> -
  • -
  • <%= link_to _("Request an internal review"), new_request_followup_path(:request_id => @info_request.id, :internal_review => '1', :anchor => 'followup') %>
  • diff --git a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb index 993e5c845f..24046b31c1 100644 --- a/app/views/alaveteli_pro/info_requests/_sidebar.html.erb +++ b/app/views/alaveteli_pro/info_requests/_sidebar.html.erb @@ -19,4 +19,20 @@ method: :delete, confirm: "Are you sure you want to publish this request?" %> <% end %> + <%= form_for([:alaveteli_pro, @info_request]) do |f| %> +

    Update status

    +

    + <% InfoRequest.enumerate_states.each do |s| %> + <%# TODO - nice labels for these %> + <%= f.label :described_state, value: s do %> + <%# TODO - There's probably a good order for these %> + <%# TODO - not all states will be applicable %> + <%# TODO - refactor into a helper %> + <%= f.radio_button :described_state, s, checked: @info_request.described_state == s %> + <%= _(s.humanize) %> + <% end %> + <% end %> +

    + "> + <% end %> diff --git a/spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb b/spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb index f106af001e..61e5401f7e 100644 --- a/spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb +++ b/spec/views/alaveteli_pro/info_requests/_after_actions.html.erb_spec.rb @@ -14,13 +14,6 @@ def render_view render :partial => 'alaveteli_pro/info_requests/after_actions' end - it 'should display a link to update the status of the request' do - render_view - expect(response.body).to have_css('.action-menu__menu__submenu') do |div| - expect(div).to have_css('a', :text => 'Update the status of this request') - end - end - it 'should display a link to request a review' do render_view expect(response.body).to have_css('.action-menu__menu__submenu') do |div| From 1acc83c6e39517023972840e07f8cdd76f369176 Mon Sep 17 00:00:00 2001 From: Steven Day Date: Mon, 12 Dec 2016 14:01:52 +0000 Subject: [PATCH 176/311] Redirect to the pro version of showing a request after creating one --- app/controllers/alaveteli_pro/info_requests_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index 5c55feadfb..11a8c362bc 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -44,7 +44,8 @@ def create @embargo.save if @embargo.present? send_initial_message(@outgoing_message) destroy_draft - redirect_to show_request_path(url_title: @info_request.url_title) + redirect_to show_alaveteli_pro_request_path( + url_title: @info_request.url_title) else show_errors end From 8c62ac1413453a55efb98f16e97f4d20ed46a25b Mon Sep 17 00:00:00 2001 From: Steven Day Date: Tue, 13 Dec 2016 16:05:55 +0000 Subject: [PATCH 177/311] Combine request status helpers into one --- app/helpers/info_request_helper.rb | 89 ++-- app/views/request/_request_status.html.erb | 14 - app/views/request/show.html.erb | 9 +- spec/helpers/info_request_helper_spec.rb | 453 +++++++++------------ 4 files changed, 247 insertions(+), 318 deletions(-) delete mode 100644 app/views/request/_request_status.html.erb diff --git a/app/helpers/info_request_helper.rb b/app/helpers/info_request_helper.rb index f77d7215e5..3b0155708f 100644 --- a/app/helpers/info_request_helper.rb +++ b/app/helpers/info_request_helper.rb @@ -4,39 +4,43 @@ module InfoRequestHelper include DateTimeHelper include LinkToHelper - def status_text(status, opts = {}) + def status_text(info_request, opts = {}) + if info_request.awaiting_description + status = "awaiting_description" + else + status = info_request.calculate_status + end method = "status_text_#{ status }" if respond_to?(method, true) - send(method, opts) + send(method, info_request, opts) else - custom_state_description(status) + custom_state_description(info_request, opts) end end - def awaiting_description_text(info_request, - new_responses_count, - opts = {}) + private + + def status_text_awaiting_description(info_request, opts = {}) + new_responses_count = opts.fetch(:new_responses_count, 0) is_owning_user = opts.fetch(:is_owning_user, false) render_to_file = opts.fetch(:render_to_file, false) old_unclassified = opts.fetch(:old_unclassified, false) if is_owning_user && !info_request.is_external? && !render_to_file - return awaiting_description_text_owner_please_answer(new_responses_count) + return status_text_awaiting_description_owner_please_answer( + new_responses_count) else if old_unclassified - return awaiting_description_text_old_unclassified(new_responses_count) + return status_text_awaiting_description_old_unclassified( + new_responses_count) else - return awaiting_description_text_other(info_request, - new_responses_count) + return status_text_awaiting_description_other(info_request, + new_responses_count) end end end - private - - def status_text_waiting_response(opts = {}) - info_request = opts.fetch(:info_request) - + def status_text_waiting_response(info_request, opts = {}) str = _('Currently waiting for a response from ' \ '{{public_body_link}}, they must respond promptly and', :public_body_link => public_body_link(info_request.public_body)) @@ -53,9 +57,7 @@ def status_text_waiting_response(opts = {}) str += ")." end - def status_text_waiting_response_overdue(opts = {}) - info_request = opts.fetch(:info_request) - + def status_text_waiting_response_overdue(info_request, opts = {}) str = _('Response to this request is delayed.') str += ' ' str += _('By law, {{public_body_link}} should normally have responded ' \ @@ -73,9 +75,7 @@ def status_text_waiting_response_overdue(opts = {}) str += ")" end - def status_text_waiting_response_very_overdue(opts = {}) - info_request = opts.fetch(:info_request) - + def status_text_waiting_response_very_overdue(info_request, opts = {}) str = _('Response to this request is long overdue.') str += ' ' str += _('By law, under all circumstances, {{public_body_link}} should ' \ @@ -100,31 +100,26 @@ def status_text_waiting_response_very_overdue(opts = {}) str end - def status_text_not_held(opts = {}) - info_request = opts.fetch(:info_request) - + def status_text_not_held(info_request, opts = {}) _('{{authority_name}} did not have the information ' \ 'requested.', :authority_name => public_body_link(info_request.public_body)) end - def status_text_rejected(opts = {}) - info_request = opts.fetch(:info_request) - + def status_text_rejected(info_request, opts = {}) _('The request was refused by {{authority_name}}.', :authority_name => public_body_link(info_request.public_body)) end - def status_text_successful(opts = {}) + def status_text_successful(info_request, opts = {}) _('The request was successful.') end - def status_text_partially_successful(opts = {}) + def status_text_partially_successful(info_request, opts = {}) _('The request was partially successful.') end - def status_text_waiting_clarification(opts = {}) - info_request = opts.fetch(:info_request) + def status_text_waiting_clarification(info_request, opts = {}) is_owning_user = opts.fetch(:is_owning_user) str = ''.html_safe @@ -158,58 +153,56 @@ def status_text_waiting_clarification(opts = {}) str end - def status_text_gone_postal(opts = {}) + def status_text_gone_postal(info_request, opts = {}) _('The authority would like to / has responded by ' \ 'post to this request.') end - def status_text_internal_review(opts = {}) - info_request = opts.fetch(:info_request) - + def status_text_internal_review(info_request, opts = {}) _('Waiting for an internal review by ' \ '{{public_body_link}} of their handling of this request.', :public_body_link => public_body_link(info_request.public_body)) end - def status_text_error_message(opts = {}) + def status_text_error_message(info_request, opts = {}) _('There was a delivery error or similar, which ' \ 'needs fixing by the {{site_name}} team.', :site_name => site_name) end - def status_text_requires_admin(opts = {}) + def status_text_requires_admin(info_request, opts = {}) _('This request has had an unusual response, and requires ' \ 'attention from the {{site_name}} team.', :site_name => site_name) end - def status_text_user_withdrawn(opts = {}) + def status_text_user_withdrawn(info_request, opts = {}) _('This request has been withdrawn by the person ' \ 'who made it. There may be an explanation in the correspondence below.') end - def status_text_attention_requested(opts = {}) + def status_text_attention_requested(info_request, opts = {}) _('This request has been reported as needing ' \ 'administrator attention (perhaps because it is vexatious, or a ' \ 'request for personal information)') end - def status_text_vexatious(opts = {}) + def status_text_vexatious(info_request, opts = {}) _('This request has been hidden from the site, ' \ 'because an administrator considers it vexatious') end - def status_text_not_foi(opts = {}) + def status_text_not_foi(info_request, opts = {}) _('This request has been hidden from the site, ' \ 'because an administrator considers it not to be an FOI request') end - def custom_state_description(status) + def custom_state_description(info_request, opts = {}) render :partial => 'general/custom_state_descriptions', - :locals => { :status => status } + :locals => { :status => info_request.calculate_status } end - def awaiting_description_text_owner_please_answer(new_responses_count) + def status_text_awaiting_description_owner_please_answer(new_responses_count) n_('Please answer the question above so we know ' \ 'whether the recent response contains useful information.', 'Please answer the question above so we know ' \ @@ -217,13 +210,13 @@ def awaiting_description_text_owner_please_answer(new_responses_count) new_responses_count) end - def awaiting_description_text_unknown + def status_text_awaiting_description_unknown _('This request has an unknown status.') end - def awaiting_description_text_old_unclassified(new_responses_count) + def status_text_awaiting_description_old_unclassified(new_responses_count) str = ''.html_safe - str += awaiting_description_text_unknown + str += status_text_awaiting_description_unknown str += ' ' str += n_('We\'re waiting for someone to read a recent response and ' \ 'update the status accordingly. Perhaps ' \ @@ -234,7 +227,7 @@ def awaiting_description_text_old_unclassified(new_responses_count) new_responses_count) end - def awaiting_description_text_other(info_request, new_responses_count) + def status_text_awaiting_description_other(info_request, new_responses_count) n_('We\'re waiting for {{user}} to read a recent response and update ' \ 'the status.', 'We\'re waiting for {{user}} to read recent responses and update ' \ diff --git a/app/views/request/_request_status.html.erb b/app/views/request/_request_status.html.erb deleted file mode 100644 index c7091f1a84..0000000000 --- a/app/views/request/_request_status.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<%# Partial view that displays a line of text displaying the current status - of a request %> -<% if @info_request.awaiting_description %> - <%= awaiting_description_text(@info_request, - @new_responses_count, - :is_owning_user => @is_owning_user, - :render_to_file => @render_to_file, - :old_unclassified => @old_unclassified) %> -<% else %> - <%= status_text(@info_request.calculate_status, - :info_request => @info_request, - :is_owning_user => @is_owning_user, - :redirect_to => request.fullpath) %> -<% end %> diff --git a/app/views/request/show.html.erb b/app/views/request/show.html.erb index 8b3572a4f3..ba97386f54 100644 --- a/app/views/request/show.html.erb +++ b/app/views/request/show.html.erb @@ -50,7 +50,12 @@

    - <%= render :partial => 'request_status' %> + <%= status_text(@info_request, + :new_responses_count => @new_responses_count, + :is_owning_user => @is_owning_user, + :render_to_file => @render_to_file, + :old_unclassified => @old_unclassified, + :redirect_to => request.fullpath) %>

    @@ -88,4 +93,4 @@ <%= content_for :javascript do %> <%= javascript_include_tag 'request-attachments.js' %> -<% end %> \ No newline at end of file +<% end %> diff --git a/spec/helpers/info_request_helper_spec.rb b/spec/helpers/info_request_helper_spec.rb index 9aa7da4869..53fd31f4fc 100644 --- a/spec/helpers/info_request_helper_spec.rb +++ b/spec/helpers/info_request_helper_spec.rb @@ -6,25 +6,30 @@ include InfoRequestHelper describe '#status_text' do + let(:info_request) { FactoryGirl.create(:info_request) } - it 'requires a status argument' do + it 'requires an info_request argument' do expect { status_text }.to raise_error(ArgumentError) end - it 'delegates the status argument for a valid status' do - expect(self).to receive(:send).with('status_text_successful', {}) - status_text('successful') + it 'delegates the info_request argument for a valid status' do + allow(info_request).to receive(:calculate_status).and_return('successful') + expect(self).to receive(:send).with('status_text_successful', info_request, {}) + status_text(info_request) end it 'delegates the options for a valid status' do - opts = { :info_request => double } - expect(self).to receive(:send).with('status_text_successful', opts) - status_text('successful', opts) + allow(info_request).to receive(:calculate_status).and_return('successful') + opts = {is_owning_user: false} + expect(self).to receive(:send).with('status_text_successful', info_request, opts) + status_text(info_request, opts) end it 'delegates to the custom partial for an unknown status' do - expect(self).to receive(:custom_state_description).with('unknown') - status_text('unknown') + allow(info_request).to receive(:calculate_status).and_return('unknown') + opts = {is_owning_user: false} + expect(self).to receive(:custom_state_description).with(info_request, opts) + status_text(info_request, opts) end context 'waiting_response' do @@ -32,12 +37,11 @@ it 'returns a description' do time_travel_to(Date.parse('2014-12-31')) - body = FactoryGirl.create(:public_body) + body = info_request.public_body body_link = %Q(#{ body.name }) - info_request = - mock_model(InfoRequest, :public_body => body, - :date_response_required_by => Time.now) + allow(info_request).to receive(:calculate_status).and_return("waiting_response") + allow(info_request).to receive(:date_response_required_by).and_return(Time.now) response_date = '