Add advanced search options [SCI-1158]

This commit is contained in:
Oleksii Kriuchykhin 2017-05-05 16:41:23 +02:00
parent dbd9da916b
commit 0ee7e11620
21 changed files with 425 additions and 332 deletions

View file

@ -0,0 +1,12 @@
$(document.body).ready(function() {
$('#search_whole_word').click(function() {
if ($(this).prop('checked') === true) {
$('#search_whole_phrase').prop('checked', false);
}
});
$('#search_whole_phrase').click(function() {
if ($(this).prop('checked') === true) {
$('#search_whole_word').prop('checked', false);
}
});
});

View file

@ -2,9 +2,7 @@ class SearchController < ApplicationController
before_filter :load_vars, only: :index
def index
if not @search_query
redirect_to new_search_path
end
redirect_to new_search_path unless @search_query
count_search_results
@ -41,47 +39,60 @@ class SearchController < ApplicationController
private
def load_vars
@search_query = params[:q] || ''
query = params[:q].strip || ''
@search_category = params[:category] || ''
@search_category = @search_category.to_sym
@search_page = params[:page].to_i || 1
@display_query = @search_query
@search_case = params[:match_case] == 'true'
@search_whole_word = params[:whole_word] == 'true'
@search_whole_phrase = params[:whole_phrase] == 'true'
@display_query = query
if @search_query.length < Constants::NAME_MIN_LENGTH
flash[:error] = t 'general.query.length_too_short',
min_length: Constants::NAME_MIN_LENGTH
return redirect_to :back
end
# splits the search query to validate all entries
@splited_query = @search_query.split
if @splited_query.first.length < Constants::NAME_MIN_LENGTH
flash[:error] = t 'general.query.length_too_short',
min_length: Constants::NAME_MIN_LENGTH
redirect_to :back
elsif @splited_query.first.length > Constants::TEXT_MAX_LENGTH
flash[:error] = t 'general.query.length_too_long',
max_length: Constants::TEXT_MAX_LENGTH
redirect_to :back
elsif @splited_query.length > 1
@search_query = ''
@splited_query.each_with_index do |w, i|
if w.length >= Constants::NAME_MIN_LENGTH
@search_query += "#{@splited_query[i]} "
end
if @search_whole_phrase || query.count(' ').zero?
if query.length < Constants::NAME_MIN_LENGTH
flash[:error] = t('general.query.length_too_short',
min_length: Constants::NAME_MIN_LENGTH)
redirect_to :back
elsif query.length > Constants::TEXT_MAX_LENGTH
flash[:error] = t('general.query.length_too_long',
max_length: Constants::TEXT_MAX_LENGTH)
redirect_to :back
else
@search_query = query
end
else
@search_query = @splited_query.join(' ')
# splits the search query to validate all entries
splited_query = query.split
@search_query = ''
splited_query.each_with_index do |w, i|
if w.length >= Constants::NAME_MIN_LENGTH &&
w.length <= Constants::TEXT_MAX_LENGTH
@search_query += "#{splited_query[i]} "
end
end
if @search_query.empty?
flash[:error] = t('general.query.wrong_query',
min_length: Constants::NAME_MIN_LENGTH,
max_length: Constants::TEXT_MAX_LENGTH)
redirect_to :back
else
@search_query.strip!
end
end
@search_page = 1 if @search_page < 1
end
protected
def search_by_name(model)
model.search(current_user, true, @search_query, @search_page)
model.search(current_user,
true,
@search_query,
@search_page,
nil,
match_case: @search_case,
whole_word: @search_whole_word,
whole_phrase: @search_whole_phrase)
end
def count_by_name(model)
@ -122,16 +133,14 @@ class SearchController < ApplicationController
def search_projects
@project_results = []
if @project_search_count > 0
@project_results = search_by_name Project
end
@project_results = search_by_name(Project) if @project_search_count > 0
@search_count = @project_search_count
end
def search_experiments
@experiment_results = []
if @experiment_search_count > 0
@experiment_results = search_by_name Experiment
@experiment_results = search_by_name(Experiment)
end
@search_count = @experiment_search_count
end
@ -139,96 +148,76 @@ class SearchController < ApplicationController
def search_workflows
@workflow_results = []
if @workflow_search_count > 0
@workflow_results = search_by_name MyModuleGroup
@workflow_results = search_by_name(MyModuleGroup)
end
@search_count = @workflow_search_count
end
def search_modules
@module_results = []
if @module_search_count > 0
@module_results = search_by_name MyModule
end
@module_results = search_by_name(MyModule) if @module_search_count > 0
@search_count = @module_search_count
end
def search_results
@result_results = []
if @result_search_count > 0
@result_results = search_by_name Result
end
@result_results = search_by_name(Result) if @result_search_count > 0
@search_count = @result_search_count
end
def search_tags
@tag_results = []
if @tag_search_count > 0
@tag_results = search_by_name Tag
end
@tag_results = search_by_name(Tag) if @tag_search_count > 0
@search_count = @tag_search_count
end
def search_reports
@report_results = []
if @report_search_count > 0
@report_results = search_by_name Report
end
@report_results = search_by_name(Report) if @report_search_count > 0
@search_count = @report_search_count
end
def search_protocols
@protocol_results = []
if @protocol_search_count > 0
@protocol_results = search_by_name Protocol
end
@protocol_results = search_by_name(Protocol) if @protocol_search_count > 0
@search_count = @protocol_search_count
end
def search_steps
@step_results = []
if @step_search_count > 0
@step_results = search_by_name Step
end
@step_results = search_by_name(Step) if @step_search_count > 0
@search_count = @step_search_count
end
def search_checklists
@checklist_results = []
if @checklist_search_count > 0
@checklist_results = search_by_name Checklist
@checklist_results = search_by_name(Checklist)
end
@search_count = @checklist_search_count
end
def search_samples
@sample_results = []
if @sample_search_count > 0
@sample_results = search_by_name Sample
end
@sample_results = search_by_name(Sample) if @sample_search_count > 0
@search_count = @sample_search_count
end
def search_assets
@asset_results = []
if @asset_search_count > 0
@asset_results = search_by_name Asset
end
@asset_results = search_by_name(Asset) if @asset_search_count > 0
@search_count = @asset_search_count
end
def search_tables
@table_results = []
if @table_search_count > 0
@table_results = search_by_name Table
end
@table_results = search_by_name(Table) if @table_search_count > 0
@search_count = @table_search_count
end
def search_comments
@comment_results = []
if @comment_search_count > 0
@comment_results = search_by_name Comment
end
@comment_results = search_by_name(Comment) if @comment_search_count > 0
@search_count = @comment_search_count
end
end

