Implement global search backend [SCI-10573]

This commit is contained in:
Andrej 2024-04-18 15:45:34 +02:00
parent 2ad7c36f0f
commit 3c4184c73e
21 changed files with 308 additions and 649 deletions

View file

@ -21,7 +21,9 @@ module Dashboard
def project_filter
projects = Project.readable_by_user(current_user)
.search(current_user, false, params[:query], 1, current_team)
.search(current_user, false, params[:query], current_team)
.page(1)
.per(Constants::SEARCH_LIMIT)
.select(:id, :name)
projects = projects.map { |i| { value: i.id, label: escape_input(i.name) } }
if (projects.map { |i| i[:label] }.exclude? params[:query]) && params[:query].present?
@ -36,7 +38,9 @@ module Dashboard
elsif @project
experiments = @project.experiments
.managable_by_user(current_user)
.search(current_user, false, params[:query], 1, current_team)
.search(current_user, false, params[:query], current_team)
.page(1)
.per(Constants::SEARCH_LIMIT)
.select(:id, :name)
experiments = experiments.map { |i| { value: i.id, label: escape_input(i.name) } }
if (experiments.map { |i| i[:label] }.exclude? params[:query]) &&

View file

@ -165,7 +165,9 @@ class ResultsController < ApplicationController
def apply_filters!
if params[:query].present?
@results = @results.search(current_user, params[:view_mode] == 'archived', params[:query], params[:page] || 1)
@results = @results.search(current_user, params[:view_mode] == 'archived', params[:query])
.page(params[:page] || 1)
.per(Constants::SEARCH_LIMIT)
end
@results = @results.where('results.created_at >= ?', params[:created_at_from]) if params[:created_at_from]

View file

