scinote-web/app/datatables/sample_datatable.rb

447 lines
17 KiB
Ruby
Raw Normal View History

2016-07-21 19:11:15 +08:00
require 'active_record'
2016-02-12 23:52:43 +08:00
class SampleDatatable < AjaxDatatablesRails::Base
include ActionView::Helpers::TextHelper
2016-02-12 23:52:43 +08:00
include SamplesHelper
include InputSanitizeHelper
2017-01-10 21:13:37 +08:00
include Rails.application.routes.url_helpers
include ActionView::Helpers::UrlHelper
include ApplicationHelper
2016-02-12 23:52:43 +08:00
2016-12-07 21:51:34 +08:00
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' => [],
2016-12-08 21:41:35 +08:00
'ColReorder' => [*0..6]
2016-12-07 21:51:34 +08:00
}
7.times do
SAMPLES_TABLE_DEFAULT_STATE['columns'] << {
'visible' => true,
'search' => { 'search' => '',
'smart' => true,
'regex' => false,
'caseInsensitive' => true }
}
end
SAMPLES_TABLE_DEFAULT_STATE.freeze
2016-02-12 23:52:43 +08:00
def initialize(view,
team,
2016-08-03 19:11:46 +08:00
project = nil,
my_module = nil,
experiment = nil,
user = nil)
2016-02-12 23:52:43 +08:00
super(view)
@team = team
2016-02-12 23:52:43 +08:00
@project = project
@my_module = my_module
@experiment = experiment
@user = user
2016-02-12 23:52:43 +08:00
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'
2016-02-12 23:52:43 +08:00
]
2016-02-12 23:52:43 +08:00
sort_array.push(*custom_fields_sort_by)
@sortable_columns = sort_array
2016-02-12 23:52:43 +08:00
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'
2016-02-12 23:52:43 +08:00
]
2016-02-12 23:52:43 +08:00
search_array.push(*custom_fields_sort_by)
2016-07-21 19:11:15 +08:00
@searchable_columns ||= filter_search_array search_array
2016-02-12 23:52:43 +08:00
end
private
2016-07-21 19:11:15 +08:00
# filters the search array by checking if the the column is visible
def filter_search_array(input_array)
2016-07-21 19:11:15 +08:00
param_index = 2
filtered_array = []
2016-07-21 19:11:15 +08:00
input_array.each do |col|
next if params[:columns].to_a[param_index].nil?
params_col =
params[:columns].to_a.find { |v| v[1]['data'] == param_index.to_s }
filtered_array.push(col) unless params_col[1]['searchable'] == 'false'
param_index += 1
2016-07-21 19:11:15 +08:00
end
filtered_array
end
2016-02-12 23:52:43 +08:00
# Get array of columns to sort by (for custom fields)
def custom_fields_sort_by
num_cf = CustomField.where(team_id: @team).count
2016-02-12 23:52:43 +08:00
array = []
num_cf.times do
array << 'SampleCustomField.value'
2016-02-12 23:52:43 +08:00
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,
2017-01-16 22:58:37 +08:00
'4': sample_group_cell(record),
'5': I18n.l(record.created_at, format: :full),
'6': escape_input(record.user.full_name),
'sampleInfoUrl':
2017-01-16 22:58:37 +08:00
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)
2016-02-12 23:52:43 +08:00
}
# Add custom attributes
record.sample_custom_fields.each do |scf|
sample[@cf_mappings[scf.custom_field_id]] = custom_auto_link(scf.value,
true,
@team)
2016-02-12 23:52:43 +08:00
end
sample
end
end
def assigned_cell(record)
@assigned_samples.include?(record) ?
"<span class='circle'>&nbsp;</span>" :
"<span class='circle disabled'>&nbsp;</span>"
end
2017-01-16 22:58:37 +08:00
def sample_group_cell(record)
if record.sample_group.nil?
"<span class='glyphicon glyphicon-asterisk'></span> " \
"#{I18n.t('samples.table.no_group')}"
else
"<span class='glyphicon glyphicon-asterisk' " \
"style='color: #{escape_input(record.sample_group.color)}'></span> " \
"#{escape_input(record.sample_group.name)}"
2017-01-16 22:58:37 +08:00
end
end
2016-02-12 23:52:43 +08:00
# 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
2016-02-12 23:52:43 +08:00
)
if @my_module
@assigned_samples = @my_module.samples
2016-08-03 19:16:51 +08:00
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.to_s} OR
sample_my_modules.id IS NULL))")
.references(:sample_my_modules)
2016-02-12 23:52:43 +08:00
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)')
2016-08-10 17:26:27 +08:00
.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))")
2016-08-10 17:26:27 +08:00
.references(:sample_my_modules)
end
elsif @experiment
@assigned_samples = @experiment.assigned_samples
ids = @experiment.my_modules.select(:id)
2016-08-03 19:16:51 +08:00
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)
2016-02-12 23:52:43 +08:00
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 params[:order].present?
escape_special_chars
2016-02-12 23:52:43 +08:00
records = filter_records(records) if params[:search].present? && (not (sorting_by_custom_column))
records = paginate_records(records) if (not (params[:length].present? && params[:length] == '-1')) && (not (sorting_by_custom_column))
records
end
# Override default sort method if needed
def sort_records(records)
if params[:order].present? && params[:order].length == 1
2016-02-12 23:52:43 +08:00
if sort_column(params[:order].values[0]) == ASSIGNED_SORT_COL
# If "assigned" column is sorted
if @my_module then
# 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(params[:order].values[0])}")
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
2017-01-31 20:33:55 +08:00
# grabs the ids that are not the previous one but are still
# of the same team
unassigned = Sample
2017-01-31 20:33:55 +08:00
.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
2016-08-03 19:11:46 +08:00
if params[:order].values[0]['dir'] == 'asc'
ids = assigned + unassigned
2016-08-03 19:11:46 +08:00
elsif params[:order].values[0]['dir'] == 'desc'
ids = unassigned + assigned
end
2016-08-03 19:11:46 +08:00
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)
2016-02-12 23:52:43 +08:00
elsif @project
2016-07-21 19:11:15 +08:00
# 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)
2016-07-21 19:11:15 +08:00
.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
2016-07-21 19:11:15 +08:00
unassigned = Sample
2017-01-25 22:40:35 +08:00
.where('"samples"."team_id" = ?', @team.id)
.where('"samples"."id" NOT IN (?)', assigned)
.select('"samples"."id"')
.distinct
2016-07-21 19:11:15 +08:00
# check the input param and merge the two arrays of ids
if params[:order].values[0]["dir"] == "asc"
ids = assigned + unassigned
elsif params[:order].values[0]["dir"] == "desc"
ids = unassigned + assigned
end
ids = ids.collect { |s| s.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)
2016-02-12 23:52:43 +08:00
end
elsif sorting_by_custom_column
# Check if have to filter samples first
if params[:search].present? and 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(params[:search][:value])
2017-01-25 22:40:35 +08:00
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})
2016-02-12 23:52:43 +08:00
records = records.where("samples.id IN (#{filter_query})")
end
ci = sortable_displayed_columns[
params[:order].values[0][:column].to_i - 1
]
cf_id = @cf_mappings.key((ci.to_i + 1).to_s)
2016-02-12 23:52:43 +08:00
dir = sort_direction(params[:order].values[0])
# 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.
2016-07-21 19:11:15 +08:00
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})
2016-02-12 23:52:43 +08:00
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
else
super(records)
end
end
2016-07-21 19:11:15 +08:00
# 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 = params[:columns].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'
2016-07-21 19:11:15 +08:00
casted_column = ::Arel::Nodes::NamedFunction.new('CAST',
[ Arel.sql("to_char( samples.created_at, '#{ formated_date }' ) AS VARCHAR") ] )
casted_column.matches("%#{value}%")
2016-07-21 19:11:15 +08:00
else
casted_column = ::Arel::Nodes::NamedFunction.new('CAST',
[model.arel_table[column.to_sym].as(typecast)])
casted_column.matches("%#{value}%")
2016-07-21 19:11:15 +08:00
end
end
2016-02-12 23:52:43 +08:00
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(params[:order].values[0]) == 'sample_custom_fields.value'
2016-02-12 23:52:43 +08:00
end
# Escapes special characters in search query
def escape_special_chars
params[:search][:value] = ActiveRecord::Base
.send(:sanitize_sql_like,
params[:search][:value]) if params[:search]
.present?
end
def new_sort_column(item)
coli = item[:column].to_i - 1
model, column = sortable_columns[sortable_displayed_columns[coli].to_i]
.split('.')
2016-12-21 22:35:05 +08:00
return model if model == ASSIGNED_SORT_COL
col = [model.constantize.table_name, column].join('.')
end
def generate_sortable_displayed_columns
sort_order = SamplesTable.where(user: @user,
team: @team)
.pluck(:status)
.first['ColReorder']
sort_order.shift
sort_order.map! { |i| (i.to_i - 1).to_s }
@sortable_displayed_columns = sort_order
end
2016-07-21 19:11:15 +08:00
end