Merge pull request #3392 from Jacek-202/issue-3183

Add search form on admin moderated content
This commit is contained in:
Javi Martín
2022-08-23 15:42:17 +02:00
committed by GitHub
35 changed files with 376 additions and 41 deletions

View File

@@ -1,4 +1,5 @@
<%= form_tag(url, options) do |f| %>
<%= hidden_current_filter_tag %>
<%= text_field_tag :search, search_terms.to_s, placeholder: label, "aria-label": label %>
<%= content %>
<%= submit_tag t("admin.shared.search.search") %>

View File

@@ -20,4 +20,14 @@ class Admin::SearchComponent < ApplicationComponent
def options
{ method: :get, role: "search" }.merge(form_options)
end
def hidden_current_filter_tag
hidden_field_tag(:filter, current_filter) if current_filter
end
def current_filter
if helpers.respond_to?(:current_filter)
helpers.current_filter
end
end
end

View File

@@ -1,6 +1,6 @@
<%= header %>
<%= render SDGManagement::Relations::SearchComponent.new(label: search_label, current_filter: current_filter) %>
<%= render SDGManagement::Relations::SearchComponent.new(label: search_label) %>
<%= render "shared/filter_subnav", i18n_namespace: "sdg_management.relations.index" %>
<table>

View File

@@ -1,7 +1,5 @@
class SDGManagement::Relations::IndexComponent < ApplicationComponent
include Header
delegate :valid_filters, :current_filter, to: :helpers
attr_reader :records
def initialize(records)

View File

@@ -5,5 +5,4 @@
<%= component.select_tag :target_code, target_options,
include_blank: target_blank_option,
"aria-label": target_label %>
<%= component.hidden_field_tag :filter, current_filter %>
<% end %>

View File

@@ -1,10 +1,9 @@
class SDGManagement::Relations::SearchComponent < ApplicationComponent
include SDG::OptionsForSelect
attr_reader :label, :current_filter
attr_reader :label
def initialize(label:, current_filter: nil)
def initialize(label:)
@label = label
@current_filter = current_filter
end
private

View File

@@ -1,16 +1,13 @@
class Admin::HiddenBudgetInvestmentsController < Admin::BaseController
include FeatureFlags
has_filters %w[all with_confirmed_hide without_confirmed_hide], only: :index
include Admin::HiddenContent
feature_flag :budgets
before_action :load_investment, only: [:confirm_hide, :restore]
def index
@investments = Budget::Investment.only_hidden.send(@current_filter)
.order(hidden_at: :desc)
.page(params[:page])
@investments = hidden_content(Budget::Investment.all)
end
def confirm_hide

View File

@@ -1,11 +1,10 @@
class Admin::HiddenCommentsController < Admin::BaseController
has_filters %w[without_confirmed_hide all with_confirmed_hide]
include Admin::HiddenContent
before_action :load_comment, only: [:confirm_hide, :restore]
def index
@comments = Comment.not_valuations.only_hidden.with_visible_author
.send(@current_filter).order(hidden_at: :desc).page(params[:page])
@comments = hidden_content(Comment.not_valuations).with_visible_author
end
def confirm_hide

View File

@@ -1,14 +1,13 @@
class Admin::HiddenDebatesController < Admin::BaseController
include FeatureFlags
include Admin::HiddenContent
feature_flag :debates
has_filters %w[without_confirmed_hide all with_confirmed_hide], only: :index
before_action :load_debate, only: [:confirm_hide, :restore]
def index
@debates = Debate.only_hidden.send(@current_filter).order(hidden_at: :desc).page(params[:page])
@debates = hidden_content(Debate.all)
end
def confirm_hide

View File

@@ -1,13 +1,10 @@
class Admin::HiddenProposalNotificationsController < Admin::BaseController
has_filters %w[without_confirmed_hide all with_confirmed_hide], only: :index
include Admin::HiddenContent
before_action :load_proposal, only: [:confirm_hide, :restore]
def index
@proposal_notifications = ProposalNotification.only_hidden
.send(@current_filter)
.order(hidden_at: :desc)
.page(params[:page])
@proposal_notifications = hidden_content(ProposalNotification.all)
end
def confirm_hide

View File