View file

@ -91,7 +91,9 @@ class Asset < ActiveRecord::Base
user,
include_archived,
query = nil,
page = 1
page = 1,
_current_team = nil,
options = {}
)
step_ids =
Step
@ -107,42 +109,67 @@ class Asset < ActiveRecord::Base
.distinct
.pluck('result_assets.id')
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
else
a_query = query
end
# Trim whitespace and replace it with OR character. Make prefixed
# wildcard search term and escape special characters.
# For example, search term 'demo project' is transformed to
# 'demo:*|project:*' which makes word inclusive search with postfix
# wildcard.
s_query = query.gsub(/[!()&|:]/, " ")
.strip
.split(/\s+/)
.map {|t| t + ":*" }
.join("|")
.gsub('\'', '"')
ids = Asset
ids =
Asset
.select(:id)
.distinct
.joins("LEFT OUTER JOIN step_assets ON step_assets.asset_id = assets.id")
.joins("LEFT OUTER JOIN result_assets ON result_assets.asset_id = assets.id")
.joins("LEFT JOIN asset_text_data ON assets.id = asset_text_data.asset_id")
.where("(step_assets.id IN (?) OR result_assets.id IN (?))", step_ids, result_ids)
.where(
"(assets.file_file_name ILIKE ANY (array[?]) " +
.joins('LEFT OUTER JOIN step_assets ON step_assets.asset_id = assets.id')
.joins('LEFT OUTER JOIN result_assets ON ' \
'result_assets.asset_id = assets.id')
.joins('LEFT JOIN asset_text_data ON ' \
'assets.id = asset_text_data.asset_id')
.where('(step_assets.id IN (?) OR result_assets.id IN (?))',
step_ids, result_ids)
a_query = s_query = ''
if options[:whole_word].to_s == 'true' ||
options[:whole_phrase].to_s == 'true'
like = options[:match_case].to_s == 'true' ? '~' : '~*'
s_query = query.gsub(/[!()&|:]/, ' ')
.strip
.split(/\s+/)
.map { |t| t + ':*' }
if options[:whole_word].to_s == 'true'
a_query = query.split
.map { |a| Regexp.escape(a) }
.join('|')
s_query = s_query.join('|')
else
a_query = Regexp.escape(query)
s_query = s_query.join('&')
end
a_query = '\\y(' + a_query + ')\\y'
s_query = s_query.tr('\'', '"')
ids = ids.where(
"(trim_html_tags(assets.file_file_name) #{like} ? " \
"OR asset_text_data.data_vector @@ to_tsquery(?))",
a_query,
s_query
)
else
like = options[:match_case].to_s == 'true' ? 'LIKE' : 'ILIKE'
a_query = query.split.map { |a| "%#{sanitize_sql_like(a)}%" }
# Trim whitespace and replace it with OR character. Make prefixed
# wildcard search term and escape special characters.
# For example, search term 'demo project' is transformed to
# 'demo:*|project:*' which makes word inclusive search with postfix
# wildcard.
s_query = query.gsub(/[!()&|:]/, ' ')
.strip
.split(/\s+/)
.map { |t| t + ':*' }
.join('|')
.tr('\'', '"')
ids = ids.where(
"(trim_html_tags(assets.file_file_name) #{like} ANY (array[?]) " \
"OR asset_text_data.data_vector @@ to_tsquery(?))",
a_query,
s_query
)
end
# Show all results if needed
if page != Constants::SEARCH_NO_LIMIT
@ -152,11 +179,12 @@ class Asset < ActiveRecord::Base
end
Asset
.joins("LEFT JOIN asset_text_data ON assets.id = asset_text_data.asset_id")
.select("assets.*")
.select("ts_headline(data, to_tsquery('" + s_query + "'),
'StartSel=<mark>, StopSel=</mark>') headline")
.where("assets.id IN (?)", ids)
.joins('LEFT JOIN asset_text_data ON ' \
' assets.id = asset_text_data.asset_id')
.select('assets.*')
.select("ts_headline(data, to_tsquery('" + s_query +
"'), 'StartSel=<mark>, StopSel=</mark>') headline")
.where('assets.id IN (?)', ids)
end
def is_image?

View file

@ -22,27 +22,25 @@ class Checklist < ActiveRecord::Base
reject_if: :all_blank,
allow_destroy: true
def self.search(user, include_archived, query = nil, page = 1)
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options= {})
step_ids =
Step
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
else
a_query = query
end
new_query = Checklist
.distinct
.where("checklists.step_id IN (?)", step_ids)
.joins("LEFT JOIN checklist_items ON checklists.id = checklist_items.checklist_id")
.where_attributes_like(["checklists.name", "checklist_items.text"], a_query)
new_query =
Checklist
.distinct
.where('checklists.step_id IN (?)', step_ids)
.joins('LEFT JOIN checklist_items ON ' \
'checklists.id = checklist_items.checklist_id')
.where_attributes_like(['checklists.name', 'checklist_items.text'],
query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -15,7 +15,9 @@ class Comment < ActiveRecord::Base
user,
include_archived,
query = nil,
page = 1
page = 1,
_current_team = nil,
options = {}
)
project_ids =
Project
@ -34,16 +36,6 @@ class Comment < ActiveRecord::Base
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = query.strip
.gsub('_', '\\_')
.gsub('%', '\\%')
.split(/\s+/)
.map { |t| '%' + t + '%' }
else
a_query = query
end
new_query =
Comment.distinct
.joins(:user)
@ -57,7 +49,8 @@ class Comment < ActiveRecord::Base
step_ids, 'StepComment',
result_ids, 'ResultComment'
)
.where_attributes_like(['message', 'users.full_name'], a_query)
.where_attributes_like(['message', 'users.full_name'],
query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -2,36 +2,67 @@ module SearchableModel
extend ActiveSupport::Concern
included do
# Helper function for relations that
# adds OR ILIKE where clause for all specified attributes
# for the given search query
scope :where_attributes_like, ->(attributes, query) do
scope :where_attributes_like, lambda { |attributes, query, options = {}|
return unless query
attrs = []
if attributes.blank? or query.blank?
if attributes.blank?
# Do nothing in this case
elsif attributes.is_a? Symbol
attrs = [attributes.to_s]
elsif attributes.is_a? String
attrs = [attributes]
elsif attributes.is_a? Array
attrs = attributes.collect { |a| a.to_s }
attrs = attributes.collect(&:to_s)
else
raise ArgumentError, ":attributes must be an array, symbol or string"
raise ArgumentError, ':attributes must be an array, symbol or string'
end
if query.is_a? Array
if options[:whole_word].to_s == 'true' ||
options[:whole_phrase].to_s == 'true'
unless attrs.empty?
like = options[:match_case].to_s == 'true' ? '~' : '~*'
if options[:whole_word].to_s == 'true'
a_query = query.split
.map { |a| Regexp.escape(a) }
.join('|')
else
a_query = Regexp.escape(query)
end
a_query = '\\y(' + a_query + ')\\y'
where_str =
(attrs.map.with_index do |a, i|
"(trim_html_tags(#{a})) ILIKE ANY (array[ :t#{i}]) OR "
"(trim_html_tags(#{a})) #{like} :t#{i} OR "
end
).join[0..-5]
vals = (attrs.map.with_index do |_, i|
["t#{i}".to_sym, query]
end
).to_h
vals = (
attrs.map.with_index do |_, i|
["t#{i}".to_sym, a_query]
end
).to_h
return where(where_str, vals)
end
end
like = options[:match_case].to_s == 'true' ? 'LIKE' : 'ILIKE'
if query.count(' ') > 0
unless attrs.empty?
a_query = query.split.map { |a| "%#{sanitize_sql_like(a)}%" }
where_str =
(attrs.map.with_index do |a, i|
"(trim_html_tags(#{a})) #{like} ANY (array[:t#{i}]) OR "
end
).join[0..-5]
vals = (
attrs.map.with_index do |_, i|
["t#{i}".to_sym, a_query]
end
).to_h
return where(where_str, vals)
end
@ -39,15 +70,18 @@ module SearchableModel
unless attrs.empty?
where_str =
(attrs.map.with_index do |a, i|
"(trim_html_tags(#{a})) ILIKE :t#{i} OR "
"(trim_html_tags(#{a})) #{like} :t#{i} OR "
end
).join[0..-5]
vals = (attrs.map.with_index { |_, i| ["t#{i}".to_sym, query.to_s] })
.to_h
vals = (
attrs.map.with_index do |_, i|
["t#{i}".to_sym, "%#{sanitize_sql_like(query.to_s)}%"]
end
).to_h
return where(where_str, vals)
end
end
end
}
end
end

View file

@ -39,19 +39,14 @@ class Experiment < ActiveRecord::Base
include_archived,
query = nil,
page = 1,
current_team = nil
current_team = nil,
options = {}
)
project_ids =
Project
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
if current_team
projects_ids =
Project
@ -65,19 +60,19 @@ class Experiment < ActiveRecord::Base
new_query =
Experiment
.where('experiments.project_id IN (?)', projects_ids)
.where_attributes_like([:name], a_query)
.where_attributes_like([:name, :description], query, options)
return include_archived ? new_query : new_query.is_archived(false)
elsif include_archived
new_query =
Experiment
.where(project: project_ids)
.where_attributes_like([:name, :description], a_query)
.where_attributes_like([:name, :description], query, options)
else
new_query =
Experiment
.is_archived(false)
.where(project: project_ids)
.where_attributes_like([:name, :description], a_query)
.where_attributes_like([:name, :description], query, options)
end
# Show all results if needed

