require 'active_record'
class RepositoryDatatable < AjaxDatatablesRails::Base
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'.freeze
REPOSITORY_TABLE_DEFAULT_STATE = {
'time' => 0,
'start' => 0,
'length' => 5,
'order' => [[2, 'desc']],
'search' => { 'search' => '',
'smart' => true,
'regex' => false,
'caseInsensitive' => true },
'columns' => [],
'assigned' => 'assigned',
'ColReorder' => [*0..4]
}
5.times do
REPOSITORY_TABLE_DEFAULT_STATE['columns'] << {
'visible' => true,
'search' => { 'search' => '',
'smart' => true,
'regex' => false,
'caseInsensitive' => true }
}
end
REPOSITORY_TABLE_DEFAULT_STATE.freeze
def initialize(view,
repository,
my_module = nil,
user = nil)
super(view)
@repository = repository
@team = repository.team
@my_module = my_module
@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,
'RepositoryRow.name',
'RepositoryRow.created_at',
'User.full_name'
]
sort_array.push(*repository_columns_sort_by)
@sortable_columns = sort_array
end
# Define attributes on which we perform search
def searchable_columns
search_array = [
'RepositoryRow.name',
'RepositoryRow.created_at',
'User.full_name'
]
# search_array.push(*repository_columns_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 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
end
filtered_array
end
# Get array of columns to sort by (for custom columns)
def repository_columns_sort_by
array = []
@repository.repository_columns.count.times do
array << 'RepositoryCell.value'
end
array
end
# Returns json of current repository rows (already paginated)
def data
records.map do |record|
row = {
'DT_RowId': record.id,
'1': @my_module ? assigned_row(record) : '',
'2': escape_input(record.name),
'3': I18n.l(record.created_at, format: :full),
'4': escape_input(record.created_by.full_name),
'recordEditUrl':
Rails.application.routes.url_helpers
.edit_repository_repository_row_path(@repository,
record.id),
'recordUpdateUrl':
Rails.application.routes.url_helpers
.repository_repository_row_path(@repository, record.id)
}
# Add custom columns
record.repository_cells.each do |cell|
row[@columns_mappings[cell.repository_column.id]] =
custom_auto_link(cell.value.data,
simple_format: true,
team: @team)
end
row
end
end
def assigned_row(record)
if @assigned_rows && @assigned_rows.include?(record)
" "
else
" "
end
end
# Query database for records (this will be later paginated and filtered)
# after that "data" function will return json
def get_raw_records
repository_rows = RepositoryRow
.preload(
:repository_columns,
:created_by,
repository_cells: :value
)
.joins(:created_by)
.where(repository: @repository)
if @my_module
@assigned_rows = @my_module.repository_rows
.preload(
:repository_columns,
:created_by,
repository_cells: :value
)
.joins(:created_by)
.where(repository: @repository)
end
# Make mappings of custom columns, so we have same id for every column
i = 5
@columns_mappings = {}
@repository.repository_columns.each do |column|
@columns_mappings[column.id] = i.to_s
i += 1
end
repository_rows
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 = @assigned_rows if @my_module && params[:assigned] == 'assigned'
records = filter_records(records) if params[:search].present? &&
!sorting_by_custom_column
records = sort_records(records) if params[:order].present?
records = paginate_records(records) if !(params[:length].present? &&
params[:length] == '-1') &&
!sorting_by_custom_column
escape_special_chars
records
end
# Overriden to make it work for custom columns, because they are polymorphic
# NOTE: Function assumes the provided records/rows are only from the current
# repository!
def simple_search(repo_rows)
return repo_rows unless params[:search].present? &&
params[:search][:value].present?
search_val = params[:search][:value]
filtered_rows = repo_rows.select do |r|
row_cells = [r.name, r.created_at.strftime(Constants::DATE_FORMAT),
r.created_by.full_name]
row_cells.push(*r.repository_cells.collect { |c| c.value.data })
row_cells.any? { |c| c.include?(search_val) }
end
repo_rows.where(id: filtered_rows)
end
# Override default sort method if needed
def sort_records(records)
if params[:order].present? && params[:order].length == 1
if sort_column(params[:order].values[0]) == ASSIGNED_SORT_COL
# If "assigned" column is sorted
if @my_module
# Depending on the sort, order nulls first or
# nulls last on repository_cells association
direction = sort_null_direction(params[:order].values[0])
records.joins(
"LEFT OUTER JOIN my_module_repository_rows ON
(repository_rows.id = my_module_repository_rows.repository_row_id
AND (my_module_repository_rows.my_module_id = #{@my_module.id} OR
my_module_repository_rows.id IS NULL))"
).order("my_module_repository_rows.id NULLS #{direction}")
end
elsif sorting_by_custom_column
# Check if have to filter records first
# if params[:search].present? && 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])
#
# filter_query = %(SELECT "samples"."id" FROM "samples"
# LEFT OUTER JOIN "sample_custom_fields" ON
# "sample_custom_fields"."sample_id" = "samples"."id"
# LEFT OUTER JOIN "users" ON "users"."id" = "repository_row"."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[
params[:order].values[0][:column].to_i - 1
]
column_id = @columns_mappings.key((ci.to_i + 1).to_s)
dir = sort_direction(params[:order].values[0])
# Because repository records can have multiple custom cells,
# we first group them by samples.id and inside that group we sort them by column_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 (repository_rows.id) *')
# .order("repository_rows.id, CASE WHEN repository_cells.repository_column_id = #{column_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, s, sq.t0_r2 as user_id, sq.custom_field_id FROM (#{records.to_sql})
# as sq ORDER BY CASE WHEN sq.custom_field_id = #{column_id} THEN 1 ELSE 2 END #{dir}, sq.value #{dir}
# LIMIT #{per_page} OFFSET #{offset}")
RepositoryRow.find_by_sql(
"SELECT repository_rows.*, values.value AS value
FROM repository_rows
LEFT OUTER JOIN (SELECT repository_cells.*,
repository_text_values.data AS value FROM repository_cells
INNER JOIN repository_text_values
ON repository_text_values.id = repository_cells.value_id
WHERE repository_cells.repository_column_id = #{column_id}) AS values
ON values.repository_row_id = repository_rows.id
WHERE repository_rows.repository_id = #{@repository.id}
ORDER BY value #{dir} LIMIT #{per_page} OFFSET #{offset}"
)
else
super(records)
end
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 = 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'
# 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(params[:order].values[0]) == 'repository_cells.value'
end
# Escapes special characters in search query
def escape_special_chars
if params[:search].present?
params[:search][:value] = ActiveRecord::Base
.__send__(:sanitize_sql_like,
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 = RepositoryTableState.load_state(@user, @repository)
.first['ColReorder']
sort_order.shift
sort_order.map! { |i| (i.to_i - 1).to_s }
@sortable_displayed_columns = sort_order
end
end