mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-11 10:06:53 +08:00
499 lines
18 KiB
Ruby
499 lines
18 KiB
Ruby
require 'active_record'
|
|
|
|
class SampleDatatable < CustomDatatable
|
|
include ActionView::Helpers::TextHelper
|
|
include SamplesHelper
|
|
include InputSanitizeHelper
|
|
include Rails.application.routes.url_helpers
|
|
include ActionView::Helpers::UrlHelper
|
|
include ApplicationHelper
|
|
include ActiveRecord::Sanitization::ClassMethods
|
|
|
|
ASSIGNED_SORT_COL = 'assigned'
|
|
|
|
SAMPLES_TABLE_DEFAULT_STATE = {
|
|
'time' => 0,
|
|
'start' => 0,
|
|
'length' => 10,
|
|
'order' => [[2, 'desc']],
|
|
'search' => { 'search' => '',
|
|
'smart' => true,
|
|
'regex' => false,
|
|
'caseInsensitive' => true },
|
|
'columns' => [],
|
|
'assigned' => 'all',
|
|
'ColReorder' => [*0..6]
|
|
}
|
|
7.times do
|
|
SAMPLES_TABLE_DEFAULT_STATE['columns'] << {
|
|
'visible' => true,
|
|
'search' => { 'search' => '',
|
|
'smart' => true,
|
|
'regex' => false,
|
|
'caseInsensitive' => true }
|
|
}
|
|
end
|
|
SAMPLES_TABLE_DEFAULT_STATE.freeze
|
|
|
|
def initialize(view,
|
|
team,
|
|
project = nil,
|
|
my_module = nil,
|
|
experiment = nil,
|
|
user = nil)
|
|
super(view)
|
|
@team = team
|
|
@project = project
|
|
@my_module = my_module
|
|
@experiment = experiment
|
|
@user = user
|
|
end
|
|
|
|
# Define sortable columns, so 1st column will be sorted
|
|
# by attribute in sortable_columns[0]
|
|
def sortable_columns
|
|
sort_array = [
|
|
ASSIGNED_SORT_COL,
|
|
'Sample.name',
|
|
'SampleType.name',
|
|
'SampleGroup.name',
|
|
'Sample.created_at',
|
|
'User.full_name'
|
|
]
|
|
|
|
sort_array.push(*custom_fields_sort_by)
|
|
|
|
@sortable_columns = sort_array
|
|
end
|
|
|
|
# Define attributes on which we perform search
|
|
def searchable_columns
|
|
search_array = [
|
|
'Sample.name',
|
|
'SampleType.name',
|
|
'SampleGroup.name',
|
|
'Sample.created_at',
|
|
'User.full_name'
|
|
]
|
|
|
|
search_array.push(*custom_fields_sort_by)
|
|
@searchable_columns ||= filter_search_array search_array
|
|
end
|
|
|
|
private
|
|
|
|
# filters the search array by checking if the the column is visible
|
|
def filter_search_array(input_array)
|
|
param_index = 2
|
|
filtered_array = []
|
|
input_array.each do |col|
|
|
next if dt_params.to_a[param_index].nil?
|
|
params_col =
|
|
columns_params.to_a.find { |v| v[1]['data'] == param_index.to_s }
|
|
filtered_array.push(col) unless params_col[1]['searchable'] == 'false'
|
|
param_index += 1
|
|
end
|
|
filtered_array
|
|
end
|
|
|
|
# Get array of columns to sort by (for custom fields)
|
|
def custom_fields_sort_by
|
|
num_cf = CustomField.where(team_id: @team).count
|
|
array = []
|
|
|
|
num_cf.times do
|
|
array << 'SampleCustomField.value'
|
|
end
|
|
array
|
|
end
|
|
|
|
# Returns json of current samples (already paginated)
|
|
def data
|
|
records.map do |record|
|
|
sample = {
|
|
'DT_RowId': record.id,
|
|
'1': assigned_cell(record),
|
|
'2': escape_input(record.name),
|
|
'3': if record.sample_type.nil?
|
|
I18n.t('samples.table.no_type')
|
|
else
|
|
escape_input(record.sample_type.name)
|
|
end,
|
|
'4': sample_group_cell(record),
|
|
'5': I18n.l(record.created_at, format: :full),
|
|
'6': escape_input(record.user.full_name),
|
|
'sampleInfoUrl':
|
|
Rails.application.routes.url_helpers.sample_path(record.id),
|
|
'sampleEditUrl':
|
|
Rails.application.routes.url_helpers.edit_sample_path(record.id),
|
|
'sampleUpdateUrl':
|
|
Rails.application.routes.url_helpers.sample_path(record.id)
|
|
}
|
|
|
|
# Add custom attributes
|
|
record.sample_custom_fields.each do |scf|
|
|
sample[@cf_mappings[scf.custom_field_id]] =
|
|
custom_auto_link(scf.value, simple_format: true, team: @team)
|
|
end
|
|
sample
|
|
end
|
|
end
|
|
|
|
def assigned_cell(record)
|
|
if @assigned_samples.include?(record)
|
|
"<span class='circle'> </span>"
|
|
else
|
|
"<span class='circle disabled'> </span>"
|
|
end
|
|
end
|
|
|
|
def sample_group_cell(record)
|
|
if record.sample_group.nil?
|
|
"<span class='fas fa-asterisk'></span> " \
|
|
"#{I18n.t('samples.table.no_group')}"
|
|
else
|
|
"<span class='fas fa-asterisk' " \
|
|
"style='color: #{escape_input(record.sample_group.color)}'></span> " \
|
|
"#{escape_input(record.sample_group.name)}"
|
|
end
|
|
end
|
|
|
|
# Query database for records (this will be later paginated and filtered)
|
|
# after that "data" function will return json
|
|
def get_raw_records
|
|
samples = Sample
|
|
.includes(
|
|
:sample_type,
|
|
:sample_group,
|
|
:user,
|
|
:sample_custom_fields
|
|
)
|
|
.references(
|
|
:sample_type,
|
|
:sample_group,
|
|
:user,
|
|
:sample_custom_fields
|
|
)
|
|
.where(
|
|
team: @team
|
|
)
|
|
|
|
if @my_module
|
|
@assigned_samples = @my_module.samples
|
|
|
|
samples = samples.joins("LEFT OUTER JOIN sample_my_modules ON
|
|
(samples.id = sample_my_modules.sample_id AND
|
|
(sample_my_modules.my_module_id =
|
|
#{@my_module.id} OR
|
|
sample_my_modules.id IS NULL))")
|
|
.references(:sample_my_modules)
|
|
if dt_params[:assigned] == 'assigned'
|
|
samples = samples.where('"sample_my_modules"."id" > 0')
|
|
end
|
|
elsif @project
|
|
@assigned_samples = @project.assigned_samples
|
|
ids = @project.my_modules_ids
|
|
|
|
if ids.blank?
|
|
samples = samples.joins('LEFT OUTER JOIN sample_my_modules ON
|
|
(samples.id = sample_my_modules.sample_id AND
|
|
sample_my_modules.id IS NULL)')
|
|
.references(:sample_my_modules)
|
|
else
|
|
samples = samples.joins("LEFT OUTER JOIN sample_my_modules ON
|
|
(samples.id = sample_my_modules.sample_id AND
|
|
(sample_my_modules.my_module_id IN (#{ids}) OR
|
|
sample_my_modules.id IS NULL))")
|
|
.references(:sample_my_modules)
|
|
end
|
|
if dt_params[:assigned] == 'assigned'
|
|
samples = samples.joins('LEFT OUTER JOIN "my_modules" ON
|
|
"my_modules"."id" =
|
|
"sample_my_modules"."my_module_id"')
|
|
.joins('LEFT OUTER JOIN "experiments" ON
|
|
"experiments"."id" =
|
|
"my_modules"."experiment_id"')
|
|
.where('"experiments"."project_id" = ?', @project.id)
|
|
.where('"my_modules"."nr_of_assigned_samples" > 0')
|
|
end
|
|
elsif @experiment
|
|
@assigned_samples = @experiment.assigned_samples
|
|
ids = @experiment.my_modules.select(:id)
|
|
|
|
samples = samples.joins("LEFT OUTER JOIN sample_my_modules ON
|
|
(samples.id = sample_my_modules.sample_id AND
|
|
(sample_my_modules.my_module_id IN (#{ids.to_sql}) OR
|
|
sample_my_modules.id IS NULL))")
|
|
.references(:sample_my_modules)
|
|
if dt_params[:assigned] == 'assigned'
|
|
samples = samples.joins('LEFT OUTER JOIN "my_modules" ON
|
|
"my_modules"."id" =
|
|
"sample_my_modules"."my_module_id"')
|
|
.where('"my_modules"."experiment_id" = ?',
|
|
@experiment.id)
|
|
.where('"my_modules"."nr_of_assigned_samples" > 0')
|
|
end
|
|
end
|
|
|
|
# Make mappings of custom fields, so we have same id for every column
|
|
i = 7
|
|
@cf_mappings = {}
|
|
all_custom_fields.each do |cf|
|
|
@cf_mappings[cf.id] = i.to_s
|
|
i += 1
|
|
end
|
|
|
|
samples
|
|
end
|
|
|
|
# Override default behaviour
|
|
# Don't filter and paginate records when sorting by custom column - everything
|
|
# is done in sort_records method - you might ask why, well if you want the
|
|
# number of samples/all samples it's dependant upon sort_record query
|
|
def fetch_records
|
|
records = get_raw_records
|
|
records = sort_records(records) if order_params.present?
|
|
escape_special_chars
|
|
unless sorting_by_custom_column
|
|
records = filter_records(records) if dt_params[:search].present?
|
|
records = paginate_records(records) unless dt_params[:length].present? &&
|
|
dt_params[:length] == '-1'
|
|
end
|
|
records
|
|
end
|
|
|
|
# Override default sort method if needed
|
|
def sort_records(records)
|
|
if sort_column(order_params) == ASSIGNED_SORT_COL
|
|
# If "assigned" column is sorted
|
|
if @my_module
|
|
# Depending on the sort, order nulls first or
|
|
# nulls last on sample_my_modules association
|
|
records.order(
|
|
"sample_my_modules.id NULLS #{sort_null_direction(order_params)}"
|
|
)
|
|
elsif @experiment
|
|
# A very elegant solution to sort assigned samples at a experiment level
|
|
# grabs the ids of samples which has a modules that belongs
|
|
# to this project
|
|
assigned =
|
|
Sample
|
|
.joins('LEFT OUTER JOIN "sample_my_modules" ' \
|
|
'ON "sample_my_modules"."sample_id" = "samples"."id"')
|
|
.joins('LEFT OUTER JOIN "my_modules" ' \
|
|
'ON "my_modules"."id" = "sample_my_modules"."my_module_id"')
|
|
.where('"my_modules"."experiment_id" = ?', @experiment.id)
|
|
.where('"my_modules"."nr_of_assigned_samples" > 0')
|
|
.select('"samples"."id"')
|
|
.distinct
|
|
|
|
# grabs the ids that are not the previous one but are still
|
|
# of the same team
|
|
unassigned = Sample.where('"samples"."team_id" = ?', @team.id)
|
|
.where('"samples"."id" NOT IN (?)', assigned)
|
|
.select('"samples"."id"')
|
|
.distinct
|
|
|
|
# check the input param and merge the two arrays of ids
|
|
if order_params['dir'] == 'asc'
|
|
ids = assigned + unassigned
|
|
elsif order_params['dir'] == 'desc'
|
|
ids = unassigned + assigned
|
|
end
|
|
ids = ids.collect(&:id)
|
|
|
|
# order the records by input ids
|
|
order_by_index = ActiveRecord::Base.__send__(
|
|
:sanitize_sql_array,
|
|
["position((',' || samples.id || ',') in ?)", ids.join(',') + ',']
|
|
)
|
|
|
|
records.where(id: ids).order(order_by_index)
|
|
elsif @project
|
|
# A very elegant solution to sort assigned samples at a project level
|
|
# grabs the ids of samples which has a modules
|
|
# that belongs to this project
|
|
assigned =
|
|
Sample
|
|
.joins('LEFT OUTER JOIN "sample_my_modules" ' \
|
|
'ON "sample_my_modules"."sample_id" = "samples"."id"')
|
|
.joins('LEFT OUTER JOIN "my_modules" ' \
|
|
'ON "my_modules"."id" = "sample_my_modules"."my_module_id"')
|
|
.joins('LEFT OUTER JOIN "experiments" ' \
|
|
'ON "experiments"."id" = "my_modules"."experiment_id"')
|
|
.where('"experiments"."project_id" = ?', @project.id)
|
|
.where('"my_modules"."nr_of_assigned_samples" > 0')
|
|
.select('"samples"."id"')
|
|
.distinct
|
|
|
|
# grabs the ids that are not the previous ones
|
|
# but are still of the same team
|
|
unassigned = Sample
|
|
.where('"samples"."team_id" = ?', @team.id)
|
|
.where('"samples"."id" NOT IN (?)', assigned)
|
|
.select('"samples"."id"')
|
|
.distinct
|
|
|
|
# check the input param and merge the two arrays of ids
|
|
if order_params['dir'] == 'asc'
|
|
ids = assigned + unassigned
|
|
elsif order_params['dir'] == 'desc'
|
|
ids = unassigned + assigned
|
|
end
|
|
ids = ids.collect(&:id)
|
|
|
|
# order the records by input ids
|
|
order_by_index = ActiveRecord::Base.__send__(
|
|
:sanitize_sql_array,
|
|
["position((',' || samples.id || ',') in ?)", ids.join(',') + ',']
|
|
)
|
|
records.where(id: ids).order(order_by_index)
|
|
end
|
|
elsif sorting_by_custom_column
|
|
# Check if have to filter samples first
|
|
if dt_params[:search].present? && dt_params[:search][:value].present?
|
|
# Couldn't force ActiveRecord to yield the same query as below because
|
|
# Rails apparently forgets to join stuff in subqueries -
|
|
# #justrailsthings
|
|
conditions = build_conditions_for(dt_params[:search][:value])
|
|
filter_query = %(SELECT "samples"."id" FROM "samples"
|
|
LEFT OUTER JOIN "sample_custom_fields" ON
|
|
"sample_custom_fields"."sample_id" = "samples"."id"
|
|
LEFT OUTER JOIN "sample_types" ON
|
|
"sample_types"."id" = "samples"."sample_type_id"
|
|
LEFT OUTER JOIN "sample_groups"
|
|
ON "sample_groups"."id" = "samples"."sample_group_id"
|
|
LEFT OUTER JOIN "users" ON "users"."id" = "samples"."user_id"
|
|
WHERE "samples"."team_id" = #{@team.id} AND #{conditions.to_sql})
|
|
|
|
records = records.where("samples.id IN (#{filter_query})")
|
|
end
|
|
|
|
ci = sortable_displayed_columns[
|
|
order_params[:column].to_i - 1
|
|
]
|
|
cf_id = @cf_mappings.key((ci.to_i + 1).to_s)
|
|
dir = sort_direction(order_params)
|
|
|
|
# Because samples can have multiple sample custom fields, we first group
|
|
# them by samples.id and inside that group we sort them by cf_id. Because
|
|
# we sort them ASC, sorted columns will be on top. Distinct then only
|
|
# takes the first row and cuts the rest of every group and voila we have
|
|
# 1 row for every sample, which are not sorted yet ...
|
|
records =
|
|
records
|
|
.select('DISTINCT ON (samples.id) *')
|
|
.order("samples.id, CASE WHEN sample_custom_fields.custom_field_id = " \
|
|
"#{cf_id} THEN 1 ELSE 2 END ASC")
|
|
|
|
# ... this little gem (pun intended) then takes the records query,
|
|
# sorts it again and paginates it.
|
|
# sq.t0_* are determined empirically and are crucial -
|
|
# imagine A -> B -> C transitive relation but where A and C are the
|
|
# same. Useless right? But not when you acknowledge that find_by_sql
|
|
# method does some funky stuff when your query spans multiple queries -
|
|
# Sample object might have id from SampleType, name from
|
|
# User ... chaos ensues basically. If something changes in db this might
|
|
# change.
|
|
formated_date = I18n.t('time.formats.datatables_date')
|
|
.gsub!(/^\"|\"?$/, '')
|
|
Sample.find_by_sql(
|
|
"SELECT sq.t0_r0 as id, sq.t0_r1 as name, " \
|
|
"to_char( sq.t0_r4, '#{formated_date}' ) as created_at, sq.t0_r5, " \
|
|
"sq.t0_r6 as sample_group_id ,sq.t0_r7 as sample_type_id, sq.t0_r2 " \
|
|
"as user_id, sq.custom_field_id FROM (#{records.to_sql}) " \
|
|
"as sq ORDER BY CASE WHEN sq.custom_field_id = #{cf_id} THEN 1 " \
|
|
"ELSE 2 END #{dir}, sq.value #{dir} LIMIT #{per_page} OFFSET #{offset}"
|
|
)
|
|
else
|
|
super(records)
|
|
end
|
|
end
|
|
|
|
# A hack that overrides the new_search_contition method default behavior
|
|
# of the ajax-datatables-rails gem now the method checks
|
|
# if the column is the created_at and generate a custom SQL to parse
|
|
# it back to the caller method
|
|
def new_search_condition(column, value)
|
|
model, column = column.split('.')
|
|
model = model.constantize
|
|
formated_date = I18n.t('time.formats.datatables_date')
|
|
.gsub!(/^\"|\"?$/, '')
|
|
if model == SampleCustomField
|
|
# Find visible (searchable) custom field IDs, and only perform filter
|
|
# on those custom fields
|
|
searchable_cfs = columns_params.select do |_, v|
|
|
v['searchable'] == 'true' && @cf_mappings.values.include?(v['data'])
|
|
end
|
|
cfmi = @cf_mappings.invert
|
|
cf_ids = searchable_cfs.map { |_, v| cfmi[v['data']] }
|
|
|
|
# Do an ILIKE on 'value', as well as make sure to only include
|
|
# custom fields that have 'custom_field_id' among visible custom fields
|
|
casted_column = ::Arel::Nodes::NamedFunction.new(
|
|
'CAST',
|
|
[model.arel_table[column.to_sym].as(typecast)]
|
|
)
|
|
casted_column = casted_column.matches("%#{value}%")
|
|
casted_column = casted_column.and(
|
|
model.arel_table['custom_field_id'].in(cf_ids)
|
|
)
|
|
casted_column
|
|
elsif column == 'created_at'
|
|
casted_column = ::Arel::Nodes::NamedFunction.new(
|
|
'CAST',
|
|
[Arel.sql(
|
|
"to_char( samples.created_at, '#{formated_date}' ) AS VARCHAR"
|
|
)]
|
|
)
|
|
casted_column.matches("%#{sanitize_sql_like(value)}%")
|
|
else
|
|
casted_column = ::Arel::Nodes::NamedFunction.new(
|
|
'CAST',
|
|
[model.arel_table[column.to_sym].as(typecast)]
|
|
)
|
|
casted_column.matches("%#{sanitize_sql_like(value)}%")
|
|
end
|
|
end
|
|
|
|
def sort_null_direction(item)
|
|
val = sort_direction(item)
|
|
val == 'ASC' ? 'LAST' : 'FIRST'
|
|
end
|
|
|
|
def inverse_sort_direction(item)
|
|
val = sort_direction(item)
|
|
val == 'ASC' ? 'DESC' : 'ASC'
|
|
end
|
|
|
|
def sorting_by_custom_column
|
|
sort_column(order_params) == 'sample_custom_fields.value'
|
|
end
|
|
|
|
# Escapes special characters in search query
|
|
def escape_special_chars
|
|
if dt_params[:search].present?
|
|
dt_params[:search][:value] =
|
|
ActiveRecord::Base.__send__(:sanitize_sql_like,
|
|
dt_params[:search][:value])
|
|
end
|
|
end
|
|
|
|
def new_sort_column(item)
|
|
coli = item[:column].to_i - 1
|
|
model, column = sortable_columns[sortable_displayed_columns[coli].to_i]
|
|
.split('.')
|
|
|
|
return model if model == ASSIGNED_SORT_COL
|
|
[model.constantize.table_name, column].join('.')
|
|
end
|
|
|
|
def generate_sortable_displayed_columns
|
|
sort_order = SamplesTable.find_status(@user, @team)['ColReorder']
|
|
|
|
sort_order.shift
|
|
sort_order.map! { |i| (i.to_i - 1).to_s }
|
|
|
|
@sortable_displayed_columns = sort_order
|
|
end
|
|
end
|