From b234c07c13942e73314532008bbdc852d5ad43e1 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Fri, 30 Jun 2017 15:20:27 +0200 Subject: [PATCH] Fix datatables [SCI-1409] --- app/assets/javascripts/application.js.erb | 1 - app/datatables/custom_datatable.rb | 40 ++ ...oad_from_repository_protocols_datatable.rb | 12 +- .../protocol_linked_children_datatable.rb | 2 +- app/datatables/protocols_datatable.rb | 12 +- app/datatables/repository_datatable.rb | 170 +++------ app/datatables/sample_datatable.rb | 353 ++++++++++-------- app/datatables/team_users_datatable.rb | 2 +- app/datatables/teams_datatable.rb | 18 +- app/models/my_module.rb | 2 +- .../shared/_secondary_navigation.html.erb | 2 +- 11 files changed, 300 insertions(+), 314 deletions(-) create mode 100644 app/datatables/custom_datatable.rb diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 240899728..fcea01b3b 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -60,7 +60,6 @@ function initFormSubmitLinks(el) { } /* Enable loading bars */ -Turbolinks.enableProgressBar(); $(document) .bind("ajaxSend", function(){ animateLoading(); diff --git a/app/datatables/custom_datatable.rb b/app/datatables/custom_datatable.rb new file mode 100644 index 000000000..c958000a0 --- /dev/null +++ b/app/datatables/custom_datatable.rb @@ -0,0 +1,40 @@ +class CustomDatatable < AjaxDatatablesRails::Base + private + + def dt_params + @dt_params ||= + params.permit(:assigned, :draw, :length, :start, search: %i(value regex)) + .to_h + end + + def columns_params + @columns_params ||= params.require(:columns).permit!.to_h + end + + def order_params + @order_params ||= + params.require(:order).require('0').permit(:column, :dir).to_h + end + + def fetch_records + records = get_raw_records + records = sort_records(records) if order_params.present? + records = filter_records(records) if dt_params[:search].present? + records = paginate_records(records) unless dt_params[:length].present? && + dt_params[:length] == '-1' + records + end + + def sort_records(records) + sort_by = "#{sort_column(order_params)} #{sort_direction(order_params)}" + records.order(sort_by) + end + + def generate_sortable_displayed_columns + @sortable_displayed_columns = [] + columns_params.each_value do |col| + @sortable_displayed_columns << col[:data] if col[:orderable] == 'true' + end + @sortable_displayed_columns + end +end diff --git a/app/datatables/load_from_repository_protocols_datatable.rb b/app/datatables/load_from_repository_protocols_datatable.rb index f26f32b64..99ee07c12 100644 --- a/app/datatables/load_from_repository_protocols_datatable.rb +++ b/app/datatables/load_from_repository_protocols_datatable.rb @@ -1,4 +1,4 @@ -class LoadFromRepositoryProtocolsDatatable < AjaxDatatablesRails::Base +class LoadFromRepositoryProtocolsDatatable < CustomDatatable # Needed for sanitize_sql_like method include ActiveRecord::Sanitization::ClassMethods include InputSanitizeHelper @@ -57,10 +57,10 @@ class LoadFromRepositoryProtocolsDatatable < AjaxDatatablesRails::Base # See https://github.com/antillas21/ajax-datatables-rails/issues/112 def as_json(options = {}) { - :draw => params[:draw].to_i, - :recordsTotal => get_raw_records.length, - :recordsFiltered => filter_records(get_raw_records).length, - :data => data + draw: dt_params[:draw].to_i, + recordsTotal: get_raw_records.length, + recordsFiltered: filter_records(get_raw_records).length, + data: data } end @@ -166,7 +166,7 @@ class LoadFromRepositoryProtocolsDatatable < AjaxDatatablesRails::Base def build_conditions_for(query) # Inner query to retrieve list of protocol IDs where concatenated # protocol keywords string, or user's full_name contains searched query - search_val = params[:search][:value] + search_val = dt_params[:search][:value] records_having = get_raw_records_base.having( ::Arel::Nodes::NamedFunction.new( 'CAST', diff --git a/app/datatables/protocol_linked_children_datatable.rb b/app/datatables/protocol_linked_children_datatable.rb index ad218ab0f..131526c99 100644 --- a/app/datatables/protocol_linked_children_datatable.rb +++ b/app/datatables/protocol_linked_children_datatable.rb @@ -1,4 +1,4 @@ -class ProtocolLinkedChildrenDatatable < AjaxDatatablesRails::Base +class ProtocolLinkedChildrenDatatable < CustomDatatable def_delegator :@view, :link_to def_delegator :@view, :protocols_my_module_path diff --git a/app/datatables/protocols_datatable.rb b/app/datatables/protocols_datatable.rb index 069a1418c..a15c2f689 100644 --- a/app/datatables/protocols_datatable.rb +++ b/app/datatables/protocols_datatable.rb @@ -1,4 +1,4 @@ -class ProtocolsDatatable < AjaxDatatablesRails::Base +class ProtocolsDatatable < CustomDatatable # Needed for sanitize_sql_like method include ActiveRecord::Sanitization::ClassMethods include InputSanitizeHelper @@ -48,10 +48,10 @@ class ProtocolsDatatable < AjaxDatatablesRails::Base # See https://github.com/antillas21/ajax-datatables-rails/issues/112 def as_json(options = {}) { - :draw => params[:draw].to_i, - :recordsTotal => get_raw_records.length, - :recordsFiltered => filter_records(get_raw_records).length, - :data => data + draw: dt_params[:draw].to_i, + recordsTotal: get_raw_records.length, + recordsFiltered: filter_records(get_raw_records).length, + data: data } end @@ -229,7 +229,7 @@ class ProtocolsDatatable < AjaxDatatablesRails::Base def build_conditions_for(query) # Inner query to retrieve list of protocol IDs where concatenated # protocol keywords string, or user's full_name contains searched query - search_val = params[:search][:value] + search_val = dt_params[:search][:value] records_having = get_raw_records_base.having( ::Arel::Nodes::NamedFunction.new( 'CAST', diff --git a/app/datatables/repository_datatable.rb b/app/datatables/repository_datatable.rb index bdc46480f..f0e94aaba 100644 --- a/app/datatables/repository_datatable.rb +++ b/app/datatables/repository_datatable.rb @@ -1,6 +1,6 @@ require 'active_record' -class RepositoryDatatable < AjaxDatatablesRails::Base +class RepositoryDatatable < CustomDatatable include ActionView::Helpers::TextHelper include SamplesHelper include InputSanitizeHelper @@ -79,9 +79,9 @@ class RepositoryDatatable < AjaxDatatablesRails::Base param_index = 2 filtered_array = [] input_array.each do |col| - next if params[:columns].to_a[param_index].nil? + next if columns_params.to_a[param_index].nil? params_col = - params[:columns].to_a.find { |v| v[1]['data'] == param_index.to_s } + 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 @@ -166,7 +166,7 @@ class RepositoryDatatable < AjaxDatatablesRails::Base ) .joins(:created_by) .where(repository: @repository) - return @assigned_rows if params[:assigned] == 'assigned' + return @assigned_rows if dt_params[:assigned] == 'assigned' else @assigned_rows = repository_rows.joins( 'INNER JOIN my_module_repository_rows ON @@ -183,10 +183,11 @@ class RepositoryDatatable < AjaxDatatablesRails::Base # number of samples/all samples it's dependant upon sort_record query def fetch_records records = get_raw_records - records = filter_records(records) if params[:search].present? - records = sort_records(records) if params[:order].present? - records = paginate_records(records) unless params[:length].present? && - params[:length] == '-1' + records = filter_records(records) if dt_params[:search].present? && + dt_params[:search][:value].present? + records = sort_records(records) if order_params.present? + records = paginate_records(records) unless dt_params[:length].present? && + dt_params[:length] == '-1' escape_special_chars records end @@ -195,10 +196,7 @@ class RepositoryDatatable < AjaxDatatablesRails::Base # NOTE: Function assumes the provided records/rows are only from the current # repository! def filter_records(repo_rows) - return repo_rows unless params[:search].present? && - params[:search][:value].present? - search_val = params[:search][:value] - + search_val = dt_params[:search][:value] filtered_rows = repo_rows.find_by_sql( "SELECT DISTINCT repository_rows.* FROM repository_rows @@ -232,123 +230,43 @@ class RepositoryDatatable < AjaxDatatablesRails::Base # 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 - direction = sort_null_direction(params[:order].values[0]) - if @my_module - # Depending on the sort, order nulls first or - # nulls last on repository_cells association - 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}") - else - records.joins( - 'LEFT OUTER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id)' - ).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}") - + if sort_column(order_params) == ASSIGNED_SORT_COL + # If "assigned" column is sorted + direction = sort_null_direction(order_params) + if @my_module + # Depending on the sort, order nulls first or + # nulls last on repository_cells association + return records if dt_params[:assigned] == 'assigned' records.joins( - "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, - 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" - ).order("values.value #{dir}") + "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}") else - super(records) + records.joins( + 'LEFT OUTER JOIN my_module_repository_rows ON + (repository_rows.id = my_module_repository_rows.repository_row_id)' + ).order("my_module_repository_rows.id NULLS #{direction}") end + elsif sorting_by_custom_column + ci = sortable_displayed_columns[order_params[:column].to_i - 1] + column_id = @columns_mappings.key((ci.to_i + 1).to_s) + dir = sort_direction(order_params) + + records.joins( + "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, + 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" + ).order("values.value #{dir}") 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' @@ -360,15 +278,15 @@ class RepositoryDatatable < AjaxDatatablesRails::Base end def sorting_by_custom_column - sort_column(params[:order].values[0]) == 'repository_cells.value' + sort_column(order_params) == '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]) + if dt_params[:search].present? + dt_params[:search][:value] = ActiveRecord::Base + .__send__(:sanitize_sql_like, + dt_params[:search][:value]) end end diff --git a/app/datatables/sample_datatable.rb b/app/datatables/sample_datatable.rb index a354a5f66..374cb8554 100644 --- a/app/datatables/sample_datatable.rb +++ b/app/datatables/sample_datatable.rb @@ -1,6 +1,6 @@ require 'active_record' -class SampleDatatable < AjaxDatatablesRails::Base +class SampleDatatable < CustomDatatable include ActionView::Helpers::TextHelper include SamplesHelper include InputSanitizeHelper @@ -49,7 +49,8 @@ class SampleDatatable < AjaxDatatablesRails::Base @user = user end - # Define sortable columns, so 1st column will be sorted by attribute in sortable_columns[0] + # Define sortable columns, so 1st column will be sorted + # by attribute in sortable_columns[0] def sortable_columns sort_array = [ ASSIGNED_SORT_COL, @@ -86,9 +87,9 @@ class SampleDatatable < AjaxDatatablesRails::Base param_index = 2 filtered_array = [] input_array.each do |col| - next if params[:columns].to_a[param_index].nil? + next if dt_params.to_a[param_index].nil? params_col = - params[:columns].to_a.find { |v| v[1]['data'] == param_index.to_s } + 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 @@ -139,9 +140,11 @@ class SampleDatatable < AjaxDatatablesRails::Base end def assigned_cell(record) - @assigned_samples.include?(record) ? - " " : + if @assigned_samples.include?(record) + " " + else " " + end end def sample_group_cell(record) @@ -159,21 +162,21 @@ class SampleDatatable < AjaxDatatablesRails::Base # 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 - ) + .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 @@ -184,7 +187,7 @@ class SampleDatatable < AjaxDatatablesRails::Base #{@my_module.id} OR sample_my_modules.id IS NULL))") .references(:sample_my_modules) - if params[:assigned] == 'assigned' + if dt_params[:assigned] == 'assigned' samples = samples.where('"sample_my_modules"."id" > 0') end elsif @project @@ -203,7 +206,7 @@ class SampleDatatable < AjaxDatatablesRails::Base sample_my_modules.id IS NULL))") .references(:sample_my_modules) end - if params[:assigned] == 'assigned' + if dt_params[:assigned] == 'assigned' samples = samples.joins('LEFT OUTER JOIN "my_modules" ON "my_modules"."id" = "sample_my_modules"."my_module_id"') @@ -221,8 +224,8 @@ class SampleDatatable < AjaxDatatablesRails::Base (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 params[:assigned] == 'assigned' + .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"') @@ -249,154 +252,177 @@ class SampleDatatable < AjaxDatatablesRails::Base # 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? + records = sort_records(records) if order_params.present? escape_special_chars - 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)) + 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 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 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 + 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 + # 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 params[:order].values[0]['dir'] == 'asc' - ids = assigned + unassigned - elsif params[:order].values[0]['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 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 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) + # 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 - 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]) - 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}) + ids = ids.collect(&:id) - records = records.where("samples.id IN (#{filter_query})") + # 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) - ci = sortable_displayed_columns[ - params[:order].values[0][:column].to_i - 1 - ] - cf_id = @cf_mappings.key((ci.to_i + 1).to_s) - 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. - 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) + # 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 + # 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!(/^\"|\"?$/, '') + 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| + searchable_cfs = columns_params.select do |_, v| v['searchable'] == 'true' && @cf_mappings.values.include?(v['data']) end cfmi = @cf_mappings.invert @@ -414,36 +440,43 @@ class SampleDatatable < AjaxDatatablesRails::Base ) 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 = ::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 = ::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" + val == 'ASC' ? 'LAST' : 'FIRST' end def inverse_sort_direction(item) val = sort_direction(item) - val == "ASC" ? "DESC" : "ASC" + val == 'ASC' ? 'DESC' : 'ASC' end def sorting_by_custom_column - sort_column(params[:order].values[0]) == 'sample_custom_fields.value' + sort_column(order_params) == 'sample_custom_fields.value' 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? + 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) @@ -452,7 +485,7 @@ class SampleDatatable < AjaxDatatablesRails::Base .split('.') return model if model == ASSIGNED_SORT_COL - col = [model.constantize.table_name, column].join('.') + [model.constantize.table_name, column].join('.') end def generate_sortable_displayed_columns diff --git a/app/datatables/team_users_datatable.rb b/app/datatables/team_users_datatable.rb index b89992ae5..2772f9062 100644 --- a/app/datatables/team_users_datatable.rb +++ b/app/datatables/team_users_datatable.rb @@ -1,4 +1,4 @@ -class TeamUsersDatatable < AjaxDatatablesRails::Base +class TeamUsersDatatable < CustomDatatable include InputSanitizeHelper include ActiveRecord::Sanitization::ClassMethods diff --git a/app/datatables/teams_datatable.rb b/app/datatables/teams_datatable.rb index 2ac27dfdd..76f8db8dd 100644 --- a/app/datatables/teams_datatable.rb +++ b/app/datatables/teams_datatable.rb @@ -1,4 +1,4 @@ -class TeamsDatatable < AjaxDatatablesRails::Base +class TeamsDatatable < CustomDatatable include InputSanitizeHelper def_delegator :@view, :link_to @@ -56,16 +56,12 @@ class TeamsDatatable < AjaxDatatablesRails::Base # Overwrite default sort method to handle custom members column # which is calculated in code and not present in DB def sort_records(records) - if params[:order].present? && params[:order].length == 1 - if sort_column(params[:order].values[0]) == MEMEBERS_SORT_COL - records = records.sort_by(&proc { |ut| ut.team.users.count }) - if params[:order].values[0]['dir'] == 'asc' - return records - elsif params[:order].values[0]['dir'] == 'desc' - return records.reverse - end - else - super(records) + if sort_column(order_params) == MEMEBERS_SORT_COL + records = records.sort_by(&proc { |ut| ut.team.users.count }) + if order_params['dir'] == 'asc' + return records + elsif order_params['dir'] == 'desc' + return records.reverse end else super(records) diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 23527d5c4..2cd0112aa 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -299,7 +299,7 @@ class MyModule < ApplicationRecord if !final.include?(my_module) final << my_module end - modules.push(*my_module.my_module_antecessors.flatten) + modules.push(*my_module.my_module_antecessors) end final end diff --git a/app/views/shared/_secondary_navigation.html.erb b/app/views/shared/_secondary_navigation.html.erb index 36b2006cc..f1b062da4 100644 --- a/app/views/shared/_secondary_navigation.html.erb +++ b/app/views/shared/_secondary_navigation.html.erb @@ -199,7 +199,7 @@