summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2012-09-29 12:57:38 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2012-09-29 12:57:38 +0000
commit1b6da80e16dd0cb200588422dfc5a655cb3b94f0 (patch)
tree5a4bedc3760b89680f96e989fa606fcba92e6250 /app
parent69b8931e92e37feceb7353d5baa6be6a51492bf0 (diff)
downloadredmine-1b6da80e16dd0cb200588422dfc5a655cb3b94f0.tar.gz
redmine-1b6da80e16dd0cb200588422dfc5a655cb3b94f0.zip
Makes related issues available for display and filtering on the issue list (#3239, #3265).
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@10513 e93f8b46-1217-0410-a6f0-8f06a7374b81
Diffstat (limited to 'app')
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/queries_helper.rb7
-rw-r--r--app/models/issue.rb21
-rw-r--r--app/models/issue_relation.rb21
-rw-r--r--app/models/query.rb73
-rw-r--r--app/views/queries/_filters.html.erb1
6 files changed, 115 insertions, 12 deletions
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bcccfd29b..4743eda43 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -64,10 +64,12 @@ module ApplicationHelper
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
# link_to_issue(issue, :subject => false) # => Defect #6
# link_to_issue(issue, :project => true) # => Foo - Defect #6
+ # link_to_issue(issue, :subject => false, :tracker => false) # => #6
#
def link_to_issue(issue, options={})
title = nil
subject = nil
+ text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
if options[:subject] == false
title = truncate(issue.subject, :length => 60)
else
@@ -76,7 +78,7 @@ module ApplicationHelper
subject = truncate(subject, :length => options[:truncate])
end
end
- s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
+ s = link_to text, {:controller => "issues", :action => "show", :id => issue},
:class => issue.css_classes,
:title => title
s << h(": #{subject}") if subject
diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb
index 076a8959e..ca452a41a 100644
--- a/app/helpers/queries_helper.rb
+++ b/app/helpers/queries_helper.rb
@@ -35,7 +35,7 @@ module QueriesHelper
def column_content(column, issue)
value = column.value(issue)
if value.is_a?(Array)
- value.collect {|v| column_value(column, issue, v)}.compact.sort.join(', ').html_safe
+ value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
else
column_value(column, issue, value)
end
@@ -73,6 +73,11 @@ module QueriesHelper
l(:general_text_No)
when 'Issue'
link_to_issue(value, :subject => false)
+ when 'IssueRelation'
+ other = value.other_issue(issue)
+ content_tag('span',
+ (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
+ :class => value.css_classes_for(issue))
else
h(value)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b44154340..2f1021b2d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -752,7 +752,7 @@ class Issue < ActiveRecord::Base
end
def relations
- @relations ||= (relations_from + relations_to).sort
+ @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
end
# Preloads relations for a collection of issues
@@ -775,6 +775,25 @@ class Issue < ActiveRecord::Base
end
end
+ # Preloads visible relations for a collection of issues
+ def self.load_visible_relations(issues, user=User.current)
+ if issues.any?
+ issue_ids = issues.map(&:id)
+ # Relations with issue_from in given issues and visible issue_to
+ relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
+ # Relations with issue_to in given issues and visible issue_from
+ relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
+
+ issues.each do |issue|
+ relations =
+ relations_from.select {|relation| relation.issue_from_id == issue.id} +
+ relations_to.select {|relation| relation.issue_to_id == issue.id}
+
+ issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
+ end
+ end
+ end
+
# Finds an issue relation given its id.
def find_relation(relation_id)
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
diff --git a/app/models/issue_relation.rb b/app/models/issue_relation.rb
index c145b87b9..285c4e306 100644
--- a/app/models/issue_relation.rb
+++ b/app/models/issue_relation.rb
@@ -15,6 +15,20 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# Class used to represent the relations of an issue
+class IssueRelations < Array
+ include Redmine::I18n
+
+ def initialize(issue, *args)
+ @issue = issue
+ super(*args)
+ end
+
+ def to_s(*args)
+ map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
+ end
+end
+
class IssueRelation < ActiveRecord::Base
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
@@ -103,6 +117,10 @@ class IssueRelation < ActiveRecord::Base
TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
end
+ def css_classes_for(issue)
+ "rel-#{relation_type_for(issue)}"
+ end
+
def handle_issue_order
reverse_if_needed
@@ -128,7 +146,8 @@ class IssueRelation < ActiveRecord::Base
end
def <=>(relation)
- TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
+ r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
+ r == 0 ? id <=> relation.id : r
end
private
diff --git a/app/models/query.rb b/app/models/query.rb
index ddf4d87d9..92a568119 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -113,7 +113,9 @@ class Query < ActiveRecord::Base
"<t-" => :label_more_than_ago,
"t-" => :label_ago,
"~" => :label_contains,
- "!~" => :label_not_contains }
+ "!~" => :label_not_contains,
+ "=p" => :label_any_issues_in_project,
+ "=!p" => :label_any_issues_not_in_project}
cattr_reader :operators
@@ -126,7 +128,8 @@ class Query < ActiveRecord::Base
:string => [ "=", "~", "!", "!~", "!*", "*" ],
:text => [ "~", "!~", "!*", "*" ],
:integer => [ "=", ">=", "<=", "><", "!*", "*" ],
- :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
+ :float => [ "=", ">=", "<=", "><", "!*", "*" ],
+ :relation => ["=", "=p", "=!p", "!*", "*"]}
cattr_reader :operators_by_filter_type
@@ -147,6 +150,7 @@ class Query < ActiveRecord::Base
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
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(:relations, :caption => :label_related_issues)
]
cattr_reader :available_columns
@@ -233,6 +237,10 @@ class Query < ActiveRecord::Base
"estimated_hours" => { :type => :float, :order => 13 },
"done_ratio" => { :type => :integer, :order => 14 }}
+ IssueRelation::TYPES.each do |relation_type, options|
+ @available_filters[relation_type] = {:type => :relation, :order => @available_filters.size + 100, :label => options[:name]}
+ end
+
principals = []
if project
principals += project.principals.sort
@@ -244,7 +252,6 @@ class Query < ActiveRecord::Base
end
end
else
- all_projects = Project.visible.all
if all_projects.any?
# members of visible projects
principals += Principal.member_of(all_projects)
@@ -254,10 +261,7 @@ class Query < ActiveRecord::Base
if User.current.logged? && User.current.memberships.any?
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
end
- Project.project_tree(all_projects) do |p, level|
- prefix = (level > 0 ? ('--' * level + ' ') : '')
- project_values << ["#{prefix}#{p.name}", p.id.to_s]
- end
+ project_values += all_projects_values
@available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
end
end
@@ -317,7 +321,7 @@ class Query < ActiveRecord::Base
}
@available_filters.each do |field, options|
- options[:name] ||= l("field_#{field}".gsub(/_id$/, ''))
+ options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
end
@available_filters
@@ -332,6 +336,21 @@ class Query < ActiveRecord::Base
json
end
+ def all_projects
+ @all_projects ||= Project.visible.all
+ end
+
+ def all_projects_values
+ return @all_projects_values if @all_projects_values
+
+ values = []
+ Project.project_tree(all_projects) do |p, level|
+ prefix = (level > 0 ? ('--' * level + ' ') : '')
+ values << ["#{prefix}#{p.name}", p.id.to_s]
+ end
+ @all_projects_values = values
+ end
+
def add_filter(field, operator, values)
# values must be an array
return unless values.nil? || values.is_a?(Array)
@@ -635,6 +654,9 @@ class Query < ActiveRecord::Base
if has_column?(:spent_hours)
Issue.load_visible_spent_hours(issues)
end
+ if has_column?(:relations)
+ Issue.load_visible_relations(issues)
+ end
issues
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
@@ -729,6 +751,41 @@ class Query < ActiveRecord::Base
"#{Issue.table_name}.is_private #{op} (#{va})"
end
+ def sql_for_relations(field, operator, value, options={})
+ relation_options = IssueRelation::TYPES[field]
+ return relation_options unless relation_options
+
+ relation_type = field
+ join_column, target_join_column = "issue_from_id", "issue_to_id"
+ if relation_options[:reverse] || options[:reverse]
+ relation_type = relation_options[:reverse] || relation_type
+ join_column, target_join_column = target_join_column, join_column
+ end
+
+ 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 = '#{connection.quote_string(relation_type)}')"
+ 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 = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
+ when "=p", "=!p"
+ op = (operator == "=p" ? '=' : '<>')
+ "#{Issue.table_name}.id IN (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{op} #{value.first.to_i})"
+ end
+
+ if relation_options[:sym] == field && !options[:reverse]
+ sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
+ sqls.join(["!", "!*"].include?(operator) ? " AND " : " OR ")
+ else
+ sql
+ end
+ end
+
+ IssueRelation::TYPES.keys.each do |relation_type|
+ alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
+ end
+
private
def sql_for_custom_field(field, operator, value, custom_field_id)
diff --git a/app/views/queries/_filters.html.erb b/app/views/queries/_filters.html.erb
index f9e371b7e..795f8075f 100644
--- a/app/views/queries/_filters.html.erb
+++ b/app/views/queries/_filters.html.erb
@@ -3,6 +3,7 @@ var operatorLabels = <%= raw_json Query.operators_labels %>;
var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
var availableFilters = <%= raw_json query.available_filters_as_json %>;
var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
+var allProjects = <%= raw query.all_projects_values.to_json %>;
$(document).ready(function(){
initFilters();
<% query.filters.each do |field, options| %>