@@ -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 |
|
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. |
|
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 |
|
|
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: |
|
742 | label_relates_to: Related to | |
741 |
label_duplicates: |
|
743 | label_duplicates: Duplicates | |
742 |
label_duplicated_by: |
|
744 | label_duplicated_by: Duplicated by | |
743 |
label_blocks: |
|
745 | label_blocks: Blocks | |
744 |
label_blocked_by: |
|
746 | label_blocked_by: Blocked by | |
745 |
label_precedes: |
|
747 | label_precedes: Precedes | |
746 |
label_follows: |
|
748 | label_follows: Follows | |
747 |
label_copied_to: |
|
749 | label_copied_to: Copied to | |
748 |
label_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: |
|
726 | label_relates_to: LiΓ© Γ | |
725 |
label_duplicates: |
|
727 | label_duplicates: Duplique | |
726 |
label_duplicated_by: |
|
728 | label_duplicated_by: DupliquΓ© par | |
727 |
label_blocks: |
|
729 | label_blocks: Bloque | |
728 |
label_blocked_by: |
|
730 | label_blocked_by: BloquΓ© par | |
729 |
label_precedes: |
|
731 | label_precedes: Précède | |
730 |
label_follows: |
|
732 | label_follows: Suit | |
731 |
label_copied_to: |
|
733 | label_copied_to: CopiΓ© vers | |
732 |
label_copied_from: |
|
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:2 |
|
344 | fieldset#filters td.field { width:230px; } | |
344 |
fieldset#filters td.operator { width:1 |
|
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