@@ -1,15 +1,13 @@
class Admin::HiddenProposalsController < Admin::BaseController
include FeatureFlags
has_filters %w[without_confirmed_hide all with_confirmed_hide], only: :index
include Admin::HiddenContent
feature_flag :proposals
before_action :load_proposal, only: [:confirm_hide, :restore]
def index
@proposals = Proposal.only_hidden.send(@current_filter).order(hidden_at: :desc)
.page(params[:page])
@proposals = hidden_content(Proposal.all)
end
def confirm_hide

View File

@@ -1,10 +1,10 @@
class Admin::HiddenUsersController < Admin::BaseController
has_filters %w[without_confirmed_hide all with_confirmed_hide], only: :index
include Admin::HiddenContent
before_action :load_user, only: [:confirm_hide, :restore]
def index
@users = User.only_hidden.send(@current_filter).order(hidden_at: :desc).page(params[:page])
@users = hidden_content(User.all)
end
def show

View File

@@ -0,0 +1,15 @@
module Admin::HiddenContent
extend ActiveSupport::Concern
include Search
included do
has_filters %w[without_confirmed_hide all with_confirmed_hide], only: :index
end
def hidden_content(relation)
records = relation.only_hidden
records = records.search(@search_terms) if @search_terms.present?
records.send(@current_filter).order(hidden_at: :desc).page(params[:page])
end
end

View File

@@ -3,6 +3,7 @@ class Comment < ApplicationRecord
include HasPublicAuthor
include Graphqlable
include Notifiable
include Searchable
COMMENTABLE_TYPES = %w[Debate Proposal Budget::Investment Poll Topic
Legislation::Question Legislation::Annotation
@@ -131,6 +132,17 @@ class Comment < ApplicationRecord
cached_votes_up - cached_votes_down
end
def searchable_values
{
body => "A",
commentable&.title => "B"
}
end
def self.search(terms)
pg_search(terms)
end
private
def validate_body_length

View File

@@ -6,7 +6,7 @@ module SearchCache
end
def calculate_tsvector
self.class.where(id: id).update_all("tsv = (#{searchable_values_sql})")
self.class.with_hidden.where(id: id).update_all("tsv = (#{searchable_values_sql})")
end
private

View File

@@ -1,6 +1,7 @@
class ProposalNotification < ApplicationRecord
include Graphqlable
include Notifiable
include Searchable
belongs_to :author, class_name: "User"
belongs_to :proposal
@@ -55,6 +56,17 @@ class ProposalNotification < ApplicationRecord
update(moderated: false)
end
def searchable_values
{
title => "A",
body => "B"
}
end
def self.search(terms)
pg_search(terms)
end
private
def set_author

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_budget_investments.index.title") %></h2>
<%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.budget_investments")) %>
<p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_budget_investments.index" %>

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_comments.index.title") %></h2>
<%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.comments")) %>
<p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_comments.index" %>

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_debates.index.title") %></h2>
<%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.debates")) %>
<p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_debates.index" %>

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_proposal_notifications.index.title") %></h2>
<%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.proposal_notifications")) %>
<p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_proposal_notifications.index" %>

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_proposals.index.title") %></h2>
<%= render Admin::SearchComponent.new(label: t("admin.shared.search.label.proposals")) %>
<p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_proposals.index" %>

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_users.index.title") %></h2>
<%= render "admin/shared/user_search", url: admin_hidden_users_path %>
<p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_users.index" %>

View File

@@ -1346,6 +1346,7 @@ en:
label:
booths: "Search booth by name or location"
budget_investments: "Search investments by title, description or heading"
comments: "Search comments"
debates: "Search debates by title or description"
legislation_processes: "Search processes by title or description"
legislation_proposals: "Search proposals by title or description"
@@ -1355,6 +1356,7 @@ en:
poll_questions: "Search poll questions"
polls: "Search polls by name or description"
proposals: "Search proposals by title, code, description or question"
proposal_notifications: "Search notifications by title or description"
users: "Search user by name or email"
search: "Search"
search_results: "Search results"

View File

@@ -1345,6 +1345,7 @@ es:
label:
booths: "Buscar urna por nombre"
budget_investments: "Buscar proyectos por título, descripción o partida"
comments: "Buscar comentarios"
debates: "Buscar debates por título o descripción"
legislation_processes: "Buscar procesos por título o descripción"
legislation_proposals: "Buscar propuestas por título o descripción"
@@ -1354,6 +1355,7 @@ es:
poll_questions: "Buscar preguntas"
polls: "Buscar votaciones por nombre o descripción"
proposals: "Buscar propuestas por título, código, descripción o pregunta"
proposal_notifications: "Buscar notificaciones por título o descripción"
users: "Buscar usuario por nombre o email"
search: "Buscar"
search_results: "Resultados de la búsqueda"

View File

@@ -0,0 +1,9 @@
class AddTsvectorToCommentsAndProposalNotifications < ActiveRecord::Migration[5.2]
def change
add_column :comments, :tsv, :tsvector
add_index :comments, :tsv, using: "gin"
add_column :proposal_notifications, :tsv, :tsvector
add_index :proposal_notifications, :tsv, using: "gin"
end
end

View File

@@ -448,6 +448,7 @@ ActiveRecord::Schema.define(version: 2021_11_03_112944) do
t.string "ancestry"
t.integer "confidence_score", default: 0, null: false
t.boolean "valuation", default: false
t.tsvector "tsv"
t.index ["ancestry"], name: "index_comments_on_ancestry"
t.index ["cached_votes_down"], name: "index_comments_on_cached_votes_down"
t.index ["cached_votes_total"], name: "index_comments_on_cached_votes_total"
@@ -455,6 +456,7 @@ ActiveRecord::Schema.define(version: 2021_11_03_112944) do
t.index ["commentable_id", "commentable_type"], name: "index_comments_on_commentable_id_and_commentable_type"
t.index ["confidence_score"], name: "index_comments_on_confidence_score"
t.index ["hidden_at"], name: "index_comments_on_hidden_at"
t.index ["tsv"], name: "index_comments_on_tsv", using: :gin
t.index ["user_id"], name: "index_comments_on_user_id"
t.index ["valuation"], name: "index_comments_on_valuation"
end
@@ -1268,6 +1270,8 @@ ActiveRecord::Schema.define(version: 2021_11_03_112944) do
t.datetime "hidden_at"
t.datetime "ignored_at"
t.datetime "confirmed_hide_at"
t.tsvector "tsv"
t.index ["tsv"], name: "index_proposal_notifications_on_tsv", using: :gin
end
create_table "proposal_translations", id: :serial, force: :cascade do |t|

View File

@@ -2,10 +2,15 @@ namespace :consul do
desc "Runs tasks needed to upgrade to the latest version"
task execute_release_tasks: ["settings:rename_setting_keys",
"settings:add_new_settings",
"execute_release_1.5.0_tasks"]
"execute_release_1.6.0_tasks"]
desc "Runs tasks needed to upgrade from 1.4.0 to 1.5.0"
task "execute_release_1.5.0_tasks": [
"active_storage:remove_paperclip_compatibility_in_existing_attachments"
]
desc "Runs tasks needed to upgrade from 1.5.0 to 1.6.0"
task "execute_release_1.6.0_tasks": [
"db:calculate_tsv"
]
end

