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