View file

@ -47,19 +47,14 @@ class MyModule < ActiveRecord::Base
include_archived,
query = nil,
page = 1,
current_team = nil
current_team = nil,
options = {}
)
exp_ids =
Experiment
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
if current_team
experiments_ids = Experiment
.search(user,
@ -71,7 +66,7 @@ class MyModule < ActiveRecord::Base
new_query = MyModule
.distinct
.where('my_modules.experiment_id IN (?)', experiments_ids)
.where_attributes_like([:name, :description], a_query)
.where_attributes_like([:name, :description], query, options)
if include_archived
return new_query
@ -82,13 +77,13 @@ class MyModule < ActiveRecord::Base
new_query = MyModule
.distinct
.where('my_modules.experiment_id IN (?)', exp_ids)
.where_attributes_like([:name, :description], a_query)
.where_attributes_like([:name, :description], query, options)
else
new_query = MyModule
.distinct
.where('my_modules.experiment_id IN (?)', exp_ids)
.where('my_modules.archived = ?', false)
.where_attributes_like([:name, :description], a_query)
.where_attributes_like([:name, :description], query, options)
end
# Show all results if needed

View file

@ -11,27 +11,21 @@ class MyModuleGroup < ActiveRecord::Base
belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User'
has_many :my_modules, inverse_of: :my_module_group, dependent: :nullify
def self.search(user, include_archived, query = nil, page = 1)
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
exp_ids =
Experiment
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
else
a_query = query
end
new_query = MyModuleGroup
.distinct
.where("my_module_groups.experiment_id IN (?)", exp_ids)
.where_attributes_like("my_module_groups.name", a_query)
.distinct
.where('my_module_groups.experiment_id IN (?)', exp_ids)
.where_attributes_like('my_module_groups.name', query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -31,15 +31,10 @@ class Project < ActiveRecord::Base
include_archived,
query = nil,
page = 1,
current_team = nil
current_team = nil,
options = {}
)
if query
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
if current_team
new_query =
Project
@ -53,7 +48,7 @@ class Project < ActiveRecord::Base
user.id
)
end
new_query = new_query.where_attributes_like(:name, a_query)
new_query = new_query.where_attributes_like(:name, query, options)
if include_archived
return new_query
@ -75,7 +70,7 @@ class Project < ActiveRecord::Base
'user_projects.user_id = ?',
user.id
)
.where_attributes_like('projects.name', a_query)
.where_attributes_like('projects.name', query, options)
else
new_query =
@ -86,7 +81,7 @@ class Project < ActiveRecord::Base
'user_projects.user_id = ?',
user.id
)
.where_attributes_like('projects.name', a_query)
.where_attributes_like('projects.name', query, options)
.where('projects.archived = ?', false)
end
end

