redmine_dmsf/app/models/dmsf_query.rb
Patrick Skillen 74dc3b5b27 Fix query fields being loaded as decimal instead of int
With some DB backends (specifically AWS Postgres RDS) these fields would load as BigDecimal instead of int. This led to
1) versions being displayed as "1.0.2.0" instead of "1.2", and 2) versions with letters (e.g A.0) leading to a 500 error, as BigDecimal#chr doesn't exist
2021-05-06 14:33:16 +01:00

427 lines
16 KiB
Ruby

# encode: utf-8
# frozen_string_literal: true
#
# Redmine plugin for Document Management System "Features"
#
# Copyright © 2011-21 Karel Pičman <karel.picman@kontron.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
class DmsfQuery < Query
attr_accessor :dmsf_folder_id, :deleted, :sub_projects
self.queried_class = DmsfFolder
self.view_permission = :view_dmsf_files
# Standard columns
self.available_columns = [
QueryColumn.new(:id, sortable: 'id', caption: +'#'),
DmsfTitleQueryColumn.new(:title, sortable: 'title', frozen: true),
QueryColumn.new(:size, sortable: 'size'),
DmsfModifiedQueryColumn.new(:modified, sortable: 'updated'),
DmsfVersionQueryColumn.new(:version, sortable: 'major_version, minor_version', caption: :label_dmsf_version),
QueryColumn.new(:workflow, sortable: 'workflow'),
QueryColumn.new(:author, sortable: 'firstname, lastname')
]
def initialize(attributes=nil, *args)
super attributes
self.sort_criteria = []
self.filters ||= { 'title' => { operator: '~', values: ['']} }
end
######################################################################################################################
# Inherited
#
def available_columns
unless @available_columns
@available_columns = self.class.available_columns.dup
@available_columns += DmsfFileRevisionCustomField.visible.collect do |cf|
c = QueryCustomFieldColumn.new(cf)
# We would like to prevent grouping in the Option form
c.groupable = false
c
end
end
@available_columns
end
def default_columns_names
unless @default_column_names
@default_column_names = []
columns = available_columns
if columns
columns.each do |column|
if DmsfFolder.is_column_on?(column.name.to_s)
@default_column_names << column.name.to_sym
end
end
end
end
@default_column_names
end
def default_sort_criteria
[['title', 'ASC']]
end
def base_scope
unless @scope
@scope = [dmsf_folders_scope, dmsf_folder_links_scope, dmsf_projects_scope, dmsf_files_scope,
dmsf_file_links_scope, dmsf_url_links_scope].compact.inject(:union_all)
end
@scope
end
# Returns the issue count
def dmsf_count
base_scope.where(statement).count
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new e.message
end
def type
'DmsfQuery'
end
def initialize_available_filters
add_available_filter 'author', type: :list, values: lambda { author_values }
add_available_filter 'title', type: :text
add_available_filter 'updated', type: :date_past
add_custom_fields_filters DmsfFileRevisionCustomField.all
end
def statement
unless @statement
@filter_dmsf_folder_id = false
filters_clauses = []
filters.each_key do |field|
v = values_for(field).clone
next unless v && !v.empty?
operator = operator_for(field)
case field
when 'author'
if v.delete('me')
v.push User.current.id.to_s
end
when 'title'
next if (operator == '~') && v.join.empty?
end
filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
end
filters_clauses.reject!(&:blank?)
@statement = filters_clauses.any? ? filters_clauses.join(' AND ') : nil
end
@statement
end
def validate_query_filters
# Skip validation for empty title (default filter)
filter = filters.delete('title')
super
# Add it back
filters['title'] = filter if filter
end
def columns
cols = super
# Just move the optional column Id to the beginning as it isn't frozen
id_index = cols.index { |col| col.name == :id }
if id_index == 1
id_col = cols.delete_at(id_index)
cols.insert 0, id_col
end
cols
end
######################################################################################################################
# New
def dmsf_nodes(options={})
order_option = ['sort', group_by_sort_order, (options[:order] || sort_clause[0])].flatten.reject(&:blank?)
if order_option.size > 1
DmsfFileRevisionCustomField.visible.pluck(:id, :name).each do |id, name|
order_option[1].gsub!("COALESCE(cf_#{id}.value, '')", "cf_#{id}")
end
order_option[1].gsub!(',', " #{$1},")
if order_option[1] =~ /(DESC|ASC)$/
order_option[1].gsub!(',', " #{$1},")
end
end
items = base_scope.
where(statement).
order(order_option).
limit(options[:limit]).
offset(options[:offset]).to_a
items.delete_if do |item|
case item.type
when 'folder'
dmsf_folder = DmsfFolder.find_by(id: item.id)
if dmsf_folder && (!DmsfFolder.permissions?(dmsf_folder, false))
true
end
when 'project'
p = Project.find_by(id: item.id)
true unless p&.dmsf_available?
end
end
items
end
def extra_columns
[]
end
private
def dmsf_projects_scope
return nil unless sub_projects
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",NULL AS cf_#{id}"
end
scope = Project.select(%{
projects.id AS id,
projects.id AS project_id,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS revision_id,
projects.name AS title,
projects.identifier AS filename,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS size,
projects.updated_on AS updated,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS major_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS minor_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow_id,
'' AS firstname,
'' AS lastname,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS author,
'project' AS type,
CAST(0 AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS deleted,
0 AS sort #{cf_columns}}).visible
if dmsf_folder_id || deleted
scope.none
else
scope = scope.non_templates if scope.respond_to?(:non_templates)
scope.where projects: { parent_id: project&.id }
end
end
def dmsf_folders_scope
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",(SELECT value from custom_values WHERE custom_field_id = #{id} AND customized_type = 'DmsfFolder' AND customized_id = dmsf_folders.id) AS cf_#{id}"
end
scope = DmsfFolder.select(%{
dmsf_folders.id AS id,
dmsf_folders.project_id AS project_id,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS revision_id,
dmsf_folders.title AS title,
NULL AS filename,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS size,
dmsf_folders.updated_at AS updated,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS major_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS minor_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'folder' AS type,
dmsf_folders.deleted AS deleted,
1 AS sort #{cf_columns}}).
joins('LEFT JOIN users ON dmsf_folders.user_id = users.id')
return scope.none unless project
if deleted
scope = scope.deleted
else
scope = scope.visible
end
if dmsf_folder_id
scope.where dmsf_folders: { dmsf_folder_id: dmsf_folder_id, deleted: deleted }
else
if statement.present? || deleted
scope.where dmsf_folders: { project_id: project.id, deleted: deleted }
else
scope.where dmsf_folders: { project_id: project.id, dmsf_folder_id: nil, deleted: deleted }
end
end
end
def dmsf_folder_links_scope
return nil unless project
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",(SELECT value from custom_values WHERE custom_field_id = #{id} AND customized_type = 'DmsfFolder' AND customized_id = dmsf_folders.id) AS cf_#{id}"
end
scope = DmsfLink.select(%{
dmsf_links.id AS id,
COALESCE(dmsf_folders.project_id, dmsf_links.project_id) AS project_id,
dmsf_links.target_id AS revision_id,
dmsf_links.name AS title,
dmsf_folders.title AS filename,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS size,
COALESCE(dmsf_folders.updated_at, dmsf_links.updated_at) AS updated,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS major_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS minor_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'folder-link' AS type,
dmsf_links.deleted AS deleted,
1 AS sort #{cf_columns}}).
joins('LEFT JOIN dmsf_folders ON dmsf_links.target_id = dmsf_folders.id').
joins('LEFT JOIN users ON users.id = COALESCE(dmsf_folders.user_id, dmsf_links.user_id)')
if dmsf_folder_id
scope.where dmsf_links: { target_type: 'DmsfFolder', dmsf_folder_id: dmsf_folder_id, deleted: deleted }
else
if statement.present? || deleted
scope.where dmsf_links: { target_type: 'DmsfFolder', project_id: project.id, deleted: deleted }
else
scope.where dmsf_links: { target_type: 'DmsfFolder', project_id: project.id, dmsf_folder_id: nil, deleted: deleted }
end
end
end
def dmsf_files_scope
return nil unless project
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",(SELECT value from custom_values WHERE custom_field_id = #{id} AND customized_type = 'DmsfFileRevision' AND customized_id = dmsf_file_revisions.id) AS cf_#{id}"
end
scope = DmsfFile.select(%{
dmsf_files.id AS id,
dmsf_files.project_id AS project_id,
dmsf_file_revisions.id AS revision_id,
dmsf_file_revisions.title AS title,
dmsf_file_revisions.name AS filename,
dmsf_file_revisions.size AS size,
dmsf_file_revisions.updated_at AS updated,
dmsf_file_revisions.major_version AS major_version,
dmsf_file_revisions.minor_version AS minor_version,
dmsf_file_revisions.workflow AS workflow,
dmsf_file_revisions.dmsf_workflow_id AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'file' AS type,
dmsf_files.deleted AS deleted,
2 AS sort #{cf_columns}}).
joins(:dmsf_file_revisions).
joins('LEFT JOIN users ON dmsf_file_revisions.user_id = users.id ').
where(sub_query)
if dmsf_folder_id
scope.where dmsf_files: { dmsf_folder_id: dmsf_folder_id, deleted: deleted }
else
if statement.present? || deleted
scope.where dmsf_files: { project_id: project.id, deleted: deleted }
else
scope.where dmsf_files: { project_id: project.id, dmsf_folder_id: nil, deleted: deleted }
end
end
end
def dmsf_file_links_scope
return nil unless project
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",(SELECT value from custom_values WHERE custom_field_id = #{id} AND customized_type = 'DmsfFileRevision' AND customized_id = dmsf_file_revisions.id) AS cf_#{id}"
end
scope = DmsfLink.select(%{
dmsf_links.id AS id,
dmsf_files.project_id AS project_id,
dmsf_files.id AS revision_id,
dmsf_links.name AS title,
dmsf_file_revisions.name AS filename,
dmsf_file_revisions.size AS size,
dmsf_file_revisions.updated_at AS updated,
dmsf_file_revisions.major_version AS major_version,
dmsf_file_revisions.minor_version AS minor_version,
dmsf_file_revisions.workflow AS workflow,
dmsf_file_revisions.dmsf_workflow_id AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'file-link' AS type,
dmsf_links.deleted AS deleted,
2 AS sort #{cf_columns}}).
joins('JOIN dmsf_files ON dmsf_files.id = dmsf_links.target_id').
joins('JOIN dmsf_file_revisions ON dmsf_file_revisions.dmsf_file_id = dmsf_files.id').
joins('LEFT JOIN users ON dmsf_file_revisions.user_id = users.id ').
where(sub_query)
if dmsf_folder_id
scope.where dmsf_links: { target_type: 'DmsfFile', dmsf_folder_id: dmsf_folder_id, deleted: deleted }
else
if statement.present? || deleted
scope.where dmsf_links: { target_type: 'DmsfFile', project_id: project.id, deleted: deleted }
else
scope.where dmsf_links: { target_type: 'DmsfFile', project_id: project.id, dmsf_folder_id: nil, deleted: deleted }
end
end
end
def dmsf_url_links_scope
return nil unless project
cf_columns = +''
DmsfFileRevisionCustomField.visible.order(:position).pluck(:id).each do |id|
cf_columns << ",NULL AS cf_#{id}"
end
scope = DmsfLink.select(%{
dmsf_links.id AS id,
dmsf_links.project_id AS project_id,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS revision_id,
dmsf_links.name AS title,
dmsf_links.external_url AS filename,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS size,
dmsf_links.updated_at AS updated,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS major_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS minor_version,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow,
CAST(NULL AS #{ActiveRecord::Base.connection.type_to_sql(:integer)}) AS workflow_id,
users.firstname AS firstname,
users.lastname AS lastname,
users.id AS author,
'url-link' AS type,
dmsf_links.deleted AS deleted,
2 AS sort #{cf_columns}}).
joins('LEFT JOIN users ON dmsf_links.user_id = users.id ')
if dmsf_folder_id
scope.where dmsf_links: { target_type: 'DmsfUrl', dmsf_folder_id: dmsf_folder_id, deleted: deleted }
else
if statement.present? || deleted
scope.where dmsf_links: { target_type: 'DmsfUrl', project_id: project.id, deleted: deleted }
else
scope.where dmsf_links: { target_type: 'DmsfUrl', project_id: project.id, dmsf_folder_id: nil, deleted: deleted }
end
end
end
def sub_query
case ActiveRecord::Base.connection.adapter_name.downcase
when 'sqlserver'
'dmsf_file_revisions.id = (SELECT TOP 1 r.id FROM dmsf_file_revisions r WHERE r.created_at = (SELECT MAX(created_at) FROM dmsf_file_revisions rr WHERE rr.dmsf_file_id = dmsf_files.id) AND r.dmsf_file_id = dmsf_files.id ORDER BY id DESC)'
else
'dmsf_file_revisions.id = (SELECT r.id FROM dmsf_file_revisions r WHERE r.created_at = (SELECT MAX(created_at) FROM dmsf_file_revisions rr WHERE rr.dmsf_file_id = dmsf_files.id) AND r.dmsf_file_id = dmsf_files.id ORDER BY id DESC LIMIT 1)'
end
end
end