View File

@@ -5,4 +5,15 @@ namespace :db do
I18n.enforce_available_locales = false
load(Rails.root.join("db", "dev_seeds.rb"))
end
desc "Calculates the TSV column for all comments and proposal notifications"
task calculate_tsv: :environment do
logger = ApplicationLogger.new
logger.info "Calculating tsvector for comments"
Comment.with_hidden.find_each(&:calculate_tsvector)
logger.info "Calculating tsvector for proposal notifications"
ProposalNotification.with_hidden.find_each(&:calculate_tsvector)
end
end

View File

@@ -0,0 +1,31 @@
require "rails_helper"
describe Admin::SearchComponent do
describe "#hidden_current_filter_tag" do
context "controller responds to current_filter", controller: ApplicationController do
it "is present when the controller has a current filter" do
allow(controller).to receive(:current_filter).and_return("all")
render_inline Admin::SearchComponent.new(label: "Search")
expect(page).to have_field "filter", type: :hidden, with: "all"
end
it "is not present when the controller has no current filter" do
render_inline Admin::SearchComponent.new(label: "Search")
expect(page).not_to have_field "filter", type: :hidden
expect(page).not_to have_field "filter"
end
end
context "controller does not respond to current_filter", controller: ActionController::Base do
it "is not present" do
render_inline Admin::SearchComponent.new(label: "Search")
expect(page).not_to have_field "filter", type: :hidden
expect(page).not_to have_field "filter"
end
end
end
end