View file

@ -106,7 +106,12 @@ class Protocol < ActiveRecord::Base
has_many :protocol_keywords, through: :protocol_protocol_keywords
has_many :steps, inverse_of: :protocol, dependent: :destroy
def self.search(user, include_archived, query = nil, page = 1)
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
team_ids = Team.joins(:user_teams)
.where('user_teams.user_id = ?', user.id)
.distinct
@ -157,16 +162,6 @@ class Protocol < ActiveRecord::Base
)
end
if query
a_query = query.strip
.gsub('_', '\\_')
.gsub('%', '\\%')
.split(/\s+/)
.map { |t| '%' + t + '%' }
else
a_query = query
end
new_query = new_query
.distinct
.joins('LEFT JOIN protocol_protocol_keywords ON ' \
@ -181,7 +176,7 @@ class Protocol < ActiveRecord::Base
'protocols.authors',
'protocol_keywords.name'
],
a_query
query, options
)
# Show all results if needed

View file

@ -22,7 +22,9 @@ class Report < ActiveRecord::Base
user,
include_archived,
query = nil,
page = 1
page = 1,
_current_team = nil,
options = {}
)
project_ids =
@ -30,28 +32,14 @@ class Report < ActiveRecord::Base
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
else
a_query = query
end
new_query = Report
new_query =
Report
.distinct
.joins("LEFT OUTER JOIN users ON users.id = reports.user_id OR users.id = reports.last_modified_by_id")
.where("reports.project_id IN (?)", project_ids)
.where("reports.user_id = (?)", user.id)
.where_attributes_like(
[
:name,
:description
],
a_query
)
.joins('LEFT OUTER JOIN users ON users.id = reports.user_id ' \
'OR users.id = reports.last_modified_by_id')
.where('reports.project_id IN (?)', project_ids)
.where('reports.user_id = (?)', user.id)
.where_attributes_like([:name, :description], query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -29,30 +29,27 @@ class Result < ActiveRecord::Base
accepts_nested_attributes_for :asset
accepts_nested_attributes_for :table
def self.search(user, include_archived, query = nil, page = 1)
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
module_ids =
MyModule
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
else
a_query = query
end
new_query = Result
new_query =
Result
.distinct
.joins("LEFT JOIN result_texts ON results.id = result_texts.result_id")
.where("results.my_module_id IN (?)", module_ids)
.where_attributes_like(["results.name", "result_texts.text"], a_query)
.joins('LEFT JOIN result_texts ON results.id = result_texts.result_id')
.where('results.my_module_id IN (?)', module_ids)
.where_attributes_like(['results.name', 'result_texts.text'],
query, options)
unless include_archived
new_query = new_query.where("results.archived = ?", false)
new_query = new_query.where('results.archived = ?', false)
end
# Show all results if needed

View file

@ -19,56 +19,51 @@ class Sample < ActiveRecord::Base
def self.search(
user,
include_archived,
_include_archived,
query = nil,
page = 1,
current_team = nil
current_team = nil,
options = {}
)
team_ids = Team.joins(:user_teams)
.where('user_teams.user_id = ?', user.id)
.distinct
.pluck(:id)
if query
a_query = '%' + query.strip.gsub('_', '\\_').gsub('%', '\\%') + '%'
else
a_query = query
end
if current_team
new_query = Sample
.distinct
.where('samples.team_id = ?', current_team.id)
.where_attributes_like(['samples.name'], a_query)
.where_attributes_like(['samples.name'], query, options)
return new_query
else
user_ids = User
.joins(:user_teams)
.where('user_teams.team_id IN (?)', team_ids)
.where_attributes_like(['users.full_name'], a_query)
.where_attributes_like(['users.full_name'], query, options)
.pluck(:id)
sample_ids = Sample
.joins(:user)
.where('team_id IN (?)', team_ids)
.where_attributes_like(['name'], a_query)
.where_attributes_like(['name'], query, options)
.pluck(:id)
sample_type_ids = SampleType
.where('team_id IN (?)', team_ids)
.where_attributes_like(['name'], a_query)
.where_attributes_like(['name'], query, options)
.pluck(:id)
sample_group_ids = SampleGroup
.where('team_id IN (?)', team_ids)
.where_attributes_like(['name'], a_query)
.where_attributes_like(['name'], query, options)
.pluck(:id)
sample_custom_fields = SampleCustomField
.joins(:sample)
.where('samples.team_id IN (?)', team_ids)
.where_attributes_like(['value'], a_query)
.where_attributes_like(['value'], query, options)
.pluck(:id)
new_query = Sample
.distinct

View file

@ -41,26 +41,21 @@ class Step < ActiveRecord::Base
after_destroy :cascade_after_destroy
before_save :set_last_modified_by
def self.search(user, include_archived, query = nil, page = 1)
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
protocol_ids =
Protocol
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
else
a_query = query
end
new_query = Step
.distinct
.where("steps.protocol_id IN (?)", protocol_ids)
.where_attributes_like([:name, :description], a_query)
.distinct
.where('steps.protocol_id IN (?)', protocol_ids)
.where_attributes_like([:name, :description], query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -21,7 +21,12 @@ class Table < ActiveRecord::Base
after_save :update_ts_index
#accepts_nested_attributes_for :table
def self.search(user, include_archived, query = nil, page = 1)
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
step_ids =
Step
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
@ -36,43 +41,63 @@ class Table < ActiveRecord::Base
.distinct
.pluck('result_tables.id')
if query
a_query = query.strip
.gsub('_', '\\_')
.gsub('%', '\\%')
.split(/\s+/)
.map { |t| '%' + t + '%' }
else
a_query = query
end
# Trim whitespace and replace it with OR character. Make prefixed
# wildcard search term and escape special characters.
# For example, search term 'demo project' is transformed to
# 'demo:*|project:*' which makes word inclusive search with postfix
# wildcard.
s_query = query.gsub(/[!()&|:]/, " ")
.strip
.split(/\s+/)
.map {|t| t + ":*" }
.join("|")
.gsub('\'', '"')
table_query =
Table
.distinct
.joins("LEFT OUTER JOIN step_tables ON step_tables.table_id = tables.id")
.joins("LEFT OUTER JOIN result_tables ON result_tables.table_id = tables.id")
.joins("LEFT OUTER JOIN results ON result_tables.result_id = results.id")
.where("step_tables.id IN (?) OR result_tables.id IN (?)", step_ids, result_ids)
.where(
'(trim_html_tags(tables.name) ILIKE ANY (array[?])'\
'OR tables.data_vector @@ to_tsquery(?))',
.joins('LEFT OUTER JOIN step_tables ON step_tables.table_id = tables.id')
.joins('LEFT OUTER JOIN result_tables ON ' \
'result_tables.table_id = tables.id')
.joins('LEFT OUTER JOIN results ON result_tables.result_id = results.id')
.where('step_tables.id IN (?) OR result_tables.id IN (?)',
step_ids, result_ids)
if options[:whole_word].to_s == 'true' ||
options[:whole_phrase].to_s == 'true'
like = options[:match_case].to_s == 'true' ? '~' : '~*'
s_query = query.gsub(/[!()&|:]/, ' ')
.strip
.split(/\s+/)
.map { |t| t + ':*' }
if options[:whole_word].to_s == 'true'
a_query = query.split
.map { |a| Regexp.escape(a) }
.join('|')
s_query = s_query.join('|')
else
a_query = Regexp.escape(query)
s_query = s_query.join('&')
end
a_query = '\\y(' + a_query + ')\\y'
s_query = s_query.tr('\'', '"')
new_query = table_query.where(
"(trim_html_tags(tables.name) #{like} ?" \
"OR tables.data_vector @@ to_tsquery(?))",
a_query,
s_query
)
else
like = options[:match_case].to_s == 'true' ? 'LIKE' : 'ILIKE'
a_query = query.split.map { |a| "%#{sanitize_sql_like(a)}%" }
new_query = table_query
# Trim whitespace and replace it with OR character. Make prefixed
# wildcard search term and escape special characters.
# For example, search term 'demo project' is transformed to
# 'demo:*|project:*' which makes word inclusive search with postfix
# wildcard.
s_query = query.gsub(/[!()&|:]/, ' ')
.strip
.split(/\s+/)
.map { |t| t + ':*' }
.join('|')
.tr('\'', '"')
new_query = table_query.where(
"(trim_html_tags(tables.name) #{like} ANY (array[?])" \
"OR tables.data_vector @@ to_tsquery(?))",
a_query,
s_query
)
end
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -16,26 +16,21 @@ class Tag < ActiveRecord::Base
has_many :my_module_tags, inverse_of: :tag, :dependent => :destroy
has_many :my_modules, through: :my_module_tags
def self.search(user, include_archived, query = nil, page = 1)
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
project_ids =
Project
.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
if query
a_query = query.strip
.gsub("_","\\_")
.gsub("%","\\%")
.split(/\s+/)
.map {|t| "%" + t + "%" }
else
a_query = query
end
new_query = Tag
.distinct
.where("tags.project_id IN (?)", project_ids)
.where_attributes_like(:name, a_query)
.distinct
.where('tags.project_id IN (?)', project_ids)
.where_attributes_like(:name, query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT

View file

@ -1,20 +1,57 @@
<% provide(:head_title, t("search.index.head_title")) %>
<h1 class="page-header"><%= t('search.index.results_title_html', query: @display_query) %></h1>
<div class="page-header">
<h1><%= t('search.index.results_title_html', query: @display_query) %></h1>
<br>
<!-- search form -->
<div class="row">
<div class="col-xs-12 col-sm-8 col-md-6">
<%= form_tag search_path,
method: :get,
role: 'search' do %>
<div class="form-group">
<div class="input-group">
<input class="form-control"
type="text"
name="q"
placeholder="<%= t('nav.search') %>"
value="<%= @display_query %>">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
<label class="checkbox-inline">
<input id="search_whole_word" type="checkbox" name="whole_word" value="true" <%= 'checked' if @search_whole_word %>><%= I18n.t('search.whole_word') %>
</label>
<label class="checkbox-inline">
<input id="search_whole_phrase" type="checkbox" name="whole_phrase" value="true" <%= 'checked' if @search_whole_phrase %>><%= I18n.t('search.whole_phrase') %>
</label>
<label class="checkbox-inline">
<input id="search_match_case" type="checkbox" name="match_case" value="true" <%= 'checked' if @search_case %>><%= I18n.t('search.match_case') %>
</label>
</div>
<% end %>
</div>
</div>
</div>
<%= form_tag search_path, method: :get do %>
<%= hidden_field_tag :q, @search_query %>
<%= hidden_field_tag :category, @search_category %>
<div class="row">
<div class="col-xs-12 col-sm-3" id="search-menu">
<div class="col-xs-12 col-sm-3 col-md-3" id="search-menu">
<ul class="nav nav-pills nav-stacked nav-stacked-arrow">
<li role="presentation"
class="
<%= "active" if @search_category.present? and @search_category == :projects %>
<%= "disabled" if @project_search_count == 0 %>"
>
<a href="?<%= {category: 'projects', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'projects', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @project_search_count %></span>
<span class="glyphicon glyphicon-blackboard"></span>
<%= t'Projects' %>
@ -25,7 +62,9 @@
<%= "active" if @search_category.present? and @search_category == :experiments %>
<%= "disabled" if @experiment_search_count == 0 %>"
>
<a href="?<%= {category: 'experiments', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'experiments', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @experiment_search_count %></span>
<%= fa_icon 'flask' %>
<%= t'Experiments' %>
@ -36,7 +75,9 @@
<%= "active" if @search_category.present? and @search_category == :workflows %>
<%= "disabled" if @workflow_search_count == 0 %>"
>
<a href="?<%= {category: 'workflows', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'workflows', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @workflow_search_count %></span>
<span class="glyphicon glyphicon-random"></span>
<%= t'Workflows' %>
@ -47,7 +88,9 @@
<%= "active" if @search_category.present? and @search_category == :modules %>
<%= "disabled" if @module_search_count == 0 %>"
>
<a href="?<%= {category: 'modules', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'modules', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @module_search_count %></span>
<span class="glyphicon glyphicon-credit-card"></span>
<%= t'Modules' %>
@ -58,7 +101,9 @@
<%= "active" if @search_category.present? and @search_category == :results %>
<%= "disabled" if @result_search_count == 0 %>"
>
<a href="?<%= {category: 'results', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'results', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @result_search_count %></span>
<span class="glyphicon glyphicon-modal-window"></span>
<%= t'Results' %>
@ -69,7 +114,9 @@
<%= "active" if @search_category.present? and @search_category == :tags %>
<%= "disabled" if @tag_search_count == 0 %>"
>
<a href="?<%= {category: 'tags', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'tags', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @tag_search_count %></span>
<span class="glyphicon glyphicon-tags"></span>
<%= t'Tags' %>
@ -80,7 +127,9 @@
<%= "active" if @search_category.present? and @search_category == :reports %>
<%= "disabled" if @report_search_count == 0 %>"
>
<a href="?<%= {category: 'reports', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'reports', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @report_search_count %></span>
<span class="glyphicon glyphicon-align-left"></span>
<%= t'Reports' %>
@ -91,7 +140,9 @@
<%= "active" if @search_category.present? and @search_category == :protocols %>
<%= "disabled" if @protocol_search_count == 0 %>"
>
<a href="?<%= {category: 'protocols', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'protocols', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @protocol_search_count %></span>
<span class="glyphicon glyphicon-list-alt"></span>
<%= t'Protocols' %>
@ -102,7 +153,9 @@
<%= "active" if @search_category.present? and @search_category == :steps %>
<%= "disabled" if @step_search_count == 0 %>"
>
<a href="?<%= {category: 'steps', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'steps', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @step_search_count %></span>
<span class="glyphicon glyphicon-circle-arrow-right"></span>
<%= t'Steps' %>
@ -113,7 +166,9 @@
<%= "active" if @search_category.present? and @search_category == :checklists %>
<%= "disabled" if @checklist_search_count == 0 %>"
>
<a href="?<%= {category: 'checklists', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'checklists', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @checklist_search_count %></span>
<span class="glyphicon glyphicon-list"></span>
<%= t'Checklists' %>
@ -124,7 +179,9 @@
<%= "active" if @search_category.present? and @search_category == :samples %>
<%= "disabled" if @sample_search_count == 0 %>"
>
<a href="?<%= {category: 'samples', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'samples', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @sample_search_count %></span>
<span class="glyphicon glyphicon-tint"></span>
<%= t'Samples' %>
@ -135,7 +192,9 @@
<%= "active" if @search_category.present? and @search_category == :assets %>
<%= "disabled" if @asset_search_count == 0 %>"
>
<a href="?<%= {category: 'assets', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'assets', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @asset_search_count %></span>
<span class="glyphicon glyphicon-file"></span>
<%= t'Assets' %>
@ -146,7 +205,9 @@
<%= "active" if @search_category.present? and @search_category == :tables %>
<%= "disabled" if @table_search_count == 0 %>"
>
<a href="?<%= {category: 'tables', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'tables', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @table_search_count %></span>
<span class="glyphicon glyphicon-th"></span>
<%= t'Tables' %>
@ -157,7 +218,9 @@
<%= "active" if @search_category.present? and @search_category == :comments %>
<%= "disabled" if @comment_search_count == 0 %>"
>
<a href="?<%= {category: 'comments', q: @search_query, utf8: '✓'}.to_query %>">
<a href="?<%= {category: 'comments', q: @search_query,
whole_word: @search_whole_word, whole_phrase: @search_whole_phrase,
match_case: @search_case, utf8: '✓'}.to_query %>">
<span class="badge pull-right"><%= @comment_search_count %></span>
<span class="glyphicon glyphicon-comment"></span>
<%= t'Comments' %>
@ -251,3 +314,5 @@
</ul>
</nav>
<% end %>
<%= javascript_include_tag('search') %>

View file

@ -15,7 +15,7 @@
<!-- Display asset contents if it exists -->
<% if asset.headline.present? && !asset.headline.empty? && asset.headline.include?("<mark>") %>
<blockquote class="blockquote-search">
<p><%= sanitize_input(asset.headline) %></p>
<p><%= highlight(sanitize_input(asset.headline), search_query.strip.split(/\s+/)) %></p>
</blockquote>
<% end %>

View file

@ -71,6 +71,7 @@ Rails.application.config.assets.precompile += %w(notifications.js)
Rails.application.config.assets.precompile += %w(users/invite_users_modal.js)
Rails.application.config.assets.precompile += %w(samples/sample_types_groups.js)
Rails.application.config.assets.precompile += %w(highlightjs-github-theme.css)
Rails.application.config.assets.precompile += %w(search.js)
# Libraries needed for Handsontable formulas
Rails.application.config.assets.precompile += %w(lodash.js)

View file

@ -100,6 +100,9 @@ en:
archive: "Archive"
search:
whole_word: "Match any whole word"
whole_phrase: "Match whole phrase"
match_case: "Match case"
index:
head_title: "Search"
page_title: "Search"
@ -1597,6 +1600,7 @@ en:
length_too_long: "is too long (maximum is %{max_length} characters)"
length_too_short: "is too short (minimum is %{min_length} characters)"
query:
wrong_query: "All words in search query are either too short (minimum is %{min_length} characters) or too long (maximum is %{max_length} characters)"
length_too_long: "Search query is too long (maximum is %{max_length} characters)"
length_too_short: "Search query is too short (minimum is %{min_length} characters)"
busy: "The server is still processing your request. If you leave this page, the changes will be lost! Are you sure you want to continue?"