@ -117,7 +117,6 @@ class SearchController < ApplicationController
}
return
when 'protocols'
@protocol_search_count = fetch_cached_count(Protocol)
search_protocols
results = if params[:preview] == 'true'
@protocol_results.take(4)
@ -184,38 +183,6 @@ class SearchController < ApplicationController
}
return
end
#@search_id = params[:search_id] ? params[:search_id] : generate_search_id
#
#count_search_results
#
#search_projects if @search_category == :projects
#search_project_folders if @search_category == :project_folders
#search_experiments if @search_category == :experiments
#search_modules if @search_category == :modules
#search_results if @search_category == :results
#search_tags if @search_category == :tags
#search_reports if @search_category == :reports
#search_protocols if @search_category == :protocols
#search_steps if @search_category == :steps
#search_checklists if @search_category == :checklists
#if @search_category == :repositories && params[:repository]
# search_repository
#end
#search_assets if @search_category == :assets
#search_tables if @search_category == :tables
#search_comments if @search_category == :comments
#@search_pages = (@search_count.to_f / Constants::SEARCH_LIMIT.to_f).ceil
#@start_page = @search_page - 2
#@start_page = 1 if @start_page < 1
#@end_page = @start_page + 4
#if @end_page > @search_pages
# @end_page = @search_pages
# @start_page = @end_page - 4
# @start_page = 1 if @start_page < 1
#end
end
end
end
@ -252,46 +219,35 @@ class SearchController < ApplicationController
def load_vars
query = (params.fetch(:q) { '' }).strip
@search_category = params[:category] || ''
@search_category = @search_category.to_sym
@search_page = params[:page].to_i || 1
@search_case = params[:match_case] == 'true'
@search_whole_word = params[:whole_word] == 'true'
@search_whole_phrase = params[:whole_phrase] == 'true'
@filters = params[:filters]
@include_archived = if @filters.present?
@filters[:include_archived] == 'true'
else
true
end
@teams = if @filters.present?
@filters[:teams]&.values || current_user.teams
else
current_user.teams
end
@display_query = query
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_back(fallback_location: root_path)
elsif query.length > Constants::TEXT_MAX_LENGTH
flash[:error] = t('general.query.length_too_long',
max_length: Constants::TEXT_MAX_LENGTH)
redirect_back(fallback_location: root_path)
else
@search_query = query
end
else
# 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.blank?
flash[:error] = t('general.query.wrong_query',
min_length: Constants::NAME_MIN_LENGTH,
max_length: Constants::TEXT_MAX_LENGTH)
redirect_back(fallback_location: root_path)
else
@search_query.strip!
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
@search_page = 1 if @search_page < 1
if @search_query.blank?
flash[:error] = t('general.query.wrong_query',
min_length: Constants::NAME_MIN_LENGTH,
max_length: Constants::TEXT_MAX_LENGTH)
redirect_back(fallback_location: root_path)
else
@search_query.strip!
end
end
protected
@ -300,109 +256,27 @@ class SearchController < ApplicationController
SecureRandom.urlsafe_base64(32)
end
def search_by_name(model)
def search_by_name(model, options={})
records = model.search(current_user,
@include_archived,
@search_query,
nil,
teams: @teams,
users: @users,
options: options)
records = filter_records(records, model) if @filters.present?
sort_records(records)
end
def count_by_name(model, options = {})
model.search(current_user,
true,
@include_archived,
@search_query,
@search_page,
nil,
match_case: @search_case,
whole_word: @search_whole_word,
whole_phrase: @search_whole_phrase)
.order(created_at: :desc)
end
def count_by_name(model)
model.search(current_user,
true,
@search_query,
Constants::SEARCH_NO_LIMIT,
nil,
match_case: @search_case,
whole_word: @search_whole_word,
whole_phrase: @search_whole_phrase).size
end
def count_by_repository
@repository_search_count =
Rails.cache.fetch("#{@search_id}/repository_search_count",
expires_in: 5.minutes) do
search_count = {}
search_results = Repository.search(current_user,
@search_query,
Constants::SEARCH_NO_LIMIT,
nil,
match_case: @search_case,
whole_word: @search_whole_word,
whole_phrase: @search_whole_phrase)
current_user.teams.includes(:repositories).each do |team|
team_results = {}
team_results[:team] = team
team_results[:count] = 0
team_results[:repositories] = {}
Repository.accessible_by_teams(team).each do |repository|
repository_results = {}
repository_results[:id] = repository.id
repository_results[:repository] = repository
repository_results[:count] = 0
search_results.each do |result|
repository_results[:count] += result.counter if repository.id == result.id
end
team_results[:repositories][repository.name] = repository_results
team_results[:count] += repository_results[:count]
end
search_count[team.name] = team_results
end
search_count
end
count_total = 0
@repository_search_count.each_value do |team_results|
count_total += team_results[:count]
end
count_total
end
def current_repository_search_count
@repository_search_count.each_value do |counter|
res = counter[:repositories].values.detect do |rep|
rep[:id] == @repository.id
end
return res[:count] if res && res[:count]
end
end
def count_search_results
@project_search_count = fetch_cached_count Project
@project_folder_search_count = fetch_cached_count ProjectFolder
@experiment_search_count = fetch_cached_count Experiment
@module_search_count = fetch_cached_count MyModule
@result_search_count = fetch_cached_count Result
@tag_search_count = fetch_cached_count Tag
@report_search_count = fetch_cached_count Report
@protocol_search_count = fetch_cached_count Protocol
@step_search_count = fetch_cached_count Step
@checklist_search_count = fetch_cached_count Checklist
@repository_search_count_total = count_by_repository
@asset_search_count = fetch_cached_count Asset
@table_search_count = fetch_cached_count Table
@comment_search_count = fetch_cached_count Comment
@search_results_count = @project_search_count
@search_results_count += @project_folder_search_count
@search_results_count += @experiment_search_count
@search_results_count += @module_search_count
@search_results_count += @result_search_count
@search_results_count += @tag_search_count
@search_results_count += @report_search_count
@search_results_count += @protocol_search_count
@search_results_count += @step_search_count
@search_results_count += @checklist_search_count
@search_results_count += @repository_search_count_total
@search_results_count += @asset_search_count
@search_results_count += @table_search_count
@search_results_count += @comment_search_count
teams: @teams,
users: @users,
options: options).size
end
def fetch_cached_count(type)
@ -410,7 +284,7 @@ class SearchController < ApplicationController
Rails.cache.fetch(
"#{@search_id}/#{type.name.underscore}_search_count", expires_in: exp
) do
count_by_name type
count_by_name(type)
end
end
@ -439,8 +313,8 @@ class SearchController < ApplicationController
end
def search_module_protocols
@module_protocol_results = search_by_name(Protocol)
@search_count = @module_protocol_results.joins(:my_module).count
@module_protocol_results = search_by_name(Protocol, { in_repository: false })
@search_count = @module_protocol_results.count
end
def search_results
@ -449,12 +323,6 @@ class SearchController < ApplicationController
@search_count = @result_search_count
end
def search_tags
@tag_results = []
@tag_results = search_by_name(Tag) if @tag_search_count.positive?
@search_count = @tag_search_count
end
def search_reports
@report_results = Report.none
@report_results = search_by_name(Report) if @report_search_count.positive?
@ -462,9 +330,8 @@ class SearchController < ApplicationController
end
def search_protocols
@protocol_results = Protocol.none
@protocol_results = search_by_name(Protocol) if @protocol_search_count.positive?
@search_count = @protocol_search_count
@protocol_results = search_by_name(Protocol, { in_repository: true })
@search_count = @protocol_results.count
end
def search_label_templates
@ -479,29 +346,6 @@ class SearchController < ApplicationController
@search_count = @step_search_count
end
def search_checklists
@checklist_results = []
@checklist_results = search_by_name(Checklist) if @checklist_search_count.positive?
@search_count = @checklist_search_count
end
def search_repository
@repository = Repository.find_by(id: params[:repository])
unless current_user.teams.include?(@repository.team) || @repository.private_shared_with?(current_user.teams)
render_403
end
@repository_results = []
if @repository_search_count_total.positive?
@repository_results =
Repository.search(current_user, @search_query, @search_page,
@repository,
match_case: @search_case,
whole_word: @search_whole_word,
whole_phrase: @search_whole_phrase)
end
@search_count = current_repository_search_count
end
def search_repository_rows
@repository_row_results = RepositoryRow.none
@repository_row_results = search_by_name(RepositoryRow) if @repository_row_search_count.positive?
@ -514,15 +358,52 @@ class SearchController < ApplicationController
@search_count = @asset_search_count
end
def search_tables
@table_results = []
@table_results = search_by_name(Table) if @table_search_count.positive?
@search_count = @table_search_count
def filter_records(records, model)
model_name = model.model_name.collection
if @filters[:created_at].present?
if @filters[:created_at][:on].present?
from_date = Time.zone.parse(@filters[:created_at][:on]).beginning_of_day.utc
to_date = Time.zone.parse(@filters[:created_at][:on]).end_of_day.utc
else
from_date = Time.zone.parse(@filters[:created_at][:from])
to_date = Time.zone.parse(@filters[:created_at][:to])
end
records = records.where("#{model_name}.created_at >= ?", from_date)
records = records.where("#{model_name}.created_at <= ?", to_date)
end
if @filters[:updated_at].present?
if @filters[:updated_at][:on].present?
from_date = Time.zone.parse(@filters[:updated_at][:on]).beginning_of_day.utc
to_date = Time.zone.parse(@filters[:updated_at][:on]).end_of_day.utc
else
from_date = Time.zone.parse(@filters[:updated_at][:from])
to_date = Time.zone.parse(@filters[:updated_at][:to])
end
records = records.where("#{model_name}.updated_at >= ?", from_date)
records = records.where("#{model_name}.updated_at <= ?", to_date)
end
if @filters[:users].present?
records = records.joins("INNER JOIN activities ON #{model_name}.id = activities.subject_id
AND activities.subject_type= '#{model.name}'")
.where('activities.owner_id': @filters[:users]&.values)
end
records
end
def search_comments
@comment_results = []
@comment_results = search_by_name(Comment) if @comment_search_count.positive?
@search_count = @comment_search_count
def sort_records(records)
case params[:sort]
when 'atoz'
records.order(name: :asc)
when 'ztoa'
records.order(name: :desc)
when 'created_asc'
records.order(created_at: :asc)
else
records.order(created_at: :desc)
end
end
end

View file

@ -11,6 +11,7 @@ class Asset < ApplicationRecord
require 'tempfile'
# Lock duration set to 30 minutes
LOCK_DURATION = 60 * 30
SEARCHABLE_ATTRIBUTES = ['active_storage_blobs.filename', 'asset_text_data.data_vector'].freeze
enum view_mode: { thumbnail: 0, list: 1, inline: 2 }
@ -58,101 +59,36 @@ class Asset < ApplicationRecord
user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
current_team = nil,
options = {}
)
teams = user.teams.select(:id)
teams = options[:teams] || current_team || user.teams.select(:id)
assets_in_steps = Asset.joins(:step).where(
'steps.id IN (?)',
Step.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
Step.search(user, include_archived, nil, teams)
.select(:id)
).pluck(:id)
assets_in_results = Asset.joins(:result).where(
'results.id IN (?)',
Result.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.select(:id)
Result.search(user, include_archived, nil, teams).select(:id)
).pluck(:id)
assets_in_inventories = Asset.joins(
repository_cell: { repository_column: :repository }
).where('repositories.team_id IN (?)', teams).pluck(:id)
).where(repositories: { team_id: teams }).pluck(:id)
assets =
Asset.distinct
.where('assets.id IN (?) OR assets.id IN (?) OR assets.id IN (?)',
assets_in_steps, assets_in_results, assets_in_inventories)
new_query = Asset.left_outer_joins(:asset_text_datum)
.joins(file_attachment: :blob)
.from(assets, 'assets')
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('\'', '"')
new_query = new_query.where(
"(active_storage_blobs.filename #{like} ? " \
"OR asset_text_data.data_vector @@ plainto_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('\'', '"')
new_query = new_query.where(
"(active_storage_blobs.filename #{like} ANY (array[?]) " \
"OR asset_text_data.data_vector @@ plainto_tsquery(?))",
a_query,
s_query
)
end
# Show all results if needed
if page != Constants::SEARCH_NO_LIMIT
new_query = new_query.select('assets.*, asset_text_data.data AS data')
.limit(Constants::SEARCH_LIMIT)
.offset((page - 1) * Constants::SEARCH_LIMIT)
Asset.select(
"assets_search.*, " \
"ts_headline(assets_search.data, plainto_tsquery('#{sanitize_sql_for_conditions(s_query)}'), " \
"'StartSel=<mark>, StopSel=</mark>') AS headline"
).from(new_query, 'assets_search')
else
new_query
end
Asset.left_outer_joins(:asset_text_datum)
.joins(file_attachment: :blob)
.from(assets, 'assets')
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
end
def blob

View file

@ -31,28 +31,6 @@ class Checklist < ApplicationRecord
scope :asc, -> { order('checklists.created_at ASC') }
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)
new_query = Checklist.distinct
.where(checklists: { step_id: step_ids })
.left_outer_joins(:checklist_items)
.where_attributes_like(['checklists.name', 'checklist_items.text'], query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
end
def duplicate(step, user, position = nil)
ActiveRecord::Base.transaction do
new_checklist = step.checklists.create!(

View file

@ -12,45 +12,6 @@ class Comment < ApplicationRecord
scope :unseen_by, ->(user) { where('? = ANY (unseen_by)', user.id) }
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)
my_module_ids = MyModule.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
step_ids = Step.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
result_ids = Result.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.pluck(:id)
new_query = Comment.distinct
.joins(:user)
.where(
'(comments.associated_id IN (?) AND comments.type = ?) OR ' \
'(comments.associated_id IN (?) AND comments.type = ?) OR ' \
'(comments.associated_id IN (?) AND comments.type = ?) OR ' \
'(comments.associated_id IN (?) AND comments.type = ?)',
project_ids, 'ProjectComment',
my_module_ids, 'TaskComment',
step_ids, 'StepComment',
result_ids, 'ResultComment'
)
.where_attributes_like(['message', 'users.full_name'], query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
end
def self.mark_as_seen_by(user, commentable)
# rubocop:disable Rails/SkipsModelValidations
all.where('? = ANY (unseen_by)', user.id).update_all("unseen_by = array_remove(unseen_by, #{user.id.to_i}::bigint)")

View file

@ -10,18 +10,7 @@ module SearchableModel
scope :where_attributes_like, lambda { |attributes, query, options = {}|
return unless query
attrs = []
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(&:to_s)
else
raise ArgumentError, ':attributes must be an array, symbol or string'
end
attrs = convert_input(attributes)
if options[:whole_word].to_s == 'true' ||
options[:whole_phrase].to_s == 'true' ||
@ -109,5 +98,100 @@ module SearchableModel
end
end
}
scope :where_attributes_like_boolean, lambda { |attributes, query, options = {}|
return unless query
attrs = convert_input(attributes)
where_array = []
value_array = {}
current_phrase = ''
exact_match = false
negate = false
index = 0
query.split.each do |phrase|
phrase = phrase.strip
if phrase.start_with?('"') && phrase.ends_with?('"')
create_query(attrs, index, negate, where_array, value_array, phrase[1..-2], true)
negate = false
elsif phrase.start_with?('"')
exact_match = true
current_phrase = phrase[1..]
elsif exact_match && phrase.ends_with?('"')
exact_match = false
create_query(attrs, index, negate, where_array, value_array, "#{current_phrase} #{phrase[0..-2]}", true)
current_phrase = ''
negate = false
elsif exact_match
current_phrase = "#{current_phrase} #{phrase}"
elsif phrase.casecmp('and').zero?
next
elsif phrase.casecmp('not').zero?
negate = true
elsif phrase.casecmp('or').zero?
where_array[-1] = "#{where_array.last[0..-5]} OR "
else
create_query(attrs, index, negate, where_array, value_array, "%#{phrase}%")
negate = false
end
index += 1
end
if current_phrase.present?
current_phrase = current_phrase[0..-2] if current_phrase.ends_with?('"')
create_query(attrs, index, negate, where_array, value_array, current_phrase, true)
end
where(where_array.join[0..-5], value_array)
}
def self.convert_input(attributes)
attrs = []
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(&:to_s)
else
raise ArgumentError, ':attributes must be an array, symbol or string'
end
attrs
end
def self.create_query(attrs, index, negate, where_array, value_array, phrase, exact_match=false)
like = exact_match ? '~' : 'ILIKE'
phrase = "\\m#{phrase}\\M" if exact_match
where_clause = (attrs.map.with_index do |a, i|
i = (index * attrs.count) + i
if %w(repository_rows.id repository_number_values.data).include?(a)
"#{a} IS NOT NULL AND (((#{a})::text) #{like} :t#{i}) OR "
elsif defined?(model::PREFIXED_ID_SQL) && a == model::PREFIXED_ID_SQL
"#{a} IS NOT NULL AND (#{a} #{like} :t#{i}) OR "
elsif a == 'asset_text_data.data_vector'
"asset_text_data.data_vector @@ plainto_tsquery(:t#{i})) OR"
else
"#{a} IS NOT NULL AND ((trim_html_tags(#{a})) #{like} :t#{i}) OR "
end
end).join[0..-5]
where_array << if negate
"NOT (#{where_clause}) AND "
else
"(#{where_clause}) AND "
end
value_array.merge!(
(attrs.map.with_index do |_, i|
i = (index * attrs.count) + i
["t#{i}".to_sym, phrase]
end).to_h
)
end
end
end

View file

@ -64,24 +64,18 @@ class Experiment < ApplicationRecord
user,
include_archived,
query = nil,
page = 1,
current_team = nil,
options = {}
)
viewable_projects = Project.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT, current_team)
.pluck(:id)
new_query = Experiment.with_granted_permissions(user, ExperimentPermissions::READ)
.where(project: viewable_projects)
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
teams = options[:teams] || current_team || user.teams.select(:id)
new_query = distinct.with_granted_permissions(user, ExperimentPermissions::READ)
.where(user_assignments: { team: teams })
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
new_query = new_query.active unless include_archived
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
new_query
end
def self.viewable_by_user(user, teams)

