scinote-web/app/datatables/protocols_datatable.rb
2016-07-21 13:11:15 +02:00

229 lines
8.1 KiB
Ruby

class ProtocolsDatatable < AjaxDatatablesRails::Base
# Needed for sanitize_sql_like method
include ActiveRecord::Sanitization::ClassMethods
def_delegator :@view, :can_edit_protocol
def_delegator :@view, :edit_protocol_path
def_delegator :@view, :can_clone_protocol
def_delegator :@view, :clone_protocol_path
def_delegator :@view, :can_make_protocol_private
def_delegator :@view, :can_publish_protocol
def_delegator :@view, :can_archive_protocol
def_delegator :@view, :can_restore_protocol
def_delegator :@view, :can_export_protocol
def_delegator :@view, :linked_children_protocol_path
def initialize(view, organization, type, user)
super(view)
@organization = organization
# :public, :private or :archive
@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
# 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 => params[:draw].to_i,
:recordsTotal => get_raw_records.length,
:recordsFiltered => filter_records(get_raw_records).length,
:data => data
}
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
formated_date = (I18n.t 'time.formats.datatables_date').gsub!(/^\"|\"?$/, '')
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
private
# Returns json of current protocols (already paginated)
def data
result_data = []
records.each do |record|
protocol = Protocol.find(record.id)
result_data << {
"DT_RowId": record.id,
"DT_CanEdit": can_edit_protocol(protocol),
"DT_EditUrl": can_edit_protocol(protocol) ?
edit_protocol_path(protocol, organization: @organization, type: @type) : nil,
"DT_CanClone": can_clone_protocol(protocol),
"DT_CloneUrl": can_clone_protocol(protocol) ?
clone_protocol_path(protocol, organization: @organization, type: @type) : nil,
"DT_CanMakePrivate": can_make_protocol_private(protocol),
"DT_CanPublish": can_publish_protocol(protocol),
"DT_CanArchive": can_archive_protocol(protocol),
"DT_CanRestore": can_restore_protocol(protocol),
"DT_CanExport": can_export_protocol(protocol),
"1": record.name,
"2": keywords_html(record),
"3": modules_html(record),
"4": record.full_username_str,
"5": timestamp_column_html(record),
"6": I18n.l(record.updated_at, format: :full)
}
end
result_data
end
def get_raw_records_base
records =
Protocol
.where(organization: @organization)
.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"."archived_by_id"')
.where("\"protocols\".\"protocol_type\" = #{Protocol.protocol_types[:in_repository_archived]}")
.where(added_by: @user)
end
records.group('"protocols"."id"')
end
# 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"',
'"protocols"."archived_on"'
)
end
# Various helper methods
def timestamp_db_column
if @type == :public
"Protocol.published_on"
elsif @type == :private
"Protocol.created_at"
else
"Protocol.archived_on"
end
end
def keywords_html(record)
if !record.protocol_keywords_str || record.protocol_keywords_str.empty?
"<i>#{I18n.t("protocols.no_keywords")}</i>"
else
kws = record.protocol_keywords_str.split(", ")
res = []
kws.sort_by{ |word| word.downcase }.each do |kw|
res << "<a href='#' data-action='filter' data-param='#{kw}'>#{kw}</a>"
end
res.join(", ")
end
end
def modules_html(record)
"<a href='#' data-action='load-linked-children'" +
" data-url='#{linked_children_protocol_path(record)}'>" +
"#{record.nr_of_linked_children}" +
"</a>"
end
def timestamp_column_html(record)
if @type == :public
I18n.l(record.published_on, format: :full)
elsif @type == :private
I18n.l(record.created_at, format: :full)
else
I18n.l(record.archived_on, 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 = 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