Add search form for hidden content

Added search for comments and proposal_notifications, added tsv column
for search and rake tasks to update/create tsv vector.
This commit is contained in:
Jacek Skrzypacz
2019-03-20 11:35:41 +01:00
committed by Javi Martín
parent e66b9687a2
commit 2af7e32415
19 changed files with 254 additions and 2 deletions

View File

@@ -1,11 +1,15 @@
module Admin::HiddenContent module Admin::HiddenContent
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Search
included do included do
has_filters %w[without_confirmed_hide all with_confirmed_hide], only: :index has_filters %w[without_confirmed_hide all with_confirmed_hide], only: :index
end end
def hidden_content(relation) def hidden_content(relation)
relation.only_hidden.send(@current_filter).order(hidden_at: :desc).page(params[:page]) 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
end end

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_budget_investments.index.title") %></h2> <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> <p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_budget_investments.index" %> <%= 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> <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> <p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_comments.index" %> <%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_comments.index" %>

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_debates.index.title") %></h2> <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> <p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_debates.index" %> <%= 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> <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> <p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_proposal_notifications.index" %> <%= 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> <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> <p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_proposals.index" %> <%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_proposals.index" %>

View File

@@ -1,4 +1,5 @@
<h2><%= t("admin.hidden_users.index.title") %></h2> <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> <p><%= t("admin.shared.moderated_content") %></p>
<%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_users.index" %> <%= render "shared/filter_subnav", i18n_namespace: "admin.hidden_users.index" %>

View File

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

View File

@@ -1345,6 +1345,7 @@ es:
label: label:
booths: "Buscar urna por nombre" booths: "Buscar urna por nombre"
budget_investments: "Buscar proyectos por título, descripción o partida" budget_investments: "Buscar proyectos por título, descripción o partida"
comments: "Buscar comentarios"
debates: "Buscar debates por título o descripción" debates: "Buscar debates por título o descripción"
legislation_processes: "Buscar procesos 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" legislation_proposals: "Buscar propuestas por título o descripción"
@@ -1354,6 +1355,7 @@ es:
poll_questions: "Buscar preguntas" poll_questions: "Buscar preguntas"
polls: "Buscar votaciones por nombre o descripción" polls: "Buscar votaciones por nombre o descripción"
proposals: "Buscar propuestas por título, código, descripción o pregunta" 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" users: "Buscar usuario por nombre o email"
search: "Buscar" search: "Buscar"
search_results: "Resultados de la búsqueda" 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.string "ancestry"
t.integer "confidence_score", default: 0, null: false t.integer "confidence_score", default: 0, null: false
t.boolean "valuation", default: false t.boolean "valuation", default: false
t.tsvector "tsv"
t.index ["ancestry"], name: "index_comments_on_ancestry" t.index ["ancestry"], name: "index_comments_on_ancestry"
t.index ["cached_votes_down"], name: "index_comments_on_cached_votes_down" t.index ["cached_votes_down"], name: "index_comments_on_cached_votes_down"
t.index ["cached_votes_total"], name: "index_comments_on_cached_votes_total" 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 ["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 ["confidence_score"], name: "index_comments_on_confidence_score"
t.index ["hidden_at"], name: "index_comments_on_hidden_at" 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 ["user_id"], name: "index_comments_on_user_id"
t.index ["valuation"], name: "index_comments_on_valuation" t.index ["valuation"], name: "index_comments_on_valuation"
end end
@@ -1268,6 +1270,8 @@ ActiveRecord::Schema.define(version: 2021_11_03_112944) do
t.datetime "hidden_at" t.datetime "hidden_at"
t.datetime "ignored_at" t.datetime "ignored_at"
t.datetime "confirmed_hide_at" t.datetime "confirmed_hide_at"
t.tsvector "tsv"
t.index ["tsv"], name: "index_proposal_notifications_on_tsv", using: :gin
end end
create_table "proposal_translations", id: :serial, force: :cascade do |t| 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" desc "Runs tasks needed to upgrade to the latest version"
task execute_release_tasks: ["settings:rename_setting_keys", task execute_release_tasks: ["settings:rename_setting_keys",
"settings:add_new_settings", "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" desc "Runs tasks needed to upgrade from 1.4.0 to 1.5.0"
task "execute_release_1.5.0_tasks": [ task "execute_release_1.5.0_tasks": [
"active_storage:remove_paperclip_compatibility_in_existing_attachments" "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 end

View File

@@ -5,4 +5,15 @@ namespace :db do
I18n.enforce_available_locales = false I18n.enforce_available_locales = false
load(Rails.root.join("db", "dev_seeds.rb")) load(Rails.root.join("db", "dev_seeds.rb"))
end 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 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 expect(Comment.public_for_api).to be_empty
end end
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 end

View File

@@ -42,6 +42,26 @@ describe ProposalNotification do
end end
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 describe "minimum interval between notifications" do
before do before do
Setting[:proposal_notification_minimum_interval_in_days] = 3 Setting[:proposal_notification_minimum_interval_in_days] = 3

View File

@@ -0,0 +1,105 @@
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
end