View file

@ -37,22 +37,15 @@ class LabelTemplate < ApplicationRecord
end
def self.search(
_user,
user,
_include_archived,
query = nil,
page = 1,
_current_team = nil,
current_team = nil,
options = {}
)
new_query = LabelTemplate.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
teams = options[:teams] || current_team || user.teams.select(:id)
distinct.viewable_by_user(user, teams)
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
end
def icon

View file

@ -3,7 +3,8 @@
class MyModule < ApplicationRecord
ID_PREFIX = 'TA'
include PrefixedIdModel
SEARCHABLE_ATTRIBUTES = ['my_modules.name', 'my_modules.description', PREFIXED_ID_SQL].freeze
SEARCHABLE_ATTRIBUTES = ['my_modules.name', 'my_modules.description', PREFIXED_ID_SQL,
'comments.message', 'tags.name', 'users.full_name', 'users.email'].freeze
include ArchivableModel
include SearchableModel
@ -108,25 +109,18 @@ class MyModule < ApplicationRecord
user,
include_archived,
query = nil,
page = 1,
current_team = nil,
options = {}
)
viewable_experiments = Experiment.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT, current_team)
.pluck(:id)
teams = options[:teams] || current_team || user.teams.select(:id)
new_query = MyModule.with_granted_permissions(user, MyModulePermissions::READ)
.where(experiment: viewable_experiments)
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
new_query = distinct.left_joins(:task_comments, my_module_tags: :tag, user_my_modules: :user)
.with_granted_permissions(user, MyModulePermissions::READ)
.where(user_assignments: { team: teams })
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
new_query = new_query.active unless include_archived
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
new_query
end
def self.viewable_by_user(user, teams)

