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