39
spec/lib/tasks/db_spec.rb Normal file
View File

@@ -0,0 +1,39 @@
require "rails_helper"
describe "rake db:calculate_tsv" do
before { Rake::Task["db:calculate_tsv"].reenable }
let :run_rake_task do
Rake.application.invoke_task("db:calculate_tsv")
end
it "calculates the tsvector for comments, including hidden ones" do
comment = create(:comment)
hidden = create(:comment, :hidden)
comment.update_column(:tsv, nil)
hidden.update_column(:tsv, nil)
expect(comment.reload.tsv).to be nil
expect(hidden.reload.tsv).to be nil
run_rake_task
expect(comment.reload.tsv).not_to be nil
expect(hidden.reload.tsv).not_to be nil
end
it "calculates the tsvector for proposal notifications, including hidden ones" do
notification = create(:proposal_notification)
hidden = create(:proposal_notification, :hidden)
notification.update_column(:tsv, nil)
hidden.update_column(:tsv, nil)
expect(notification.reload.tsv).to be nil
expect(hidden.reload.tsv).to be nil
run_rake_task
expect(notification.reload.tsv).not_to be nil
expect(hidden.reload.tsv).not_to be nil
end
end

View File

@@ -193,4 +193,25 @@ describe Comment do
expect(Comment.public_for_api).to be_empty
end
end
describe ".search" do
it "searches by body" do
comment = create(:comment, body: "I agree")
expect(Comment.search("agree")).to eq([comment])
end
it "searches by commentable title" do
proposal = create(:proposal, title: "More wood!")
comment = create(:comment, body: "I agree", commentable: proposal)
expect(Comment.search("wood")).to eq([comment])
end
it "does not return non-matching records" do
create(:comment, body: "I agree")
expect(Comment.search("disagree")).to be_empty
end
end
end

View File

@@ -42,6 +42,26 @@ describe ProposalNotification do
end
end
describe ".search" do
it "searches by title" do
notification = create(:proposal_notification, title: "Check this!", body: "It's awesome!")
expect(ProposalNotification.search("Check")).to eq([notification])
end
it "searches by body" do
notification = create(:proposal_notification, title: "Check this!", body: "It's awesome!")
expect(ProposalNotification.search("awesome")).to eq([notification])
end
it "does not return non-matching records" do
create(:proposal_notification, title: "Check this!", body: "It's awesome!")
expect(ProposalNotification.search("terrible")).to be_empty
end
end
describe "minimum interval between notifications" do
before do
Setting[:proposal_notification_minimum_interval_in_days] = 3

View File

@@ -0,0 +1,27 @@
require "rails_helper"
describe SearchCache do
describe "#calculate_tsvector" do
it "calculates the tsv column of a record" do
debate = create(:debate)
debate.update_column(:tsv, nil)
expect(debate.reload.tsv).to be_nil
debate.calculate_tsvector
expect(debate.reload.tsv).not_to be_nil
end
it "calculates the tsv column of a hidden record" do
debate = create(:debate, :hidden)
debate.update_column(:tsv, nil)
expect(debate.reload.tsv).to be_nil
debate.calculate_tsvector
expect(debate.reload.tsv).not_to be_nil
end
end
end

View File

@@ -31,9 +31,6 @@ describe "Admin hidden budget investments", :admin do
investment = create(:budget_investment, :hidden, heading: heading)
visit admin_hidden_budget_investments_path
click_link "Pending"
expect(page).not_to have_link "Pending"
expect(page).to have_content(investment.title)
click_button "Confirm moderation"
@@ -46,18 +43,18 @@ describe "Admin hidden budget investments", :admin do
scenario "Current filter is properly highlighted" do
visit admin_hidden_budget_investments_path
expect(page).not_to have_link("All")
expect(page).to have_link("Pending")
expect(page).to have_link("Confirmed")
visit admin_hidden_budget_investments_path(filter: "without_confirmed_hide")
expect(page).not_to have_link("Pending")
expect(page).to have_link("All")
expect(page).to have_link("Confirmed")
expect(page).not_to have_link("Pending")
visit admin_hidden_budget_investments_path(filter: "all")
expect(page).to have_link("Pending")
expect(page).not_to have_link("All")
expect(page).to have_link("Confirmed")
visit admin_hidden_budget_investments_path(filter: "with_confirmed_hide")
expect(page).to have_link("All")
expect(page).to have_link("Pending")
expect(page).to have_link("All")
expect(page).not_to have_link("Confirmed")
end