View file

@ -3,7 +3,7 @@
class Project < ApplicationRecord
ID_PREFIX = 'PR'
include PrefixedIdModel
SEARCHABLE_ATTRIBUTES = ['projects.name', PREFIXED_ID_SQL].freeze
SEARCHABLE_ATTRIBUTES = ['projects.name', PREFIXED_ID_SQL, 'comments.message'].freeze
include ArchivableModel
include SearchableModel
@ -84,21 +84,17 @@ class Project < ApplicationRecord
user,
include_archived,
query = nil,
page = 1,
current_team = nil,
options = {}
)
teams = options[:teams] || current_team || user.teams.select(:id)
new_query = distinct.viewable_by_user(user, teams)
.left_joins(:project_comments)
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
new_query = Project.viewable_by_user(user, current_team || user.teams)
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
new_query = new_query.active unless include_archived
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
new_query
end
def self.viewable_by_user(user, teams)

View file

@ -39,21 +39,14 @@ class ProjectFolder < ApplicationRecord
.where(team: teams)
end
def self.search(user, _include_archived, query = nil, page = 1, current_team = nil, options = {})
new_query = if current_team
current_team.project_folders.where_attributes_like(:name, query, options)
else
distinct.joins(team: :users)
.where(teams: { user_assignments: { user: user } })
.where_attributes_like('project_folders.name', query, options)
end
def self.search(user, include_archived, query = nil, current_team = nil, options = {})
teams = options[:teams] || current_team || user.teams.select(:id)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
new_query = distinct.viewable_by_user(user, teams)
.where_attributes_like_boolean('project_folders.name', query, options)
new_query = new_query.active unless include_archived
new_query
end
def self.inner_folders(team, project_folder = nil)

