Redmine 4.1.1
This commit is contained in:
parent
33e7b881a5
commit
3d976f1b3b
1593 changed files with 36180 additions and 19489 deletions
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2017 Jean-Philippe Lang
|
||||
# Copyright (C) 2006-2019 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
|
@ -25,24 +27,28 @@ class IssueQuery < Query
|
|||
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
|
||||
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
|
||||
QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
|
||||
QueryAssociationColumn.new(:parent, :subject, :caption => :field_parent_issue_subject),
|
||||
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
|
||||
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
|
||||
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
|
||||
QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
|
||||
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
|
||||
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
|
||||
TimestampQueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc', :groupable => true),
|
||||
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
|
||||
QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
|
||||
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
|
||||
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
|
||||
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date", :groupable => true),
|
||||
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date", :groupable => true),
|
||||
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
|
||||
QueryColumn.new(:total_estimated_hours,
|
||||
:sortable => -> { "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
|
||||
" WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')} AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)" },
|
||||
QueryColumn.new(
|
||||
:total_estimated_hours,
|
||||
:sortable => -> {
|
||||
"COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
|
||||
" WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')} AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)"
|
||||
},
|
||||
:default_order => 'desc'),
|
||||
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
|
||||
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
|
||||
QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
|
||||
TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc', :groupable => true),
|
||||
TimestampQueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc', :groupable => true),
|
||||
QueryColumn.new(:last_updated_by, :sortable => lambda {User.fields_for_order_statement("last_journal_user")}),
|
||||
QueryColumn.new(:relations, :caption => :label_related_issues),
|
||||
QueryColumn.new(:attachments, :caption => :label_attachment_plural),
|
||||
|
@ -73,59 +79,76 @@ class IssueQuery < Query
|
|||
options[:draw_progress_line] = (arg == '1' ? '1' : nil)
|
||||
end
|
||||
|
||||
def build_from_params(params)
|
||||
def draw_selected_columns
|
||||
r = options[:draw_selected_columns]
|
||||
r == '1'
|
||||
end
|
||||
|
||||
def draw_selected_columns=(arg)
|
||||
options[:draw_selected_columns] = (arg == '1' ? '1' : nil)
|
||||
end
|
||||
|
||||
def build_from_params(params, defaults={})
|
||||
super
|
||||
self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
|
||||
self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
|
||||
self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations]) || options[:draw_relations]
|
||||
self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line]) || options[:draw_progress_line]
|
||||
self.draw_selected_columns = params[:draw_selected_columns] || (params[:query] && params[:query][:draw_selected_columns]) || options[:draw_progress_line]
|
||||
self
|
||||
end
|
||||
|
||||
def initialize_available_filters
|
||||
add_available_filter "status_id",
|
||||
add_available_filter(
|
||||
"status_id",
|
||||
:type => :list_status, :values => lambda { issue_statuses_values }
|
||||
|
||||
add_available_filter("project_id",
|
||||
)
|
||||
add_available_filter(
|
||||
"project_id",
|
||||
:type => :list, :values => lambda { project_values }
|
||||
) if project.nil?
|
||||
|
||||
add_available_filter "tracker_id",
|
||||
add_available_filter(
|
||||
"tracker_id",
|
||||
:type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
|
||||
|
||||
add_available_filter "priority_id",
|
||||
)
|
||||
add_available_filter(
|
||||
"priority_id",
|
||||
:type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
|
||||
|
||||
add_available_filter("author_id",
|
||||
)
|
||||
add_available_filter(
|
||||
"author_id",
|
||||
:type => :list, :values => lambda { author_values }
|
||||
)
|
||||
|
||||
add_available_filter("assigned_to_id",
|
||||
add_available_filter(
|
||||
"assigned_to_id",
|
||||
:type => :list_optional, :values => lambda { assigned_to_values }
|
||||
)
|
||||
|
||||
add_available_filter("member_of_group",
|
||||
add_available_filter(
|
||||
"member_of_group",
|
||||
:type => :list_optional, :values => lambda { Group.givable.visible.collect {|g| [g.name, g.id.to_s] } }
|
||||
)
|
||||
|
||||
add_available_filter("assigned_to_role",
|
||||
add_available_filter(
|
||||
"assigned_to_role",
|
||||
:type => :list_optional, :values => lambda { Role.givable.collect {|r| [r.name, r.id.to_s] } }
|
||||
)
|
||||
|
||||
add_available_filter "fixed_version_id",
|
||||
add_available_filter(
|
||||
"fixed_version_id",
|
||||
:type => :list_optional, :values => lambda { fixed_version_values }
|
||||
|
||||
add_available_filter "fixed_version.due_date",
|
||||
)
|
||||
add_available_filter(
|
||||
"fixed_version.due_date",
|
||||
:type => :date,
|
||||
:name => l(:label_attribute_of_fixed_version, :name => l(:field_effective_date))
|
||||
|
||||
add_available_filter "fixed_version.status",
|
||||
)
|
||||
add_available_filter(
|
||||
"fixed_version.status",
|
||||
:type => :list,
|
||||
:name => l(:label_attribute_of_fixed_version, :name => l(:field_status)),
|
||||
:values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
|
||||
|
||||
add_available_filter "category_id",
|
||||
)
|
||||
add_available_filter(
|
||||
"category_id",
|
||||
:type => :list_optional,
|
||||
:values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } } if project
|
||||
|
||||
:values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
|
||||
) if project
|
||||
add_available_filter "subject", :type => :text
|
||||
add_available_filter "description", :type => :text
|
||||
add_available_filter "created_on", :type => :date_past
|
||||
|
@ -134,37 +157,54 @@ class IssueQuery < Query
|
|||
add_available_filter "start_date", :type => :date
|
||||
add_available_filter "due_date", :type => :date
|
||||
add_available_filter "estimated_hours", :type => :float
|
||||
|
||||
if User.current.allowed_to?(:view_time_entries, project, :global => true)
|
||||
add_available_filter "spent_time", :type => :float, :label => :label_spent_time
|
||||
end
|
||||
|
||||
add_available_filter "done_ratio", :type => :integer
|
||||
|
||||
if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
|
||||
User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
|
||||
add_available_filter "is_private",
|
||||
add_available_filter(
|
||||
"is_private",
|
||||
:type => :list,
|
||||
:values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
|
||||
)
|
||||
end
|
||||
|
||||
add_available_filter "attachment",
|
||||
add_available_filter(
|
||||
"attachment",
|
||||
:type => :text, :name => l(:label_attachment)
|
||||
|
||||
)
|
||||
if User.current.logged?
|
||||
add_available_filter "watcher_id",
|
||||
:type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
|
||||
add_available_filter(
|
||||
"watcher_id",
|
||||
:type => :list, :values => lambda { watcher_values }
|
||||
)
|
||||
end
|
||||
|
||||
add_available_filter("updated_by",
|
||||
add_available_filter(
|
||||
"updated_by",
|
||||
:type => :list, :values => lambda { author_values }
|
||||
)
|
||||
|
||||
add_available_filter("last_updated_by",
|
||||
add_available_filter(
|
||||
"last_updated_by",
|
||||
:type => :list, :values => lambda { author_values }
|
||||
)
|
||||
|
||||
if project && !project.leaf?
|
||||
add_available_filter "subproject_id",
|
||||
add_available_filter(
|
||||
"subproject_id",
|
||||
:type => :list_subprojects,
|
||||
:values => lambda { subproject_values }
|
||||
)
|
||||
end
|
||||
|
||||
add_available_filter(
|
||||
"project.status",
|
||||
:type => :list,
|
||||
:name => l(:label_attribute_of_project, :name => l(:field_status)),
|
||||
:values => lambda { project_statuses_values }
|
||||
) if project.nil? || !project.leaf?
|
||||
|
||||
add_custom_fields_filters(issue_custom_fields)
|
||||
add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
|
||||
|
||||
|
@ -195,11 +235,13 @@ class IssueQuery < Query
|
|||
" JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id" +
|
||||
" WHERE (#{TimeEntry.visible_condition(User.current)}) AND #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
|
||||
|
||||
@available_columns.insert index, QueryColumn.new(:spent_hours,
|
||||
:sortable => "COALESCE((#{subselect}), 0)",
|
||||
:default_order => 'desc',
|
||||
:caption => :label_spent_time,
|
||||
:totalable => true
|
||||
@available_columns.insert(
|
||||
index,
|
||||
QueryColumn.new(:spent_hours,
|
||||
:sortable => "COALESCE((#{subselect}), 0)",
|
||||
:default_order => 'desc',
|
||||
:caption => :label_spent_time,
|
||||
:totalable => true)
|
||||
)
|
||||
|
||||
subselect = "SELECT SUM(hours) FROM #{TimeEntry.table_name}" +
|
||||
|
@ -208,10 +250,12 @@ class IssueQuery < Query
|
|||
" WHERE (#{TimeEntry.visible_condition(User.current)})" +
|
||||
" AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt"
|
||||
|
||||
@available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
|
||||
:sortable => "COALESCE((#{subselect}), 0)",
|
||||
:default_order => 'desc',
|
||||
:caption => :label_total_spent_time
|
||||
@available_columns.insert(
|
||||
index + 1,
|
||||
QueryColumn.new(:total_spent_hours,
|
||||
:sortable => "COALESCE((#{subselect}), 0)",
|
||||
:default_order => 'desc',
|
||||
:caption => :label_total_spent_time)
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -274,6 +318,10 @@ class IssueQuery < Query
|
|||
# Valid options are :order, :offset, :limit, :include, :conditions
|
||||
def issues(options={})
|
||||
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
|
||||
# The default order of IssueQuery is issues.id DESC(by IssueQuery#default_sort_criteria)
|
||||
unless ["#{Issue.table_name}.id ASC", "#{Issue.table_name}.id DESC"].any?{|i| order_option.include?(i)}
|
||||
order_option << "#{Issue.table_name}.id DESC"
|
||||
end
|
||||
|
||||
scope = Issue.visible.
|
||||
joins(:status, :project).
|
||||
|
@ -316,6 +364,10 @@ class IssueQuery < Query
|
|||
# Returns the issues ids
|
||||
def issue_ids(options={})
|
||||
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
|
||||
# The default order of IssueQuery is issues.id DESC(by IssueQuery#default_sort_criteria)
|
||||
unless ["#{Issue.table_name}.id ASC", "#{Issue.table_name}.id DESC"].any?{|i| order_option.include?(i)}
|
||||
order_option << "#{Issue.table_name}.id DESC"
|
||||
end
|
||||
|
||||
Issue.visible.
|
||||
joins(:status, :project).
|
||||
|
@ -381,27 +433,43 @@ class IssueQuery < Query
|
|||
"#{neg} EXISTS (#{subquery})"
|
||||
end
|
||||
|
||||
def sql_for_spent_time_field(field, operator, value)
|
||||
first, second = value.first.to_f, value.second.to_f
|
||||
sql_op =
|
||||
case operator
|
||||
when "=", ">=", "<=" then "#{operator} #{first}"
|
||||
when "><" then "BETWEEN #{first} AND #{second}"
|
||||
when "*" then "> 0"
|
||||
when "!*" then "= 0"
|
||||
else
|
||||
return nil
|
||||
end
|
||||
"COALESCE((" +
|
||||
"SELECT ROUND(CAST(SUM(hours) AS DECIMAL(30,3)), 2) " +
|
||||
"FROM #{TimeEntry.table_name} " +
|
||||
"WHERE issue_id = #{Issue.table_name}.id), 0) #{sql_op}"
|
||||
end
|
||||
|
||||
def sql_for_watcher_id_field(field, operator, value)
|
||||
db_table = Watcher.table_name
|
||||
|
||||
me, others = value.partition { |id| ['0', User.current.id.to_s].include?(id) }
|
||||
sql = if others.any?
|
||||
"SELECT #{Issue.table_name}.id FROM #{Issue.table_name} " +
|
||||
"INNER JOIN #{db_table} ON #{Issue.table_name}.id = #{db_table}.watchable_id AND #{db_table}.watchable_type = 'Issue' " +
|
||||
"LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id " +
|
||||
"WHERE (" +
|
||||
sql_for_field(field, '=', me, db_table, 'user_id') +
|
||||
') OR (' +
|
||||
Project.allowed_to_condition(User.current, :view_issue_watchers) +
|
||||
' AND ' +
|
||||
sql_for_field(field, '=', others, db_table, 'user_id') +
|
||||
')'
|
||||
else
|
||||
"SELECT #{db_table}.watchable_id FROM #{db_table} " +
|
||||
"WHERE #{db_table}.watchable_type='Issue' AND " +
|
||||
sql_for_field(field, '=', me, db_table, 'user_id')
|
||||
end
|
||||
|
||||
sql =
|
||||
if others.any?
|
||||
"SELECT #{Issue.table_name}.id FROM #{Issue.table_name} " +
|
||||
"INNER JOIN #{db_table} ON #{Issue.table_name}.id = #{db_table}.watchable_id AND #{db_table}.watchable_type = 'Issue' " +
|
||||
"LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id " +
|
||||
"WHERE (" +
|
||||
sql_for_field(field, '=', me, db_table, 'user_id') +
|
||||
') OR (' +
|
||||
Project.allowed_to_condition(User.current, :view_issue_watchers) +
|
||||
' AND ' +
|
||||
sql_for_field(field, '=', others, db_table, 'user_id') +
|
||||
')'
|
||||
else
|
||||
"SELECT #{db_table}.watchable_id FROM #{db_table} " +
|
||||
"WHERE #{db_table}.watchable_type='Issue' AND " +
|
||||
sql_for_field(field, '=', me, db_table, 'user_id')
|
||||
end
|
||||
"#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (#{sql})"
|
||||
end
|
||||
|
||||
|
@ -475,13 +543,22 @@ class IssueQuery < Query
|
|||
c = sql_contains("a.filename", value.first)
|
||||
e = (operator == "~" ? "EXISTS" : "NOT EXISTS")
|
||||
"#{e} (SELECT 1 FROM #{Attachment.table_name} a WHERE a.container_type = 'Issue' AND a.container_id = #{Issue.table_name}.id AND #{c})"
|
||||
when "^", "$"
|
||||
c = sql_contains("a.filename", value.first, (operator == "^" ? :starts_with : :ends_with) => true)
|
||||
"EXISTS (SELECT 1 FROM #{Attachment.table_name} a WHERE a.container_type = 'Issue' AND a.container_id = #{Issue.table_name}.id AND #{c})"
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_parent_id_field(field, operator, value)
|
||||
case operator
|
||||
when "="
|
||||
"#{Issue.table_name}.parent_id = #{value.first.to_i}"
|
||||
# accepts a comma separated list of ids
|
||||
ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
|
||||
if ids.present?
|
||||
"#{Issue.table_name}.parent_id IN (#{ids.join(",")})"
|
||||
else
|
||||
"1=0"
|
||||
end
|
||||
when "~"
|
||||
root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
|
||||
if root_id && lft && rgt
|
||||
|
@ -499,9 +576,11 @@ class IssueQuery < Query
|
|||
def sql_for_child_id_field(field, operator, value)
|
||||
case operator
|
||||
when "="
|
||||
parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
|
||||
if parent_id
|
||||
"#{Issue.table_name}.id = #{parent_id}"
|
||||
# accepts a comma separated list of child ids
|
||||
child_ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
|
||||
ids = Issue.where(:id => child_ids).pluck(:parent_id).compact.uniq
|
||||
if ids.present?
|
||||
"#{Issue.table_name}.id IN (#{ids.join(",")})"
|
||||
else
|
||||
"1=0"
|
||||
end
|
||||
|
@ -554,8 +633,8 @@ class IssueQuery < Query
|
|||
relation_type = relation_options[:reverse] || relation_type
|
||||
join_column, target_join_column = target_join_column, join_column
|
||||
end
|
||||
|
||||
sql = case operator
|
||||
sql =
|
||||
case operator
|
||||
when "*", "!*"
|
||||
op = (operator == "*" ? 'IN' : 'NOT IN')
|
||||
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
|
||||
|
@ -570,7 +649,6 @@ class IssueQuery < Query
|
|||
op = (operator == "!o" ? 'NOT IN' : 'IN')
|
||||
"#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))"
|
||||
end
|
||||
|
||||
if relation_options[:sym] == field && !options[:reverse]
|
||||
sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
|
||||
sql = sqls.join(["!", "!*", "!p", '!o'].include?(operator) ? " AND " : " OR ")
|
||||
|
@ -578,12 +656,16 @@ class IssueQuery < Query
|
|||
"(#{sql})"
|
||||
end
|
||||
|
||||
def sql_for_project_status_field(field, operator, value, options={})
|
||||
sql_for_field(field, operator, value, Project.table_name, "status")
|
||||
end
|
||||
|
||||
def find_assigned_to_id_filter_values(values)
|
||||
Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
|
||||
end
|
||||
alias :find_author_id_filter_values :find_assigned_to_id_filter_values
|
||||
|
||||
IssueRelation::TYPES.keys.each do |relation_type|
|
||||
IssueRelation::TYPES.each_key do |relation_type|
|
||||
alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue