diff --git a/app/datatables/load_from_repository_protocols_datatable.rb b/app/datatables/load_from_repository_protocols_datatable.rb
index 88e1e4ed8..da8f7bd55 100644
--- a/app/datatables/load_from_repository_protocols_datatable.rb
+++ b/app/datatables/load_from_repository_protocols_datatable.rb
@@ -3,6 +3,8 @@ class LoadFromRepositoryProtocolsDatatable < CustomDatatable
include ActiveRecord::Sanitization::ClassMethods
include InputSanitizeHelper
+ PREFIXED_ID_SQL = "('#{Protocol::ID_PREFIX}' || COALESCE(\"protocols\".\"parent_id\", \"protocols\".\"id\"))".freeze
+
def initialize(view, team, user)
super(view)
@team = team
@@ -12,8 +14,8 @@ class LoadFromRepositoryProtocolsDatatable < CustomDatatable
def sortable_columns
@sortable_columns ||= [
'Protocol.name',
- 'nr_of_versions',
- 'Protocol.id',
+ 'Protocol.version_number',
+ 'adjusted_parent_id',
'protocol_keywords_str',
'full_username_str',
'Protocol.published_on'
@@ -23,20 +25,18 @@ class LoadFromRepositoryProtocolsDatatable < CustomDatatable
def searchable_columns
@searchable_columns ||= [
'Protocol.name',
- 'Protocol.id',
- 'Protocol.published_on'
+ "Protocol.#{PREFIXED_ID_SQL}",
+ 'Protocol.published_on',
+ 'Protocol.version_number',
+ 'ProtocolKeyword.name'
]
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 = {})
+ def as_json(_options = {})
{
draw: dt_params[:draw].to_i,
- recordsTotal: get_raw_records.length,
- recordsFiltered: filter_records(get_raw_records).length,
+ recordsTotal: get_raw_records_base.distinct.count,
+ recordsFiltered: records.present? ? records.first.filtered_count : 0,
data: data
}
end
@@ -46,33 +46,55 @@ class LoadFromRepositoryProtocolsDatatable < CustomDatatable
# Returns json of current protocols (already paginated)
def data
records.map do |record|
+ parent = record.parent || record
{
- 'DT_RowId': record.id,
+ DT_RowId: parent.id,
'0': escape_input(record.name),
- '1': record.nr_of_versions,
- '2': record.code,
+ '1': record.version_number,
+ '2': parent.code,
'3': keywords_html(record),
- '4': escape_input(record.full_username_str),
+ '4': escape_input(record.published_by.full_name),
'5': I18n.l(record.published_on, format: :full)
}
end
end
- def get_raw_records_base
- records =
- Protocol
- .where(team: @team)
- .where(protocols: { protocol_type: Protocol.protocol_types[:in_repository_published_original] })
- .joins("LEFT OUTER JOIN protocols protocol_versions "\
- "ON protocol_versions.protocol_type = #{Protocol.protocol_types[:in_repository_published_version]} "\
- "AND protocol_versions.parent_id = protocols.id")
- .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"')
- .joins('LEFT OUTER JOIN users ON users.id = protocols.published_by_id').active
+ def new_search_condition(column, value)
+ model, column = column.split('.', 2)
+ model = model.constantize
- records.group('"protocols"."id"')
+ casted_column = case column
+ when PREFIXED_ID_SQL
+ ::Arel::Nodes::SqlLiteral.new(PREFIXED_ID_SQL)
+ when 'published_on'
+ ::Arel::Nodes::NamedFunction.new(
+ 'CAST', [Arel.sql("to_char( protocols.published_on, '#{formated_date}' ) AS VARCHAR")]
+ )
+ else
+ ::Arel::Nodes::NamedFunction.new('CAST', [model.arel_table[column.to_sym].as(typecast)])
+ end
+ casted_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value)}%")
+ end
+
+ def fetch_records
+ super.select('COUNT("protocols"."id") OVER() AS filtered_count')
+ end
+
+ def get_raw_records_base
+ original_without_versions = @team.protocols
+ .left_outer_joins(:published_versions)
+ .where(protocol_type: Protocol.protocol_types[:in_repository_published_original])
+ .where(published_versions: { id: nil })
+ .select(:id)
+
+ published_versions = @team.protocols
+ .where(protocol_type: Protocol.protocol_types[:in_repository_published_version])
+ .order('parent_id, version_number DESC')
+ .select('DISTINCT ON (parent_id) id')
+
+ Protocol.where("protocols.id IN ((#{original_without_versions.to_sql}) UNION (#{published_versions.to_sql}))")
+ .active
+ .with_granted_permissions(@user, ProtocolPermissions::READ)
end
# OVERRIDE - query database for records (this will be
@@ -80,10 +102,17 @@ class LoadFromRepositoryProtocolsDatatable < CustomDatatable
# will return json
def get_raw_records
get_raw_records_base
+ .preload(:parent, :protocol_keywords, user_assignments: %i(user user_role))
+ .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"')
+ .joins('LEFT OUTER JOIN "users" ON "users"."id" = "protocols"."published_by_id"')
+ .group('"protocols"."id"')
.select(
'"protocols".*',
- 'STRING_AGG("protocol_keywords"."name", \', \') AS "protocol_keywords_str"',
- 'COUNT("protocol_versions"."id") + 1 AS "nr_of_versions"',
+ 'COALESCE("protocols"."parent_id", "protocols"."id") AS adjusted_parent_id',
+ 'STRING_AGG(DISTINCT("protocol_keywords"."name"), \', \') AS "protocol_keywords_str"',
'MAX("users"."full_name") AS "full_username_str"'
)
end
@@ -91,59 +120,15 @@ class LoadFromRepositoryProtocolsDatatable < CustomDatatable
# Various helper methods
def keywords_html(record)
- if record.protocol_keywords_str.blank?
+ if record.protocol_keywords.blank?
"#{I18n.t('protocols.no_keywords')}"
else
- kws = record.protocol_keywords_str.split(', ')
res = []
- kws.sort_by(&:downcase).each do |kw|
- res << "#{kw}"
+ record.protocol_keywords.sort_by { |kw| kw.name.downcase }.each do |kw|
+ sanitized_kw = sanitize_input(kw.name)
+ res << "#{sanitized_kw}"
end
- sanitize_input(res.join(', '))
+ 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 +
- ' OR ' +
- ::Arel::Nodes::NamedFunction.new(
- 'CAST',
- [::Arel::Nodes::SqlLiteral.new("COUNT(\"protocol_versions\".\"id\") + 1 AS #{typecast}")]
- ).matches("%#{sanitize_sql_like(search_val)}%").to_sql
- ).select(:id)
-
- # Call parent function
- criteria = super(query)
-
- # Aight, now append another or
- criteria.or(Protocol.arel_table[:id].in(records_having.arel))
- end
end