View file

@ -6,7 +6,8 @@ class Protocol < ApplicationRecord
include ArchivableModel
include PrefixedIdModel
SEARCHABLE_ATTRIBUTES = ['protocols.name', 'protocols.description',
'protocols.authors', 'protocol_keywords.name', PREFIXED_ID_SQL].freeze
'step_texts.name', 'step_texts.text', 'tables.name',
'checklists.name', 'checklist_items.text', PREFIXED_ID_SQL].freeze
REPOSITORY_TYPES = %i(in_repository_published_original in_repository_draft in_repository_published_version).freeze
include SearchableModel
@ -165,34 +166,27 @@ class Protocol < ApplicationRecord
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
current_team = nil,
options = {})
repository_protocols = latest_available_versions(user.teams)
.with_granted_permissions(user, ProtocolPermissions::READ)
.select(:id)
repository_protocols = repository_protocols.active unless include_archived
teams = options[:teams] || current_team || user.teams.select(:id)
new_query = if options[:options][:in_repository]
latest_available_versions(teams)
.with_granted_permissions(user, ProtocolPermissions::READ)
else
distinct.joins(:my_module)
.joins("INNER JOIN user_assignments my_module_user_assignments " \
"ON my_module_user_assignments.assignable_type = 'MyModule' " \
"AND my_module_user_assignments.assignable_id = my_modules.id")
.where(my_module_user_assignments: { user_id: user })
.where(team: teams)
end
module_ids = MyModule.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT).pluck(:id)
new_query = new_query.active unless include_archived
new_query = Protocol
.where(
'(protocol_type IN (?) AND my_module_id IN (?)) OR (protocols.id IN (?))',
[Protocol.protocol_types[:unlinked], Protocol.protocol_types[:linked]],
module_ids,
repository_protocols
)
new_query = new_query.left_outer_joins(:protocol_keywords)
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
.distinct
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
new_query.left_outer_joins(steps: [:step_texts, { step_tables: :table },
{ checklists: :checklist_items }, :step_comments])
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
.distinct
end
def self.latest_available_versions(teams)

