diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index ea449d955..2021abc18 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -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
diff --git a/app/helpers/editions_helper.rb b/app/helpers/editions_helper.rb
index 9fbc69fbe..314561349 100644
--- a/app/helpers/editions_helper.rb
+++ b/app/helpers/editions_helper.rb
@@ -24,7 +24,7 @@ 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
@@ -32,4 +32,13 @@ def format_filter_selection_options
[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
diff --git a/app/models/edition.rb b/app/models/edition.rb
index f58dfd2c6..49e05fc91 100644
--- a/app/models/edition.rb
+++ b/app/models/edition.rb
@@ -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",
diff --git a/app/presenters/filtered_editions_presenter.rb b/app/presenters/filtered_editions_presenter.rb
index 9f97a6b1d..21540c217 100644
--- a/app/presenters/filtered_editions_presenter.rb
+++ b/app/presenters/filtered_editions_presenter.rb
@@ -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
diff --git a/app/views/legacy_root/index.html.erb b/app/views/legacy_root/index.html.erb
index 163a6b53d..a0b8b84b3 100644
--- a/app/views/legacy_root/index.html.erb
+++ b/app/views/legacy_root/index.html.erb
@@ -40,7 +40,7 @@
<%= select_tag("format_filter", options_for_select(
- format_filter_selection_options,
+ legacy_format_filter_selection_options,
params[:format_filter]
), class: 'form-control') %>
diff --git a/app/views/root/index.html.erb b/app/views/root/index.html.erb
index e16679748..1bc5c7c77 100644
--- a/app/views/root/index.html.erb
+++ b/app/views/root/index.html.erb
@@ -2,8 +2,23 @@
<% content_for :title, "Publications" %>
-
[replace with filter controls]
+ <%= 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 %>
-
[replace with publications table]
+
<%= @presenter.editions.count %> document(s)
diff --git a/app/views/user_search/index.html.erb b/app/views/user_search/index.html.erb
index 99a1917a5..7b9a3e1f0 100644
--- a/app/views/user_search/index.html.erb
+++ b/app/views/user_search/index.html.erb
@@ -17,7 +17,7 @@
<%= select_tag("format_filter", options_for_select(
- format_filter_selection_options,
+ legacy_format_filter_selection_options,
params[:format_filter]
), class: 'form-control') %>
diff --git a/test/functional/root_controller_test.rb b/test/functional/root_controller_test.rb
index aab090f67..6657980dc 100644
--- a/test/functional/root_controller_test.rb
+++ b/test/functional/root_controller_test.rb
@@ -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
diff --git a/test/models/edition_test.rb b/test/models/edition_test.rb
index 8ff5cfac6..0c0502568 100644
--- a/test/models/edition_test.rb
+++ b/test/models/edition_test.rb
@@ -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
diff --git a/test/unit/presenters/filtered_editions_presenter_test.rb b/test/unit/presenters/filtered_editions_presenter_test.rb
new file mode 100644
index 000000000..28cfdef27
--- /dev/null
+++ b/test/unit/presenters/filtered_editions_presenter_test.rb
@@ -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