Skip to content

Commit

Permalink
Merge pull request #2264 from alphagov/205_filter-publications
Browse files Browse the repository at this point in the history
Backend filtering support for the new publications page (root controller)
  • Loading branch information
mtaylorgds authored Aug 6, 2024
2 parents fa36150 + 948f2da commit a03c24c
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 7 deletions.
31 changes: 30 additions & 1 deletion app/controllers/root_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,36 @@
class RootController < ApplicationController
layout "design_system"

PERMITTED_FILTER_STATES = %w[
draft
amends_needed
in_review
fact_check
fact_check_received
ready
scheduled_for_publishing
published
archived
].freeze

def index
@presenter = FilteredEditionsPresenter.new
filter_params_hash = filter_params.to_h
states_filter_params = filter_params_hash[:states_filter]
sanitised_states_filter_params = states_filter_params&.select { |fp| PERMITTED_FILTER_STATES.include?(fp) }
assignee_filter = filter_params_hash[:assignee_filter]
format_filter = filter_params_hash[:format_filter]
title_filter = filter_params_hash[:title_filter]
@presenter = FilteredEditionsPresenter.new(
states_filter: sanitised_states_filter_params,
assigned_to_filter: assignee_filter,
format_filter:,
title_filter:,
)
end

private

def filter_params
params.permit(:assignee_filter, :format_filter, :title_filter, states_filter: [])
end
end
11 changes: 10 additions & 1 deletion app/helpers/editions_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,21 @@ def format_conversion_select_options(edition)
possible_target_formats.map { |format_name| [format_name.humanize, format_name] }
end

def format_filter_selection_options
def legacy_format_filter_selection_options
[%w[All edition]] +
Artefact::FORMATS_BY_DEFAULT_OWNING_APP["publisher"].map do |format_name|
displayed_format_name = format_name.humanize
displayed_format_name += " (Retired)" if Artefact::RETIRED_FORMATS.include?(format_name)
[displayed_format_name, format_name]
end
end

def format_filter_selection_options
[%w[All all]] +
Artefact::FORMATS_BY_DEFAULT_OWNING_APP["publisher"].map do |format_name|
displayed_format_name = format_name.humanize
displayed_format_name += " (Retired)" if Artefact::RETIRED_FORMATS.include?(format_name)
[displayed_format_name, format_name]
end
end
end
7 changes: 7 additions & 0 deletions app/models/edition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ class ResurrectionError < RuntimeError
scope :published, -> { where(state: "published") }
scope :draft_in_publishing_api, -> { where(state: { "$in" => PUBLISHING_API_DRAFT_STATES }) }

scope :in_states, ->(states) { where(state: { "$in" => states }) }
scope :title_contains,
lambda { |term|
regex = ::Regexp.new(::Regexp.escape(term), true) # case-insensitive
where({ title: regex })
}

