class LoadFromRepositoryProtocolsDatatable < CustomDatatable # Needed for sanitize_sql_like method include ActiveRecord::Sanitization::ClassMethods include InputSanitizeHelper def initialize(view, team, type, user) super(view) @team = team # :public or :private @type = type @user = user end def sortable_columns @sortable_columns ||= [ "Protocol.name", "protocol_keywords_str", "Protocol.nr_of_linked_children", "full_username_str", timestamp_db_column, "Protocol.updated_at" ] end def searchable_columns @searchable_columns ||= [ "Protocol.name", timestamp_db_column, "Protocol.updated_at" ] 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 or updated_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 case column when 'published_on' casted_column = ::Arel::Nodes::NamedFunction.new('CAST', [ Arel.sql("to_char( protocols.created_at, '#{ formated_date }' ) AS VARCHAR") ] ) when 'updated_at' casted_column = ::Arel::Nodes::NamedFunction.new('CAST', [ Arel.sql("to_char( protocols.updated_at, '#{ formated_date }' ) AS VARCHAR") ] ) else casted_column = ::Arel::Nodes::NamedFunction.new('CAST', [model.arel_table[column.to_sym].as(typecast)]) end casted_column.matches("%#{value}%") end # This hack is needed to display a correct amount of # searched entries (needed for pagination). # This is needed because of usage of GROUP operator in SQL. # See https://github.com/antillas21/ajax-datatables-rails/issues/112 def as_json(options = {}) { draw: dt_params[:draw].to_i, recordsTotal: get_raw_records.length, recordsFiltered: filter_records(get_raw_records).length, data: data } end private # Returns json of current protocols (already paginated) def data records.map do |record| { 'DT_RowId': record.id, '1': escape_input(record.name), '2': keywords_html(record), '3': record.nr_of_linked_children, '4': escape_input(record.full_username_str), '5': timestamp_column_html(record), '6': I18n.l(record.updated_at, format: :full) } end end def get_raw_records_base records = Protocol .where(team: @team) .joins('LEFT OUTER JOIN "protocol_protocol_keywords" ON "protocol_protocol_keywords"."protocol_id" = "protocols"."id"') .joins('LEFT OUTER JOIN "protocol_keywords" ON "protocol_protocol_keywords"."protocol_keyword_id" = "protocol_keywords"."id"') if @type == :public records = records .joins('LEFT OUTER JOIN users ON users.id = protocols.added_by_id') .where('protocols.protocol_type = ?', Protocol.protocol_types[:in_repository_public]) elsif @type == :private records = records .joins('LEFT OUTER JOIN users ON users.id = protocols.added_by_id') .where('protocols.protocol_type = ?', Protocol.protocol_types[:in_repository_private]) .where(added_by: @user) else records = records .joins('LEFT OUTER JOIN users ON users.id = protocols.added_by_id') .where('(protocols.protocol_type = ? OR (protocols.protocol_type = ? AND added_by_id = ?))', Protocol.protocol_types[:in_repository_public], Protocol.protocol_types[:in_repository_private], @user.id) end records.group('"protocols"."id"') end # OVERRIDE - query database for records (this will be # later paginated and filtered) after that "data" function # will return json def get_raw_records get_raw_records_base .select( '"protocols"."id"', '"protocols"."name"', '"protocols"."protocol_type"', 'string_agg("protocol_keywords"."name", \', \') AS "protocol_keywords_str"', '"protocols"."nr_of_linked_children"', 'max("users"."full_name") AS "full_username_str"', # "Hack" to get single username '"protocols"."created_at"', '"protocols"."updated_at"', '"protocols"."published_on"' ) end # Various helper methods def timestamp_db_column if @type == :public "Protocol.published_on" else "Protocol.created_at" end end def keywords_html(record) if !record.protocol_keywords_str || record.protocol_keywords_str.empty? "#{I18n.t("protocols.no_keywords")}" else kws = record.protocol_keywords_str.split(", ") res = [] kws.sort_by{ |word| word.downcase }.each do |kw| res << "#{kw}" end sanitize_input(res.join(', ')) end end def timestamp_column_html(record) if @type == :public I18n.l(record.published_on, format: :full) else I18n.l(record.created_at, format: :full) end end # OVERRIDE - This is only called when filtering results; # when using GROUP BY function, SQL cannot perform a WHERE # clause on aggregated columns (protocol keywords & users' full_name), but # since we want those 2 columns to be searchable/filterable, we do an "inner" # query where we select only protocol IDs which are filtered by those 2 columns # using HAVING keyword (which is the correct way to filter aggregated columns). # Another OR is then appended to the WHERE clause, checking if protocol is inside # this list of IDs. 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 = dt_params[:search][:value] records_having = get_raw_records_base.having( ::Arel::Nodes::NamedFunction.new( 'CAST', [::Arel::Nodes::SqlLiteral.new("string_agg(\"protocol_keywords\".\"name\", ' ') AS #{typecast}")] ).matches("%#{sanitize_sql_like(search_val)}%").to_sql + " OR " + ::Arel::Nodes::NamedFunction.new( 'CAST', [::Arel::Nodes::SqlLiteral.new("max(\"users\".\"full_name\") AS #{typecast}")] ).matches("%#{sanitize_sql_like(search_val)}%").to_sql ).select(:id) # Call parent function criteria = super(query) # Aight, now append another or criteria = criteria.or(Protocol.arel_table[:id].in(records_having.arel)) criteria end end