diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2012-09-29 12:57:38 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2012-09-29 12:57:38 +0000 |
commit | 1b6da80e16dd0cb200588422dfc5a655cb3b94f0 (patch) | |
tree | 5a4bedc3760b89680f96e989fa606fcba92e6250 /app | |
parent | 69b8931e92e37feceb7353d5baa6be6a51492bf0 (diff) | |
download | redmine-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.rb | 4 | ||||
-rw-r--r-- | app/helpers/queries_helper.rb | 7 | ||||
-rw-r--r-- | app/models/issue.rb | 21 | ||||
-rw-r--r-- | app/models/issue_relation.rb | 21 | ||||
-rw-r--r-- | app/models/query.rb | 73 | ||||
-rw-r--r-- | app/views/queries/_filters.html.erb | 1 |
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| %> |