ACTIONS = {
send_fact_check: "Send to Fact check",
resend_fact_check: "Resend fact check email",
Expand Down
54 changes: 53 additions & 1 deletion app/presenters/filtered_editions_presenter.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,59 @@
# frozen_string_literal: true

class FilteredEditionsPresenter
def initialize(states_filter: [], assigned_to_filter: nil, format_filter: nil, title_filter: nil)
@states_filter = states_filter || []
@assigned_to_filter = assigned_to_filter
@format_filter = format_filter
@title_filter = title_filter
end

def available_users
User.enabled.alphabetized
end

def editions
Edition.all
result = editions_by_format
result = apply_states_filter(result)
result = apply_assigned_to_filter(result)
apply_title_filter(result)
end

private

def editions_by_format
return Edition.all unless format_filter && format_filter != "all"

Edition.where(_type: "#{format_filter.camelcase}Edition")
end

def apply_states_filter(editions)
return editions if states_filter.empty?

editions.in_states(states_filter)
end

def apply_assigned_to_filter(editions)
return editions unless assigned_to_filter

if assigned_to_filter == "nobody"
editions = editions.assigned_to(nil)
else
begin
assigned_user = User.find(assigned_to_filter)
editions = editions.assigned_to(assigned_user) if assigned_user
rescue Mongoid::Errors::DocumentNotFound
Rails.logger.warn "An attempt was made to filter by an unknown user ID: '#{assigned_to_filter}'"
end
end
editions
end

def apply_title_filter(editions)
return editions if title_filter.blank?

editions.title_contains(title_filter)
end

attr_reader :states_filter, :assigned_to_filter, :format_filter, :title_filter
end
2 changes: 1 addition & 1 deletion app/views/legacy_root/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

<label for="format_filter" class="add-top-margin nav-header">Format</label>
<%= select_tag("format_filter", options_for_select(
format_filter_selection_options,
legacy_format_filter_selection_options,
params[:format_filter]
), class: 'form-control') %>
<input class="add-top-margin btn btn-default" type="submit" value="Filter publications">
Expand Down
19 changes: 17 additions & 2 deletions app/views/root/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@
<% content_for :title, "Publications" %>

<div class="govuk-grid-column-one-third">
<p>[replace with filter controls]</p>
<%= form_with url: root_path, method: :get do |form| %>
<%= form.label :title_filter, "Title" %>
<%= text_field_tag :title_filter %>
<%= form.label :format_filter, "Format" %>
<%= select_tag :format_filter,
options_for_select(format_filter_selection_options) %>
<%= form.label :assignee_filter, "Assignee" %>
<%= select_tag :assignee_filter,
options_for_select([%w[Nobody nobody]]) <<
options_from_collection_for_select(@presenter.available_users, "id", "name") %>
<%= form.label :states_filter_draft, "Draft" %>
<%= check_box_tag "states_filter[]", "draft", false, { id: "states_filter_draft" } %>
<%= form.label :states_filter_published, "Published" %>
<%= check_box_tag "states_filter[]", "published", false, :id => "states_filter_published" %>
<%= form.submit %>
<% end %>
</div>
<div class="govuk-grid-column-two-thirds">
<p>[replace with publications table]</p>
<h2><%= @presenter.editions.count %> document(s)</h2>
</div>
2 changes: 1 addition & 1 deletion app/views/user_search/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<label for="format_filter" class="add-top-margin nav-header">Format</label>
<%= select_tag("format_filter", options_for_select(
format_filter_selection_options,
legacy_format_filter_selection_options,
params[:format_filter]
), class: 'form-control') %>
<input class="add-top-margin btn btn-default" type="submit" value="Filter publications">
Expand Down
49 changes: 49 additions & 0 deletions test/functional/root_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,54 @@ class RootControllerTest < ActionController::TestCase
assert_response :ok
assert_template "root/index"
end

should "filter publications by state" do
FactoryBot.create(:guide_edition, state: "draft")
FactoryBot.create(:guide_edition, state: "published")

get :index, params: { states_filter: %w[draft] }

assert_response :ok
assert_select "h2", "1 document(s)"
end

should "filter publications by assignee" do
anna = FactoryBot.create(:user, name: "Anna")
FactoryBot.create(:guide_edition)

get :index, params: { assignee_filter: [anna.id] }

assert_response :ok
assert_select "h2", "1 document(s)"
end

should "filter publications by format" do
FactoryBot.create(:guide_edition)
FactoryBot.create(:completed_transaction_edition)

get :index, params: { format_filter: "guide" }

assert_response :ok
assert_select "h2", "1 document(s)"
end

should "filter publications by title text" do
FactoryBot.create(:guide_edition, title: "How to train your dragon")
FactoryBot.create(:guide_edition, title: "What to do in the event of a zombie apocalypse")

get :index, params: { title_filter: "zombie" }

assert_response :ok
assert_select "h2", "1 document(s)"
end

should "ignore unrecognised filter states" do
FilteredEditionsPresenter
.expects(:new)
.with(states_filter: %w[draft], assigned_to_filter: anything, format_filter: anything, title_filter: anything)
.returns(stub(editions: [], available_users: []))

get :index, params: { states_filter: %w[draft not_a_real_state] }
end
end
end
21 changes: 21 additions & 0 deletions test/models/edition_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,27 @@ def draft_second_edition_from(published_edition)
assert_equal [b], Edition.assigned_to(bob).to_a
end

test "should scope publications by state" do
draft_guide = FactoryBot.create(:guide_edition, state: "draft")
FactoryBot.create(:guide_edition, state: "published")

assert_equal [draft_guide], Edition.in_states(%w[draft]).to_a
end

test "should scope publications by partial title match" do
guide = FactoryBot.create(:guide_edition, title: "Hitchhiker's Guide to the Galaxy")
FactoryBot.create(:guide_edition)

assert_equal [guide], Edition.title_contains("Galaxy").to_a
end

test "should scope publications by case-insensitive title match" do
guide = FactoryBot.create(:guide_edition, title: "Hitchhiker's Guide to the Galaxy")
FactoryBot.create(:guide_edition)

assert_equal [guide], Edition.title_contains("Hitchhiker's gUIDE to the Galaxy").to_a
end

test "cannot delete a publication that has been published" do
dummy_answer = template_published_answer
loaded_answer = AnswerEdition.where(slug: "childcare").first
Expand Down
107 changes: 107 additions & 0 deletions test/unit/presenters/filtered_editions_presenter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

require "test_helper"

class FilteredEditionsPresenterTest < ActiveSupport::TestCase
context "#editions" do
should "return all editions when no filters are specified" do
draft_guide = FactoryBot.create(:guide_edition, state: "draft")
published_guide = FactoryBot.create(:guide_edition, state: "published")

filtered_editions = FilteredEditionsPresenter.new.editions

assert_equal(2, filtered_editions.count)
assert_equal(draft_guide, filtered_editions[0])
assert_equal(published_guide, filtered_editions[1])
end

should "filter by state" do
draft_guide = FactoryBot.create(:guide_edition, state: "draft")
FactoryBot.create(:guide_edition, state: "published")

filtered_editions = FilteredEditionsPresenter.new(states_filter: %w[draft]).editions

assert_equal(1, filtered_editions.count)
assert_equal(draft_guide, filtered_editions[0])
end

should "filter by 'assigned to'" do
anna = FactoryBot.create(:user, name: "Anna")
assigned_to_anna = FactoryBot.create(:guide_edition, assigned_to: anna.id)
FactoryBot.create(:guide_edition)

filtered_editions = FilteredEditionsPresenter.new(assigned_to_filter: anna.id).editions.to_a

assert_equal([assigned_to_anna], filtered_editions)
end

should "filter by 'not assigned'" do
anna = FactoryBot.create(:user, name: "Anna")
FactoryBot.create(:guide_edition, assigned_to: anna.id)
not_assigned = FactoryBot.create(:guide_edition)

filtered_editions = FilteredEditionsPresenter.new(assigned_to_filter: "nobody").editions.to_a

assert_equal([not_assigned], filtered_editions)
end

should "ignore invalid 'assigned to'" do
anna = FactoryBot.create(:user, name: "Anna")
FactoryBot.create(:guide_edition, assigned_to: anna.id)
FactoryBot.create(:guide_edition)

filtered_editions =
FilteredEditionsPresenter.new(assigned_to_filter: "not a valid user id").editions

assert_equal(2, filtered_editions.count)
end

should "filter by format" do
guide = FactoryBot.create(:guide_edition)
FactoryBot.create(:completed_transaction_edition)

filtered_editions = FilteredEditionsPresenter.new(format_filter: "guide").editions

assert_equal([guide], filtered_editions)
end

should "return all formats when specified by the format filter" do
FactoryBot.create(:guide_edition)
FactoryBot.create(:completed_transaction_edition)

filtered_editions = FilteredEditionsPresenter.new(format_filter: "all").editions

assert_equal(2, filtered_editions.count)
end

should "filter by a partially-matching title" do
guide_fawkes = FactoryBot.create(:guide_edition, title: "Guide Fawkes")
FactoryBot.create(:guide_edition, title: "Hitchhiker's Guide")

filtered_editions = FilteredEditionsPresenter.new(title_filter: "Fawkes").editions

assert_equal([guide_fawkes], filtered_editions)
end
end

context "#available_users" do
should "return users in alphabetical order" do
bob = FactoryBot.create(:user, name: "Bob")
charlie = FactoryBot.create(:user, name: "Charlie")
anna = FactoryBot.create(:user, name: "Anna")

users = FilteredEditionsPresenter.new.available_users.to_a

assert_equal([anna, bob, charlie], users)
end

should "not include disabled users" do
enabled_user = FactoryBot.create(:user, name: "enabled user")
FactoryBot.create(:user, name: "disabled user", disabled: true)

users = FilteredEditionsPresenter.new.available_users.to_a

assert_equal(users, [enabled_user])
end
end
end

0 comments on commit a03c24c

Please sign in to comment.