2016-07-21 19:11:15 +08:00
require 'active_record'
2016-02-12 23:52:43 +08:00
class SampleDatatable < AjaxDatatablesRails :: Base
2016-11-23 21:18:50 +08:00
include ActionView :: Helpers :: TextHelper
2016-02-12 23:52:43 +08:00
include SamplesHelper
2017-01-04 22:04:12 +08:00
include InputSanitizeHelper
2017-01-10 21:13:37 +08:00
include Rails . application . routes . url_helpers
include ActionView :: Helpers :: UrlHelper
include ApplicationHelper
2017-04-25 23:37:42 +08:00
include ActiveRecord :: Sanitization :: ClassMethods
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' = > [ ] ,
2017-06-06 18:46:56 +08:00
'assigned' = > 'all' ,
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
2016-08-03 18:57:51 +08:00
def initialize ( view ,
2017-01-25 16:48:49 +08:00
team ,
2016-08-03 19:11:46 +08:00
project = nil ,
my_module = nil ,
2016-12-21 21:32:28 +08:00
experiment = nil ,
user = nil )
2016-02-12 23:52:43 +08:00
super ( view )
2017-01-25 16:48:49 +08:00
@team = team
2016-02-12 23:52:43 +08:00
@project = project
@my_module = my_module
2016-08-03 18:57:51 +08:00
@experiment = experiment
2016-12-21 21:32:28 +08:00
@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 ,
2016-12-21 21:32:28 +08:00
'Sample.name' ,
'SampleType.name' ,
'SampleGroup.name' ,
'Sample.created_at' ,
'User.full_name'
2016-02-12 23:52:43 +08:00
]
2016-12-21 21:32:28 +08:00
2016-02-12 23:52:43 +08:00
sort_array . push ( * custom_fields_sort_by )
2016-12-21 21:32:28 +08:00
@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 = [
2016-12-21 21:32:28 +08:00
'Sample.name' ,
'SampleType.name' ,
'SampleGroup.name' ,
'Sample.created_at' ,
'User.full_name'
2016-02-12 23:52:43 +08:00
]
2016-12-21 21:32:28 +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
2016-12-21 21:32:28 +08:00
def filter_search_array ( input_array )
2016-07-21 19:11:15 +08:00
param_index = 2
2016-12-24 02:56:54 +08:00
filtered_array = [ ]
2016-07-21 19:11:15 +08:00
input_array . each do | col |
2016-12-24 02:56:54 +08:00
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
2017-01-25 16:48:49 +08:00
num_cf = CustomField . where ( team_id : @team ) . count
2016-02-12 23:52:43 +08:00
array = [ ]
2017-01-11 20:21:38 +08:00
num_cf . times do
2016-12-21 21:32:28 +08:00
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 = {
2016-12-21 21:32:28 +08:00
'DT_RowId' : record . id ,
'1' : assigned_cell ( record ) ,
2017-01-11 22:50:11 +08:00
'2' : escape_input ( record . name ) ,
2017-01-04 22:04:12 +08:00
'3' : if record . sample_type . nil?
I18n . t ( 'samples.table.no_type' )
else
2017-01-11 22:50:11 +08:00
escape_input ( record . sample_type . name )
2017-01-04 22:04:12 +08:00
end ,
2017-01-16 22:58:37 +08:00
'4' : sample_group_cell ( record ) ,
2017-01-04 22:04:12 +08:00
'5' : I18n . l ( record . created_at , format : :full ) ,
2017-01-11 22:50:11 +08:00
'6' : escape_input ( record . user . full_name ) ,
2017-01-04 22:04:12 +08:00
'sampleInfoUrl' :
2017-01-16 22:58:37 +08:00
Rails . application . routes . url_helpers . sample_path ( record . id ) ,
2017-01-20 18:05:05 +08:00
'sampleEditUrl' :
2017-01-04 22:04:12 +08:00
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 |
2017-04-19 15:11:52 +08:00
sample [ @cf_mappings [ scf . custom_field_id ] ] =
custom_auto_link ( scf . value , simple_format : true , team : @team )
2016-02-12 23:52:43 +08:00
end
sample
end
end
def assigned_cell ( record )
@assigned_samples . include? ( record ) ?
" <span class='circle'> </span> " :
" <span class='circle disabled'> </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' " \
2017-01-24 21:33:23 +08:00
" 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 (
2017-01-25 16:48:49 +08:00
team : @team
2016-02-12 23:52:43 +08:00
)
if @my_module
@assigned_samples = @my_module . samples
2016-08-03 18:57:51 +08:00
2016-08-03 19:16:51 +08:00
samples = samples . joins ( " LEFT OUTER JOIN sample_my_modules ON
2017-06-06 18:46:56 +08:00
( 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 params [ :assigned ] == 'assigned'
samples = samples . where ( '"sample_my_modules"."id" > 0' )
end
2016-02-12 23:52:43 +08:00
elsif @project
@assigned_samples = @project . assigned_samples
2016-08-03 18:57:51 +08:00
ids = @project . my_modules_ids
2016-08-10 15:13:12 +08:00
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 )
2016-08-10 15:13:12 +08:00
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 )
2016-08-10 15:13:12 +08:00
end
2017-06-06 18:46:56 +08:00
if 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
2016-08-03 18:57:51 +08:00
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 )
2017-06-06 18:46:56 +08:00
if 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
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?
2016-08-12 17:08:51 +08:00
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 )
2016-12-21 21:32:28 +08:00
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 ] ) } " )
2016-08-03 18:57:51 +08:00
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
2016-08-03 18:57:51 +08:00
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
2016-08-03 18:57:51 +08:00
# 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'
2016-08-03 18:57:51 +08:00
ids = assigned + unassigned
2016-08-03 19:11:46 +08:00
elsif params [ :order ] . values [ 0 ] [ 'dir' ] == 'desc'
2016-08-03 18:57:51 +08:00
ids = unassigned + assigned
end
2016-08-03 19:11:46 +08:00
ids = ids . collect ( & :id )
2016-08-03 18:57:51 +08:00
# order the records by input ids
order_by_index = ActiveRecord :: Base . send (
:sanitize_sql_array ,
[ " position((',' || samples.id || ',') in ?) " ,
ids . join ( ',' ) + ',' ] )
2016-12-21 21:32:28 +08:00
2016-08-03 18:57:51 +08:00
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"' )
2016-08-03 18:57:51 +08:00
. 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
2017-01-25 16:48:49 +08:00
# 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
2017-06-06 18:46:56 +08:00
if params [ :order ] . values [ 0 ] [ 'dir' ] == 'asc'
2016-07-21 19:11:15 +08:00
ids = assigned + unassigned
2017-06-06 18:46:56 +08:00
elsif params [ :order ] . values [ 0 ] [ 'dir' ] == 'desc'
2016-07-21 19:11:15 +08:00
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
2017-01-11 20:21:38 +08:00
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-12-21 21:32:28 +08:00
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! ( / ^ \ "| \ "?$ / , '' )
2017-01-14 00:03:19 +08:00
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 " ) ] )
2017-04-25 23:37:42 +08:00
casted_column . matches ( " % #{ sanitize_sql_like ( 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 ) ] )
2017-04-25 23:37:42 +08:00
casted_column . matches ( " % #{ sanitize_sql_like ( 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
2017-01-11 20:21:38 +08:00
sort_column ( params [ :order ] . values [ 0 ] ) == 'sample_custom_fields.value'
2016-02-12 23:52:43 +08:00
end
2016-08-12 17:08:51 +08:00
# 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
2016-12-21 21:32:28 +08:00
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
2016-12-21 21:32:28 +08:00
col = [ model . constantize . table_name , column ] . join ( '.' )
end
def generate_sortable_displayed_columns
sort_order = SamplesTable . where ( user : @user ,
2017-01-25 16:48:49 +08:00
team : @team )
2016-12-21 21:32:28 +08:00
. 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