View file

@ -68,26 +68,15 @@ class Report < ApplicationRecord
def self.search(
user,
include_archived,
_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)
new_query = Report.distinct
.where(reports: { project_id: project_ids })
.where_attributes_like(SEARCHABLE_ATTRIBUTES, query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
)
teams = options[:teams] || current_team || user.teams.select(:id)
distinct.with_granted_permissions(user, ReportPermissions::READ)
.where(team: teams)
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
end
def self.viewable_by_user(user, teams)

View file

@ -78,36 +78,6 @@ class Repository < RepositoryBase
team.repositories.count < Rails.configuration.x.team_repositories_limit
end
def self.search(
user,
query = nil,
page = 1,
repository = nil,
options = {}
)
serchable_row_fields = [RepositoryRow::PREFIXED_ID_SQL, 'repository_rows.name', 'users.full_name']
repositories = repository&.id || Repository.accessible_by_teams(user.teams).pluck(:id)
readable_rows = RepositoryRow.joins(:repository, :created_by).where(repository_id: repositories)
repository_rows = readable_rows.where_attributes_like(serchable_row_fields, query, options)
Extends::REPOSITORY_EXTRA_SEARCH_ATTR.each do |_data_type, config|
custom_cell_matches = readable_rows.joins(config[:includes])
.where_attributes_like(config[:field], query, options)
repository_rows = repository_rows.or(readable_rows.where(id: custom_cell_matches))
end
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
repository_rows.select('repositories.id AS id, COUNT(DISTINCT repository_rows.id) AS counter')
.group('repositories.id')
else
repository_rows.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
end
def self.filter_by_teams(teams = [])
teams.blank? ? self : where(team: teams)
end

