##// END OF EJS Templates
Makes related issues available for display and filtering on the issue list (#3239, #3265)....
Jean-Philippe Lang -
r10303:1b6da80e16dd
parent child
Show More
@@ -64,10 +64,12 module ApplicationHelper
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :subject => false) # => Defect #6
65 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
67 #
68 #
68 def link_to_issue(issue, options={})
69 def link_to_issue(issue, options={})
69 title = nil
70 title = nil
70 subject = nil
71 subject = nil
72 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
71 if options[:subject] == false
73 if options[:subject] == false
72 title = truncate(issue.subject, :length => 60)
74 title = truncate(issue.subject, :length => 60)
73 else
75 else
@@ -76,7 +78,7 module ApplicationHelper
76 subject = truncate(subject, :length => options[:truncate])
78 subject = truncate(subject, :length => options[:truncate])
77 end
79 end
78 end
80 end
79 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
81 s = link_to text, {:controller => "issues", :action => "show", :id => issue},
80 :class => issue.css_classes,
82 :class => issue.css_classes,
81 :title => title
83 :title => title
82 s << h(": #{subject}") if subject
84 s << h(": #{subject}") if subject
@@ -35,7 +35,7 module QueriesHelper
35 def column_content(column, issue)
35 def column_content(column, issue)
36 value = column.value(issue)
36 value = column.value(issue)
37 if value.is_a?(Array)
37 if value.is_a?(Array)
38 value.collect {|v| column_value(column, issue, v)}.compact.sort.join(', ').html_safe
38 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
39 else
39 else
40 column_value(column, issue, value)
40 column_value(column, issue, value)
41 end
41 end
@@ -73,6 +73,11 module QueriesHelper
73 l(:general_text_No)
73 l(:general_text_No)
74 when 'Issue'
74 when 'Issue'
75 link_to_issue(value, :subject => false)
75 link_to_issue(value, :subject => false)
76 when 'IssueRelation'
77 other = value.other_issue(issue)
78 content_tag('span',
79 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
80 :class => value.css_classes_for(issue))
76 else
81 else
77 h(value)
82 h(value)
78 end
83 end
@@ -752,7 +752,7 class Issue < ActiveRecord::Base
752 end
752 end
753
753
754 def relations
754 def relations
755 @relations ||= (relations_from + relations_to).sort
755 @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
756 end
756 end
757
757
758 # Preloads relations for a collection of issues
758 # Preloads relations for a collection of issues
@@ -775,6 +775,25 class Issue < ActiveRecord::Base
775 end
775 end
776 end
776 end
777
777
778 # Preloads visible relations for a collection of issues
779 def self.load_visible_relations(issues, user=User.current)
780 if issues.any?
781 issue_ids = issues.map(&:id)
782 # Relations with issue_from in given issues and visible issue_to
783 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
784 # Relations with issue_to in given issues and visible issue_from
785 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
786
787 issues.each do |issue|
788 relations =
789 relations_from.select {|relation| relation.issue_from_id == issue.id} +
790 relations_to.select {|relation| relation.issue_to_id == issue.id}
791
792 issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
793 end
794 end
795 end
796
778 # Finds an issue relation given its id.
797 # Finds an issue relation given its id.
779 def find_relation(relation_id)
798 def find_relation(relation_id)
780 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
799 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
@@ -15,6 +15,20
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 # Class used to represent the relations of an issue
19 class IssueRelations < Array
20 include Redmine::I18n
21
22 def initialize(issue, *args)
23 @issue = issue
24 super(*args)
25 end
26
27 def to_s(*args)
28 map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
29 end
30 end
31
18 class IssueRelation < ActiveRecord::Base
32 class IssueRelation < ActiveRecord::Base
19 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
33 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
20 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
34 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
@@ -103,6 +117,10 class IssueRelation < ActiveRecord::Base
103 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
117 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
104 end
118 end
105
119
120 def css_classes_for(issue)
121 "rel-#{relation_type_for(issue)}"
122 end
123
106 def handle_issue_order
124 def handle_issue_order
107 reverse_if_needed
125 reverse_if_needed
108
126
@@ -128,7 +146,8 class IssueRelation < ActiveRecord::Base
128 end
146 end
129
147
130 def <=>(relation)
148 def <=>(relation)
131 TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
149 r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
150 r == 0 ? id <=> relation.id : r
132 end
151 end
133
152
134 private
153 private
@@ -113,7 +113,9 class Query < ActiveRecord::Base
113 "<t-" => :label_more_than_ago,
113 "<t-" => :label_more_than_ago,
114 "t-" => :label_ago,
114 "t-" => :label_ago,
115 "~" => :label_contains,
115 "~" => :label_contains,
116 "!~" => :label_not_contains }
116 "!~" => :label_not_contains,
117 "=p" => :label_any_issues_in_project,
118 "=!p" => :label_any_issues_not_in_project}
117
119
118 cattr_reader :operators
120 cattr_reader :operators
119
121
@@ -126,7 +128,8 class Query < ActiveRecord::Base
126 :string => [ "=", "~", "!", "!~", "!*", "*" ],
128 :string => [ "=", "~", "!", "!~", "!*", "*" ],
127 :text => [ "~", "!~", "!*", "*" ],
129 :text => [ "~", "!~", "!*", "*" ],
128 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
130 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
129 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
131 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :relation => ["=", "=p", "=!p", "!*", "*"]}
130
133
131 cattr_reader :operators_by_filter_type
134 cattr_reader :operators_by_filter_type
132
135
@@ -147,6 +150,7 class Query < ActiveRecord::Base
147 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
148 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
149 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
153 QueryColumn.new(:relations, :caption => :label_related_issues)
150 ]
154 ]
151 cattr_reader :available_columns
155 cattr_reader :available_columns
152
156
@@ -233,6 +237,10 class Query < ActiveRecord::Base
233 "estimated_hours" => { :type => :float, :order => 13 },
237 "estimated_hours" => { :type => :float, :order => 13 },
234 "done_ratio" => { :type => :integer, :order => 14 }}
238 "done_ratio" => { :type => :integer, :order => 14 }}
235
239
240 IssueRelation::TYPES.each do |relation_type, options|
241 @available_filters[relation_type] = {:type => :relation, :order => @available_filters.size + 100, :label => options[:name]}
242 end
243
236 principals = []
244 principals = []
237 if project
245 if project
238 principals += project.principals.sort
246 principals += project.principals.sort
@@ -244,7 +252,6 class Query < ActiveRecord::Base
244 end
252 end
245 end
253 end
246 else
254 else
247 all_projects = Project.visible.all
248 if all_projects.any?
255 if all_projects.any?
249 # members of visible projects
256 # members of visible projects
250 principals += Principal.member_of(all_projects)
257 principals += Principal.member_of(all_projects)
@@ -254,10 +261,7 class Query < ActiveRecord::Base
254 if User.current.logged? && User.current.memberships.any?
261 if User.current.logged? && User.current.memberships.any?
255 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
262 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
256 end
263 end
257 Project.project_tree(all_projects) do |p, level|
264 project_values += all_projects_values
258 prefix = (level > 0 ? ('--' * level + ' ') : '')
259 project_values << ["#{prefix}#{p.name}", p.id.to_s]
260 end
261 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
265 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
262 end
266 end
263 end
267 end
@@ -317,7 +321,7 class Query < ActiveRecord::Base
317 }
321 }
318
322
319 @available_filters.each do |field, options|
323 @available_filters.each do |field, options|
320 options[:name] ||= l("field_#{field}".gsub(/_id$/, ''))
324 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
321 end
325 end
322
326
323 @available_filters
327 @available_filters
@@ -332,6 +336,21 class Query < ActiveRecord::Base
332 json
336 json
333 end
337 end
334
338
339 def all_projects
340 @all_projects ||= Project.visible.all
341 end
342
343 def all_projects_values
344 return @all_projects_values if @all_projects_values
345
346 values = []
347 Project.project_tree(all_projects) do |p, level|
348 prefix = (level > 0 ? ('--' * level + ' ') : '')
349 values << ["#{prefix}#{p.name}", p.id.to_s]
350 end
351 @all_projects_values = values
352 end
353
335 def add_filter(field, operator, values)
354 def add_filter(field, operator, values)
336 # values must be an array
355 # values must be an array
337 return unless values.nil? || values.is_a?(Array)
356 return unless values.nil? || values.is_a?(Array)
@@ -635,6 +654,9 class Query < ActiveRecord::Base
635 if has_column?(:spent_hours)
654 if has_column?(:spent_hours)
636 Issue.load_visible_spent_hours(issues)
655 Issue.load_visible_spent_hours(issues)
637 end
656 end
657 if has_column?(:relations)
658 Issue.load_visible_relations(issues)
659 end
638 issues
660 issues
639 rescue ::ActiveRecord::StatementInvalid => e
661 rescue ::ActiveRecord::StatementInvalid => e
640 raise StatementInvalid.new(e.message)
662 raise StatementInvalid.new(e.message)
@@ -729,6 +751,41 class Query < ActiveRecord::Base
729 "#{Issue.table_name}.is_private #{op} (#{va})"
751 "#{Issue.table_name}.is_private #{op} (#{va})"
730 end
752 end
731
753
754 def sql_for_relations(field, operator, value, options={})
755 relation_options = IssueRelation::TYPES[field]
756 return relation_options unless relation_options
757
758 relation_type = field
759 join_column, target_join_column = "issue_from_id", "issue_to_id"
760 if relation_options[:reverse] || options[:reverse]
761 relation_type = relation_options[:reverse] || relation_type
762 join_column, target_join_column = target_join_column, join_column
763 end
764
765 sql = case operator
766 when "*", "!*"
767 op = (operator == "*" ? 'IN' : 'NOT IN')
768 "#{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)}')"
769 when "=", "!"
770 op = (operator == "=" ? 'IN' : 'NOT IN')
771 "#{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})"
772 when "=p", "=!p"
773 op = (operator == "=p" ? '=' : '<>')
774 "#{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})"
775 end
776
777 if relation_options[:sym] == field && !options[:reverse]
778 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
779 sqls.join(["!", "!*"].include?(operator) ? " AND " : " OR ")
780 else
781 sql
782 end
783 end
784
785 IssueRelation::TYPES.keys.each do |relation_type|
786 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
787 end
788
732 private
789 private
733
790
734 def sql_for_custom_field(field, operator, value, custom_field_id)
791 def sql_for_custom_field(field, operator, value, custom_field_id)
@@ -3,6 +3,7 var operatorLabels = <%= raw_json Query.operators_labels %>;
3 var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
3 var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
4 var availableFilters = <%= raw_json query.available_filters_as_json %>;
4 var availableFilters = <%= raw_json query.available_filters_as_json %>;
5 var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
5 var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
6 var allProjects = <%= raw query.all_projects_values.to_json %>;
6 $(document).ready(function(){
7 $(document).ready(function(){
7 initFilters();
8 initFilters();
8 <% query.filters.each do |field, options| %>
9 <% query.filters.each do |field, options| %>
@@ -669,6 +669,8 en:
669 label_ago: days ago
669 label_ago: days ago
670 label_contains: contains
670 label_contains: contains
671 label_not_contains: doesn't contain
671 label_not_contains: doesn't contain
672 label_any_issues_in_project: any issues in project
673 label_any_issues_not_in_project: any issues not in project
672 label_day_plural: days
674 label_day_plural: days
673 label_repository: Repository
675 label_repository: Repository
674 label_repository_new: New repository
676 label_repository_new: New repository
@@ -737,15 +739,15 en:
737 label_loading: Loading...
739 label_loading: Loading...
738 label_relation_new: New relation
740 label_relation_new: New relation
739 label_relation_delete: Delete relation
741 label_relation_delete: Delete relation
740 label_relates_to: related to
742 label_relates_to: Related to
741 label_duplicates: duplicates
743 label_duplicates: Duplicates
742 label_duplicated_by: duplicated by
744 label_duplicated_by: Duplicated by
743 label_blocks: blocks
745 label_blocks: Blocks
744 label_blocked_by: blocked by
746 label_blocked_by: Blocked by
745 label_precedes: precedes
747 label_precedes: Precedes
746 label_follows: follows
748 label_follows: Follows
747 label_copied_to: copied to
749 label_copied_to: Copied to
748 label_copied_from: copied from
750 label_copied_from: Copied from
749 label_end_to_start: end to start
751 label_end_to_start: end to start
750 label_end_to_end: end to end
752 label_end_to_end: end to end
751 label_start_to_start: start to start
753 label_start_to_start: start to start
@@ -659,6 +659,8 fr:
659 label_ago: il y a
659 label_ago: il y a
660 label_contains: contient
660 label_contains: contient
661 label_not_contains: ne contient pas
661 label_not_contains: ne contient pas
662 label_any_issues_in_project: une demande du projet
663 label_any_issues_not_in_project: une demande hors du projet
662 label_day_plural: jours
664 label_day_plural: jours
663 label_repository: DΓ©pΓ΄t
665 label_repository: DΓ©pΓ΄t
664 label_repository_new: Nouveau dΓ©pΓ΄t
666 label_repository_new: Nouveau dΓ©pΓ΄t
@@ -721,15 +723,15 fr:
721 label_loading: Chargement...
723 label_loading: Chargement...
722 label_relation_new: Nouvelle relation
724 label_relation_new: Nouvelle relation
723 label_relation_delete: Supprimer la relation
725 label_relation_delete: Supprimer la relation
724 label_relates_to: liΓ© Γ 
726 label_relates_to: LiΓ© Γ 
725 label_duplicates: duplique
727 label_duplicates: Duplique
726 label_duplicated_by: dupliquΓ© par
728 label_duplicated_by: DupliquΓ© par
727 label_blocks: bloque
729 label_blocks: Bloque
728 label_blocked_by: bloquΓ© par
730 label_blocked_by: BloquΓ© par
729 label_precedes: précède
731 label_precedes: Précède
730 label_follows: suit
732 label_follows: Suit
731 label_copied_to: copiΓ© vers
733 label_copied_to: CopiΓ© vers
732 label_copied_from: copiΓ© depuis
734 label_copied_from: CopiΓ© depuis
733 label_end_to_start: fin Γ  dΓ©but
735 label_end_to_start: fin Γ  dΓ©but
734 label_end_to_end: fin Γ  fin
736 label_end_to_end: fin Γ  fin
735 label_start_to_start: dΓ©but Γ  dΓ©but
737 label_start_to_start: dΓ©but Γ  dΓ©but
@@ -178,6 +178,20 function buildFilterRow(field, operator, values) {
178 );
178 );
179 $('#values_'+fieldId).val(values[0]);
179 $('#values_'+fieldId).val(values[0]);
180 break;
180 break;
181 case "relation":
182 tr.find('td.values').append(
183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
184 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
185 );
186 $('#values_'+fieldId).val(values[0]);
187 select = tr.find('td.values select');
188 for (i=0;i<allProjects.length;i++){
189 var filterValue = allProjects[i];
190 var option = $('<option>');
191 option.val(filterValue[1]).text(filterValue[0]);
192 if (values[0] == filterValue[1]) {option.attr('selected', true)};
193 select.append(option);
194 }
181 case "integer":
195 case "integer":
182 case "float":
196 case "float":
183 tr.find('td.values').append(
197 tr.find('td.values').append(
@@ -244,6 +258,10 function toggleOperator(field) {
244 case "t-":
258 case "t-":
245 enableValues(field, [2]);
259 enableValues(field, [2]);
246 break;
260 break;
261 case "=p":
262 case "=!p":
263 enableValues(field, [1]);
264 break;
247 default:
265 default:
248 enableValues(field, [0]);
266 enableValues(field, [0]);
249 break;
267 break;
@@ -120,7 +120,7 a#toggle-completed-versions {color:#999;}
120 /***** Tables *****/
120 /***** Tables *****/
121 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
121 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
122 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
122 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
123 table.list td { vertical-align: top; }
123 table.list td { vertical-align: top; padding-right:10px; }
124 table.list td.id { width: 2%; text-align: center;}
124 table.list td.id { width: 2%; text-align: center;}
125 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
125 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
126 table.list td.checkbox input {padding:0px;}
126 table.list td.checkbox input {padding:0px;}
@@ -144,9 +144,10 tr.project.idnt-8 td.name {padding-left: 11em;}
144 tr.project.idnt-9 td.name {padding-left: 12.5em;}
144 tr.project.idnt-9 td.name {padding-left: 12.5em;}
145
145
146 tr.issue { text-align: center; white-space: nowrap; }
146 tr.issue { text-align: center; white-space: nowrap; }
147 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
147 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
148 tr.issue td.subject { text-align: left; }
148 tr.issue td.subject, tr.issue td.relations { text-align: left; }
149 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
149 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
150 tr.issue td.relations span {white-space: nowrap;}
150
151
151 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
152 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
152 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
153 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
@@ -340,12 +341,14 fieldset#date-range p { margin: 2px 0 2px 0; }
340 fieldset#filters table { border-collapse: collapse; }
341 fieldset#filters table { border-collapse: collapse; }
341 fieldset#filters table td { padding: 0; vertical-align: middle; }
342 fieldset#filters table td { padding: 0; vertical-align: middle; }
342 fieldset#filters tr.filter { height: 2.1em; }
343 fieldset#filters tr.filter { height: 2.1em; }
343 fieldset#filters td.field { width:250px; }
344 fieldset#filters td.field { width:230px; }
344 fieldset#filters td.operator { width:170px; }
345 fieldset#filters td.operator { width:180px; }
346 fieldset#filters td.operator select {max-width:170px;}
345 fieldset#filters td.values { white-space:nowrap; }
347 fieldset#filters td.values { white-space:nowrap; }
346 fieldset#filters td.values select {min-width:130px;}
348 fieldset#filters td.values select {min-width:130px;}
347 fieldset#filters td.values input {height:1em;}
349 fieldset#filters td.values input {height:1em;}
348 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
350 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
351
349 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
352 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
350 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
353 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
351
354
@@ -766,7 +766,7 class IssuesControllerTest < ActionController::TestCase
766 end
766 end
767 end
767 end
768
768
769 def test_index_with_done_ratio
769 def test_index_with_done_ratio_column
770 Issue.find(1).update_attribute :done_ratio, 40
770 Issue.find(1).update_attribute :done_ratio, 40
771
771
772 get :index, :set_filter => 1, :c => %w(done_ratio)
772 get :index, :set_filter => 1, :c => %w(done_ratio)
@@ -792,12 +792,48 class IssuesControllerTest < ActionController::TestCase
792 assert_no_tag 'td', :attributes => {:class => /spent_hours/}
792 assert_no_tag 'td', :attributes => {:class => /spent_hours/}
793 end
793 end
794
794
795 def test_index_with_fixed_version
795 def test_index_with_fixed_version_column
796 get :index, :set_filter => 1, :c => %w(fixed_version)
796 get :index, :set_filter => 1, :c => %w(fixed_version)
797 assert_tag 'td', :attributes => {:class => /fixed_version/},
797 assert_tag 'td', :attributes => {:class => /fixed_version/},
798 :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}}
798 :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}}
799 end
799 end
800
800
801 def test_index_with_relations_column
802 IssueRelation.delete_all
803 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7))
804 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1))
805 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11))
806 IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2))
807
808 get :index, :set_filter => 1, :c => %w(subject relations)
809 assert_response :success
810 assert_select "tr#issue-1 td.relations" do
811 assert_select "span", 3
812 assert_select "span", :text => "Related to #7"
813 assert_select "span", :text => "Related to #8"
814 assert_select "span", :text => "Blocks #11"
815 end
816 assert_select "tr#issue-2 td.relations" do
817 assert_select "span", 1
818 assert_select "span", :text => "Blocked by #12"
819 end
820 assert_select "tr#issue-3 td.relations" do
821 assert_select "span", 0
822 end
823
824 get :index, :set_filter => 1, :c => %w(relations), :format => 'csv'
825 assert_response :success
826 assert_equal 'text/csv; header=present', response.content_type
827 lines = response.body.chomp.split("\n")
828 assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines
829 assert_include '2,Blocked by #12', lines
830 assert_include '3,""', lines
831
832 get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf'
833 assert_response :success
834 assert_equal 'application/pdf', response.content_type
835 end
836
801 def test_index_send_html_if_query_is_invalid
837 def test_index_send_html_if_query_is_invalid
802 get :index, :f => ['start_date'], :op => {:start_date => '='}
838 get :index, :f => ['start_date'], :op => {:start_date => '='}
803 assert_equal 'text/html', @response.content_type
839 assert_equal 'text/html', @response.content_type
@@ -624,6 +624,76 class QueryTest < ActiveSupport::TestCase
624 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
624 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
625 end
625 end
626
626
627 def test_filter_on_relations_with_a_specific_issue
628 IssueRelation.delete_all
629 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
630 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
631
632 query = Query.new(:name => '_')
633 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
634 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
635
636 query = Query.new(:name => '_')
637 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
638 assert_equal [1], find_issues_with_query(query).map(&:id).sort
639 end
640
641 def test_filter_on_relations_with_any_issues_in_a_project
642 IssueRelation.delete_all
643 with_settings :cross_project_issue_relations => '1' do
644 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
645 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
646 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
647 end
648
649 query = Query.new(:name => '_')
650 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
651 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
652
653 query = Query.new(:name => '_')
654 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
655 assert_equal [1], find_issues_with_query(query).map(&:id).sort
656
657 query = Query.new(:name => '_')
658 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
659 assert_equal [], find_issues_with_query(query).map(&:id).sort
660 end
661
662 def test_filter_on_relations_with_any_issues_not_in_a_project
663 IssueRelation.delete_all
664 with_settings :cross_project_issue_relations => '1' do
665 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
666 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
667 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
668 end
669
670 query = Query.new(:name => '_')
671 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
672 assert_equal [1], find_issues_with_query(query).map(&:id).sort
673 end
674
675 def test_filter_on_relations_with_no_issues
676 IssueRelation.delete_all
677 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
678 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
679
680 query = Query.new(:name => '_')
681 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
682 ids = find_issues_with_query(query).map(&:id)
683 assert_equal [], ids & [1, 2, 3]
684 assert_include 4, ids
685 end
686
687 def test_filter_on_relations_with_any_issue
688 IssueRelation.delete_all
689 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
690 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
691
692 query = Query.new(:name => '_')
693 query.filters = {"relates" => {:operator => '*', :values => ['']}}
694 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id)
695 end
696
627 def test_statement_should_be_nil_with_no_filters
697 def test_statement_should_be_nil_with_no_filters
628 q = Query.new(:name => '_')
698 q = Query.new(:name => '_')
629 q.filters = {}
699 q.filters = {}
General Comments 0
You need to be logged in to leave comments. Login now