View File

@@ -0,0 +1,125 @@
require "rails_helper"
describe "Hidden content search", :admin do
scenario "finds matching records" do
create(:budget_investment, :hidden, title: "New football field")
create(:budget_investment, :hidden, title: "New basketball field")
create(:budget_investment, :hidden, title: "New sports center")
visit admin_hidden_budget_investments_path
fill_in "search", with: "field"
click_button "Search"
expect(page).not_to have_content "New sports center"
expect(page).to have_content "New football field"
expect(page).to have_content "New basketball field"
end
scenario "returns no results if no records match the term" do
create(:comment, :hidden, body: "I like this feature")
visit admin_hidden_comments_path
fill_in "search", with: "love"
click_button "Search"
expect(page).to have_content "There are no hidden comments"
expect(page).not_to have_content "I like this feature"
expect(page).not_to have_content "I hate this feature"
end
scenario "returns all records when the search term is empty" do
create(:debate, :hidden, title: "Can we make it better?")
create(:debate, :hidden, title: "Can we make it worse?")
visit admin_hidden_debates_path(search: "worse")
expect(page).not_to have_content "Can we make it better?"
expect(page).to have_content "Can we make it worse?"
fill_in "search", with: " "
click_button "Search"
expect(page).to have_content "Can we make it better?"
expect(page).to have_content "Can we make it worse?"
expect(page).not_to have_content "There are no hidden debates"
end
scenario "keeps search parameters after restoring a record" do
create(:proposal_notification, :hidden, title: "Someone is telling you something")
create(:proposal_notification, :hidden, title: "Someone else says whatever")
create(:proposal_notification, :hidden, title: "Nobody is saying anything")
visit admin_hidden_proposal_notifications_path(search: "Someone")
expect(page).to have_content "Someone is telling you something"
expect(page).to have_content "Someone else says whatever"
expect(page).not_to have_content "Nobody is saying anything"
within "tr", text: "Someone is telling you something" do
accept_confirm("Are you sure? Restore") { click_button "Restore" }
end
expect(page).not_to have_content "Someone is telling you something"
expect(page).to have_content "Someone else says whatever"
expect(page).not_to have_content "Nobody is saying anything"
end
scenario "keeps search parameters after confirming moderation" do
create(:proposal, :hidden, title: "Reduce the incoming traffic")
create(:proposal, :hidden, title: "Reduce pollution")
create(:proposal, :hidden, title: "Increment pollution")
visit admin_hidden_proposals_path(search: "Reduce")
expect(page).to have_content "Reduce the incoming traffic"
expect(page).to have_content "Reduce pollution"
expect(page).not_to have_content "Increment pollution"
within("tr", text: "Reduce the incoming traffic") { click_button "Confirm moderation" }
expect(page).not_to have_content "Reduce the incoming traffic"
expect(page).to have_content "Reduce pollution"
expect(page).not_to have_content "Increment pollution"
end
scenario "keeps search parameters while browsing through filters" do
create(:user, :hidden, username: "person1")
create(:user, :hidden, username: "alien1")
create(:user, :hidden, :with_confirmed_hide, username: "person2")
create(:user, :hidden, :with_confirmed_hide, username: "alien2")
visit admin_hidden_users_path(search: "person")
expect(page).to have_content "person1"
expect(page).not_to have_content "person2"
expect(page).not_to have_content "alien1"
expect(page).not_to have_content "alien2"
click_link "Confirmed"
expect(page).not_to have_content "person1"
expect(page).to have_content "person2"
expect(page).not_to have_content "alien1"
expect(page).not_to have_content "alien2"
end
scenario "keeps filter parameters after searching" do
create(:user, :hidden, :with_confirmed_hide, username: "person1")
create(:user, :hidden, :with_confirmed_hide, username: "alien1")
create(:user, :hidden, username: "person2")
create(:user, :hidden, username: "alien2")
visit admin_hidden_users_path(filter: "with_confirmed_hide")
fill_in "search", with: "alien"
click_button "Search"
expect(page).not_to have_content "person1"
expect(page).to have_content "alien1"
expect(page).not_to have_content "person2"
expect(page).not_to have_content "alien2"
expect(page).to have_content "Confirmed"
expect(page).not_to have_link "Confirmed"
end
end