View file

@ -123,30 +123,28 @@ class RepositoryRow < ApplicationRecord
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
options = {})
teams = options[:teams] || current_team || user.teams.select(:id)
searchable_row_fields = [RepositoryRow::PREFIXED_ID_SQL, 'repository_rows.name', 'users.full_name']
repositories = Repository.search(user).pluck(:id)
new_query =
RepositoryRow
.joins(:repository, :created_by)
.where(repository_id: repositories)
.distinct
.where_attributes_like(
searchable_row_fields, query, options
)
readable_rows = distinct.joins(:repository, :created_by)
.joins("INNER JOIN user_assignments repository_user_assignments " \
"ON repository_user_assignments.assignable_type = 'RepositoryBase' " \
"AND repository_user_assignments.assignable_id = repositories.id")
.where(repository_user_assignments: { user_id: user, team_id: teams })
new_query = new_query.active unless include_archived
readable_rows = readable_rows.active unless include_archived
repository_rows = readable_rows.where_attributes_like_boolean(searchable_row_fields, query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
Extends::REPOSITORY_EXTRA_SEARCH_ATTR.each do |_data_type, config|
custom_cell_matches = readable_rows.joins(config[:includes])
.where_attributes_like_boolean(config[:field], query, options)
repository_rows = repository_rows.or(readable_rows.where(id: custom_cell_matches))
end
repository_rows
end
def self.filter_by_teams(teams = [])

View file

@ -9,6 +9,9 @@ class Result < ApplicationRecord
auto_strip_attributes :name, nullify: false
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
SEARCHABLE_ATTRIBUTES = ['results.name', 'result_texts.name', 'result_texts.text',
'tables.name', 'comments.message'].freeze
enum assets_view_mode: { thumbnail: 0, list: 1, inline: 2 }
belongs_to :user, inverse_of: :results
@ -35,33 +38,21 @@ class Result < ApplicationRecord
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
current_team = nil,
options = {})
module_ids = MyModule.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT).pluck(:id)
teams = options[:teams] || current_team || user.teams.select(:id)
new_query =
Result
.distinct
.left_outer_joins(:result_texts, result_tables: :table)
.where(results: { my_module_id: module_ids })
.where_attributes_like(
[
'results.name',
'result_texts.name',
'result_texts.text',
'tables.name'
], query, options
)
new_query = distinct.left_joins(:result_comments, :result_texts, result_tables: :table)
.joins(:my_module)
.joins("INNER JOIN user_assignments my_module_user_assignments " \
"ON my_module_user_assignments.assignable_type = 'MyModule' " \
"AND my_module_user_assignments.assignable_id = my_modules.id")
.where(my_module_user_assignments: { user_id: user, team_id: teams })
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
new_query = new_query.active unless include_archived
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
new_query
end
def duplicate(my_module, user, result_name: nil)

View file

@ -61,23 +61,22 @@ class Step < ApplicationRecord
def self.search(user,
include_archived,
query = nil,
page = 1,
_current_team = nil,
current_team = nil,
options = {})
protocol_ids = Protocol.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
teams = options[:teams] || current_team || user.teams.select(:id)
protocol_ids = Protocol.search(user, include_archived, nil, teams,
options: { in_repository: false })
.pluck(:id)
my_module_ids = Protocol.search(user, include_archived, nil, teams,
options: { in_repository: true })
.pluck(:id)
new_query = Step.distinct
.left_outer_joins(:step_texts)
.where(steps: { protocol_id: protocol_ids })
.where_attributes_like(['steps.name', 'step_texts.text'], query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
Step.distinct
.where(protocol_id: protocol_ids + my_module_ids)
.left_outer_joins(:step_texts)
.where(steps: { protocol_id: protocol_ids })
.where_attributes_like_boolean(['steps.name', 'step_texts.text'], query, options)
end
def self.filter_by_teams(teams = [])

View file

@ -29,82 +29,6 @@ class Table < ApplicationRecord
after_save :update_ts_index
after_save { result&.touch; step&.touch }
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)
.joins(:step_tables)
.distinct
.pluck('step_tables.id')
result_ids = Result.search(user, include_archived, nil, Constants::SEARCH_NO_LIMIT)
.joins(:result_tables)
.distinct
.pluck('result_tables.id')
table_query = Table.distinct
.left_outer_joins(:step_table, :result_table, :result)
.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)}%" }
# 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
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
end
def metadata
attributes['metadata'].is_a?(String) ? JSON.parse(attributes['metadata']) : attributes['metadata']
end

View file

@ -17,26 +17,4 @@ class Tag < ApplicationRecord
belongs_to :project
has_many :my_module_tags, inverse_of: :tag, dependent: :destroy
has_many :my_modules, through: :my_module_tags, dependent: :destroy
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)
new_query = Tag
.distinct
.where(tags: { project_id: project_ids })
.where_attributes_like(:name, query, options)
# Show all results if needed
if page == Constants::SEARCH_NO_LIMIT
new_query
else
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
end
end
end

View file

@ -15,7 +15,7 @@ module GlobalSearch
def created_by
{
name: object.created_by ? object.created_by.name : object.created_by_user,
name: object.type == 'FluicsLabelTemplate' ? object.created_by_user : object.created_by&.name,
avatar_url: object.created_by ? avatar_path(object.created_by, :icon_small) : nil
}
end