##// END OF EJS Templates
Fixed: "None" category issue count is empty while grouping by category (#4308)....
Jean-Philippe Lang -
r2998:346c569f98f5
parent child
Show More
@@ -1,70 +1,64
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
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 module QueriesHelper
18 module QueriesHelper
19
19
20 def operators_for_select(filter_type)
20 def operators_for_select(filter_type)
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
22 end
22 end
23
23
24 def column_header(column)
24 def column_header(column)
25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
26 :default_order => column.default_order) :
26 :default_order => column.default_order) :
27 content_tag('th', column.caption)
27 content_tag('th', column.caption)
28 end
28 end
29
29
30 def column_value(column, issue)
31 if column.is_a?(QueryCustomFieldColumn)
32 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 show_value(cv)
34 else
35 value = issue.send(column.name)
36 end
37 end
38
39 def column_content(column, issue)
30 def column_content(column, issue)
40 if column.is_a?(QueryCustomFieldColumn)
31 value = column.value(issue)
41 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
32
42 show_value(cv)
33 case value.class.name
43 else
34 when 'String'
44 value = issue.send(column.name)
35 if column.name == :subject
45 if value.is_a?(Date)
36 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
46 format_date(value)
37 else
47 elsif value.is_a?(Time)
38 h(value)
48 format_time(value)
39 end
40 when 'Time'
41 format_time(value)
42 when 'Date'
43 format_date(value)
44 when 'Fixnum', 'Float'
45 if column.name == :done_ratio
46 progress_bar(value, :width => '80px')
49 else
47 else
50 case column.name
48 value.to_s
51 when :subject
52 h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
53 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
54 when :project
55 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
56 when :assigned_to
57 link_to_user value
58 when :author
59 link_to_user value
60 when :done_ratio
61 progress_bar(value, :width => '80px')
62 when :fixed_version
63 link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
64 else
65 h(value)
66 end
67 end
49 end
50 when 'User'
51 link_to_user value
52 when 'Project'
53 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
54 when 'Version'
55 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
56 when 'TrueClass'
57 l(:general_text_Yes)
58 when 'FalseClass'
59 l(:general_text_No)
60 else
61 h(value)
68 end
62 end
69 end
63 end
70 end
64 end
@@ -1,111 +1,130
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
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 CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 has_many :custom_values, :dependent => :delete_all
19 has_many :custom_values, :dependent => :delete_all
20 acts_as_list :scope => 'type = \'#{self.class}\''
20 acts_as_list :scope => 'type = \'#{self.class}\''
21 serialize :possible_values
21 serialize :possible_values
22
22
23 FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
23 FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
24 "text" => { :name => :label_text, :order => 2 },
24 "text" => { :name => :label_text, :order => 2 },
25 "int" => { :name => :label_integer, :order => 3 },
25 "int" => { :name => :label_integer, :order => 3 },
26 "float" => { :name => :label_float, :order => 4 },
26 "float" => { :name => :label_float, :order => 4 },
27 "list" => { :name => :label_list, :order => 5 },
27 "list" => { :name => :label_list, :order => 5 },
28 "date" => { :name => :label_date, :order => 6 },
28 "date" => { :name => :label_date, :order => 6 },
29 "bool" => { :name => :label_boolean, :order => 7 }
29 "bool" => { :name => :label_boolean, :order => 7 }
30 }.freeze
30 }.freeze
31
31
32 validates_presence_of :name, :field_format
32 validates_presence_of :name, :field_format
33 validates_uniqueness_of :name, :scope => :type
33 validates_uniqueness_of :name, :scope => :type
34 validates_length_of :name, :maximum => 30
34 validates_length_of :name, :maximum => 30
35 validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
35 validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
36 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
36 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
37
37
38 def initialize(attributes = nil)
38 def initialize(attributes = nil)
39 super
39 super
40 self.possible_values ||= []
40 self.possible_values ||= []
41 end
41 end
42
42
43 def before_validation
43 def before_validation
44 # make sure these fields are not searchable
44 # make sure these fields are not searchable
45 self.searchable = false if %w(int float date bool).include?(field_format)
45 self.searchable = false if %w(int float date bool).include?(field_format)
46 true
46 true
47 end
47 end
48
48
49 def validate
49 def validate
50 if self.field_format == "list"
50 if self.field_format == "list"
51 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
51 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
52 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
52 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
53 end
53 end
54
54
55 # validate default value
55 # validate default value
56 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
56 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
57 v.custom_field.is_required = false
57 v.custom_field.is_required = false
58 errors.add(:default_value, :invalid) unless v.valid?
58 errors.add(:default_value, :invalid) unless v.valid?
59 end
59 end
60
60
61 # Makes possible_values accept a multiline string
61 # Makes possible_values accept a multiline string
62 def possible_values=(arg)
62 def possible_values=(arg)
63 if arg.is_a?(Array)
63 if arg.is_a?(Array)
64 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
64 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
65 else
65 else
66 self.possible_values = arg.to_s.split(/[\n\r]+/)
66 self.possible_values = arg.to_s.split(/[\n\r]+/)
67 end
67 end
68 end
68 end
69
69
70 def cast_value(value)
71 casted = nil
72 unless value.blank?
73 case field_format
74 when 'string', 'text', 'list'
75 casted = value
76 when 'date'
77 casted = begin; value.to_date; rescue; nil end
78 when 'bool'
79 casted = (value == '1' ? true : false)
80 when 'int'
81 casted = value.to_i
82 when 'float'
83 casted = value.to_f
84 end
85 end
86 casted
87 end
88
70 # Returns a ORDER BY clause that can used to sort customized
89 # Returns a ORDER BY clause that can used to sort customized
71 # objects by their value of the custom field.
90 # objects by their value of the custom field.
72 # Returns false, if the custom field can not be used for sorting.
91 # Returns false, if the custom field can not be used for sorting.
73 def order_statement
92 def order_statement
74 case field_format
93 case field_format
75 when 'string', 'text', 'list', 'date', 'bool'
94 when 'string', 'text', 'list', 'date', 'bool'
76 # COALESCE is here to make sure that blank and NULL values are sorted equally
95 # COALESCE is here to make sure that blank and NULL values are sorted equally
77 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
96 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
78 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
97 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
79 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
98 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
80 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
99 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
81 when 'int', 'float'
100 when 'int', 'float'
82 # Make the database cast values into numeric
101 # Make the database cast values into numeric
83 # Postgresql will raise an error if a value can not be casted!
102 # Postgresql will raise an error if a value can not be casted!
84 # CustomValue validations should ensure that it doesn't occur
103 # CustomValue validations should ensure that it doesn't occur
85 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
104 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
86 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
105 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
87 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
106 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
88 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
107 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
89 else
108 else
90 nil
109 nil
91 end
110 end
92 end
111 end
93
112
94 def <=>(field)
113 def <=>(field)
95 position <=> field.position
114 position <=> field.position
96 end
115 end
97
116
98 def self.customized_class
117 def self.customized_class
99 self.name =~ /^(.+)CustomField$/
118 self.name =~ /^(.+)CustomField$/
100 begin; $1.constantize; rescue nil; end
119 begin; $1.constantize; rescue nil; end
101 end
120 end
102
121
103 # to move in project_custom_field
122 # to move in project_custom_field
104 def self.for_all
123 def self.for_all
105 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
124 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
106 end
125 end
107
126
108 def type_name
127 def type_name
109 nil
128 nil
110 end
129 end
111 end
130 end
@@ -1,545 +1,558
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
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 QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 end
30 end
31
31
32 def caption
32 def caption
33 l("field_#{name}")
33 l("field_#{name}")
34 end
34 end
35
35
36 # Returns true if the column is sortable, otherwise false
36 # Returns true if the column is sortable, otherwise false
37 def sortable?
37 def sortable?
38 !sortable.nil?
38 !sortable.nil?
39 end
39 end
40
41 def value(issue)
42 issue.send name
43 end
40 end
44 end
41
45
42 class QueryCustomFieldColumn < QueryColumn
46 class QueryCustomFieldColumn < QueryColumn
43
47
44 def initialize(custom_field)
48 def initialize(custom_field)
45 self.name = "cf_#{custom_field.id}".to_sym
49 self.name = "cf_#{custom_field.id}".to_sym
46 self.sortable = custom_field.order_statement || false
50 self.sortable = custom_field.order_statement || false
47 if %w(list date bool int).include?(custom_field.field_format)
51 if %w(list date bool int).include?(custom_field.field_format)
48 self.groupable = custom_field.order_statement
52 self.groupable = custom_field.order_statement
49 end
53 end
50 self.groupable ||= false
54 self.groupable ||= false
51 @cf = custom_field
55 @cf = custom_field
52 end
56 end
53
57
54 def caption
58 def caption
55 @cf.name
59 @cf.name
56 end
60 end
57
61
58 def custom_field
62 def custom_field
59 @cf
63 @cf
60 end
64 end
65
66 def value(issue)
67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv && @cf.cast_value(cv.value)
69 end
61 end
70 end
62
71
63 class Query < ActiveRecord::Base
72 class Query < ActiveRecord::Base
64 class StatementInvalid < ::ActiveRecord::StatementInvalid
73 class StatementInvalid < ::ActiveRecord::StatementInvalid
65 end
74 end
66
75
67 belongs_to :project
76 belongs_to :project
68 belongs_to :user
77 belongs_to :user
69 serialize :filters
78 serialize :filters
70 serialize :column_names
79 serialize :column_names
71 serialize :sort_criteria, Array
80 serialize :sort_criteria, Array
72
81
73 attr_protected :project_id, :user_id
82 attr_protected :project_id, :user_id
74
83
75 validates_presence_of :name, :on => :save
84 validates_presence_of :name, :on => :save
76 validates_length_of :name, :maximum => 255
85 validates_length_of :name, :maximum => 255
77
86
78 @@operators = { "=" => :label_equals,
87 @@operators = { "=" => :label_equals,
79 "!" => :label_not_equals,
88 "!" => :label_not_equals,
80 "o" => :label_open_issues,
89 "o" => :label_open_issues,
81 "c" => :label_closed_issues,
90 "c" => :label_closed_issues,
82 "!*" => :label_none,
91 "!*" => :label_none,
83 "*" => :label_all,
92 "*" => :label_all,
84 ">=" => :label_greater_or_equal,
93 ">=" => :label_greater_or_equal,
85 "<=" => :label_less_or_equal,
94 "<=" => :label_less_or_equal,
86 "<t+" => :label_in_less_than,
95 "<t+" => :label_in_less_than,
87 ">t+" => :label_in_more_than,
96 ">t+" => :label_in_more_than,
88 "t+" => :label_in,
97 "t+" => :label_in,
89 "t" => :label_today,
98 "t" => :label_today,
90 "w" => :label_this_week,
99 "w" => :label_this_week,
91 ">t-" => :label_less_than_ago,
100 ">t-" => :label_less_than_ago,
92 "<t-" => :label_more_than_ago,
101 "<t-" => :label_more_than_ago,
93 "t-" => :label_ago,
102 "t-" => :label_ago,
94 "~" => :label_contains,
103 "~" => :label_contains,
95 "!~" => :label_not_contains }
104 "!~" => :label_not_contains }
96
105
97 cattr_reader :operators
106 cattr_reader :operators
98
107
99 @@operators_by_filter_type = { :list => [ "=", "!" ],
108 @@operators_by_filter_type = { :list => [ "=", "!" ],
100 :list_status => [ "o", "=", "!", "c", "*" ],
109 :list_status => [ "o", "=", "!", "c", "*" ],
101 :list_optional => [ "=", "!", "!*", "*" ],
110 :list_optional => [ "=", "!", "!*", "*" ],
102 :list_subprojects => [ "*", "!*", "=" ],
111 :list_subprojects => [ "*", "!*", "=" ],
103 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
104 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
105 :string => [ "=", "~", "!", "!~" ],
114 :string => [ "=", "~", "!", "!~" ],
106 :text => [ "~", "!~" ],
115 :text => [ "~", "!~" ],
107 :integer => [ "=", ">=", "<=", "!*", "*" ] }
116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
108
117
109 cattr_reader :operators_by_filter_type
118 cattr_reader :operators_by_filter_type
110
119
111 @@available_columns = [
120 @@available_columns = [
112 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
113 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
114 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
115 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
116 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
117 QueryColumn.new(:author),
126 QueryColumn.new(:author),
118 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
119 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
120 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
121 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
122 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
123 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
124 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
125 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
126 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
127 ]
136 ]
128 cattr_reader :available_columns
137 cattr_reader :available_columns
129
138
130 def initialize(attributes = nil)
139 def initialize(attributes = nil)
131 super attributes
140 super attributes
132 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
133 end
142 end
134
143
135 def after_initialize
144 def after_initialize
136 # Store the fact that project is nil (used in #editable_by?)
145 # Store the fact that project is nil (used in #editable_by?)
137 @is_for_all = project.nil?
146 @is_for_all = project.nil?
138 end
147 end
139
148
140 def validate
149 def validate
141 filters.each_key do |field|
150 filters.each_key do |field|
142 errors.add label_for(field), :blank unless
151 errors.add label_for(field), :blank unless
143 # filter requires one or more values
152 # filter requires one or more values
144 (values_for(field) and !values_for(field).first.blank?) or
153 (values_for(field) and !values_for(field).first.blank?) or
145 # filter doesn't require any value
154 # filter doesn't require any value
146 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
147 end if filters
156 end if filters
148 end
157 end
149
158
150 def editable_by?(user)
159 def editable_by?(user)
151 return false unless user
160 return false unless user
152 # Admin can edit them all and regular users can edit their private queries
161 # Admin can edit them all and regular users can edit their private queries
153 return true if user.admin? || (!is_public && self.user_id == user.id)
162 return true if user.admin? || (!is_public && self.user_id == user.id)
154 # Members can not edit public queries that are for all project (only admin is allowed to)
163 # Members can not edit public queries that are for all project (only admin is allowed to)
155 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
156 end
165 end
157
166
158 def available_filters
167 def available_filters
159 return @available_filters if @available_filters
168 return @available_filters if @available_filters
160
169
161 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
162
171
163 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
164 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
165 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
166 "subject" => { :type => :text, :order => 8 },
175 "subject" => { :type => :text, :order => 8 },
167 "created_on" => { :type => :date_past, :order => 9 },
176 "created_on" => { :type => :date_past, :order => 9 },
168 "updated_on" => { :type => :date_past, :order => 10 },
177 "updated_on" => { :type => :date_past, :order => 10 },
169 "start_date" => { :type => :date, :order => 11 },
178 "start_date" => { :type => :date, :order => 11 },
170 "due_date" => { :type => :date, :order => 12 },
179 "due_date" => { :type => :date, :order => 12 },
171 "estimated_hours" => { :type => :integer, :order => 13 },
180 "estimated_hours" => { :type => :integer, :order => 13 },
172 "done_ratio" => { :type => :integer, :order => 14 }}
181 "done_ratio" => { :type => :integer, :order => 14 }}
173
182
174 user_values = []
183 user_values = []
175 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
176 if project
185 if project
177 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
178 else
187 else
179 # members of the user's projects
188 # members of the user's projects
180 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
189 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
181 end
190 end
182 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
191 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
183 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
192 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
184
193
185 if User.current.logged?
194 if User.current.logged?
186 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
195 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
187 end
196 end
188
197
189 if project
198 if project
190 # project specific filters
199 # project specific filters
191 unless @project.issue_categories.empty?
200 unless @project.issue_categories.empty?
192 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
201 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
193 end
202 end
194 unless @project.versions.empty?
203 unless @project.versions.empty?
195 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
204 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
196 end
205 end
197 unless @project.descendants.active.empty?
206 unless @project.descendants.active.empty?
198 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
207 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
199 end
208 end
200 add_custom_fields_filters(@project.all_issue_custom_fields)
209 add_custom_fields_filters(@project.all_issue_custom_fields)
201 else
210 else
202 # global filters for cross project issue list
211 # global filters for cross project issue list
203 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
212 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
204 end
213 end
205 @available_filters
214 @available_filters
206 end
215 end
207
216
208 def add_filter(field, operator, values)
217 def add_filter(field, operator, values)
209 # values must be an array
218 # values must be an array
210 return unless values and values.is_a? Array # and !values.first.empty?
219 return unless values and values.is_a? Array # and !values.first.empty?
211 # check if field is defined as an available filter
220 # check if field is defined as an available filter
212 if available_filters.has_key? field
221 if available_filters.has_key? field
213 filter_options = available_filters[field]
222 filter_options = available_filters[field]
214 # check if operator is allowed for that filter
223 # check if operator is allowed for that filter
215 #if @@operators_by_filter_type[filter_options[:type]].include? operator
224 #if @@operators_by_filter_type[filter_options[:type]].include? operator
216 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
225 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
217 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
226 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
218 #end
227 #end
219 filters[field] = {:operator => operator, :values => values }
228 filters[field] = {:operator => operator, :values => values }
220 end
229 end
221 end
230 end
222
231
223 def add_short_filter(field, expression)
232 def add_short_filter(field, expression)
224 return unless expression
233 return unless expression
225 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
234 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
226 add_filter field, (parms[0] || "="), [parms[1] || ""]
235 add_filter field, (parms[0] || "="), [parms[1] || ""]
227 end
236 end
228
237
229 def has_filter?(field)
238 def has_filter?(field)
230 filters and filters[field]
239 filters and filters[field]
231 end
240 end
232
241
233 def operator_for(field)
242 def operator_for(field)
234 has_filter?(field) ? filters[field][:operator] : nil
243 has_filter?(field) ? filters[field][:operator] : nil
235 end
244 end
236
245
237 def values_for(field)
246 def values_for(field)
238 has_filter?(field) ? filters[field][:values] : nil
247 has_filter?(field) ? filters[field][:values] : nil
239 end
248 end
240
249
241 def label_for(field)
250 def label_for(field)
242 label = available_filters[field][:name] if available_filters.has_key?(field)
251 label = available_filters[field][:name] if available_filters.has_key?(field)
243 label ||= field.gsub(/\_id$/, "")
252 label ||= field.gsub(/\_id$/, "")
244 end
253 end
245
254
246 def available_columns
255 def available_columns
247 return @available_columns if @available_columns
256 return @available_columns if @available_columns
248 @available_columns = Query.available_columns
257 @available_columns = Query.available_columns
249 @available_columns += (project ?
258 @available_columns += (project ?
250 project.all_issue_custom_fields :
259 project.all_issue_custom_fields :
251 IssueCustomField.find(:all)
260 IssueCustomField.find(:all)
252 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
261 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
253 end
262 end
254
263
255 # Returns an array of columns that can be used to group the results
264 # Returns an array of columns that can be used to group the results
256 def groupable_columns
265 def groupable_columns
257 available_columns.select {|c| c.groupable}
266 available_columns.select {|c| c.groupable}
258 end
267 end
259
268
260 def columns
269 def columns
261 if has_default_columns?
270 if has_default_columns?
262 available_columns.select do |c|
271 available_columns.select do |c|
263 # Adds the project column by default for cross-project lists
272 # Adds the project column by default for cross-project lists
264 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
273 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
265 end
274 end
266 else
275 else
267 # preserve the column_names order
276 # preserve the column_names order
268 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
277 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
269 end
278 end
270 end
279 end
271
280
272 def column_names=(names)
281 def column_names=(names)
273 if names
282 if names
274 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
283 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
275 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
284 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
276 # Set column_names to nil if default columns
285 # Set column_names to nil if default columns
277 if names.map(&:to_s) == Setting.issue_list_default_columns
286 if names.map(&:to_s) == Setting.issue_list_default_columns
278 names = nil
287 names = nil
279 end
288 end
280 end
289 end
281 write_attribute(:column_names, names)
290 write_attribute(:column_names, names)
282 end
291 end
283
292
284 def has_column?(column)
293 def has_column?(column)
285 column_names && column_names.include?(column.name)
294 column_names && column_names.include?(column.name)
286 end
295 end
287
296
288 def has_default_columns?
297 def has_default_columns?
289 column_names.nil? || column_names.empty?
298 column_names.nil? || column_names.empty?
290 end
299 end
291
300
292 def sort_criteria=(arg)
301 def sort_criteria=(arg)
293 c = []
302 c = []
294 if arg.is_a?(Hash)
303 if arg.is_a?(Hash)
295 arg = arg.keys.sort.collect {|k| arg[k]}
304 arg = arg.keys.sort.collect {|k| arg[k]}
296 end
305 end
297 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
306 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
298 write_attribute(:sort_criteria, c)
307 write_attribute(:sort_criteria, c)
299 end
308 end
300
309
301 def sort_criteria
310 def sort_criteria
302 read_attribute(:sort_criteria) || []
311 read_attribute(:sort_criteria) || []
303 end
312 end
304
313
305 def sort_criteria_key(arg)
314 def sort_criteria_key(arg)
306 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
315 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
307 end
316 end
308
317
309 def sort_criteria_order(arg)
318 def sort_criteria_order(arg)
310 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
319 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
311 end
320 end
312
321
313 # Returns the SQL sort order that should be prepended for grouping
322 # Returns the SQL sort order that should be prepended for grouping
314 def group_by_sort_order
323 def group_by_sort_order
315 if grouped? && (column = group_by_column)
324 if grouped? && (column = group_by_column)
316 column.sortable.is_a?(Array) ?
325 column.sortable.is_a?(Array) ?
317 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
326 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
318 "#{column.sortable} #{column.default_order}"
327 "#{column.sortable} #{column.default_order}"
319 end
328 end
320 end
329 end
321
330
322 # Returns true if the query is a grouped query
331 # Returns true if the query is a grouped query
323 def grouped?
332 def grouped?
324 !group_by.blank?
333 !group_by.blank?
325 end
334 end
326
335
327 def group_by_column
336 def group_by_column
328 groupable_columns.detect {|c| c.name.to_s == group_by}
337 groupable_columns.detect {|c| c.name.to_s == group_by}
329 end
338 end
330
339
331 def group_by_statement
340 def group_by_statement
332 group_by_column.groupable
341 group_by_column.groupable
333 end
342 end
334
343
335 def project_statement
344 def project_statement
336 project_clauses = []
345 project_clauses = []
337 if project && !@project.descendants.active.empty?
346 if project && !@project.descendants.active.empty?
338 ids = [project.id]
347 ids = [project.id]
339 if has_filter?("subproject_id")
348 if has_filter?("subproject_id")
340 case operator_for("subproject_id")
349 case operator_for("subproject_id")
341 when '='
350 when '='
342 # include the selected subprojects
351 # include the selected subprojects
343 ids += values_for("subproject_id").each(&:to_i)
352 ids += values_for("subproject_id").each(&:to_i)
344 when '!*'
353 when '!*'
345 # main project only
354 # main project only
346 else
355 else
347 # all subprojects
356 # all subprojects
348 ids += project.descendants.collect(&:id)
357 ids += project.descendants.collect(&:id)
349 end
358 end
350 elsif Setting.display_subprojects_issues?
359 elsif Setting.display_subprojects_issues?
351 ids += project.descendants.collect(&:id)
360 ids += project.descendants.collect(&:id)
352 end
361 end
353 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
362 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
354 elsif project
363 elsif project
355 project_clauses << "#{Project.table_name}.id = %d" % project.id
364 project_clauses << "#{Project.table_name}.id = %d" % project.id
356 end
365 end
357 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
366 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
358 project_clauses.join(' AND ')
367 project_clauses.join(' AND ')
359 end
368 end
360
369
361 def statement
370 def statement
362 # filters clauses
371 # filters clauses
363 filters_clauses = []
372 filters_clauses = []
364 filters.each_key do |field|
373 filters.each_key do |field|
365 next if field == "subproject_id"
374 next if field == "subproject_id"
366 v = values_for(field).clone
375 v = values_for(field).clone
367 next unless v and !v.empty?
376 next unless v and !v.empty?
368 operator = operator_for(field)
377 operator = operator_for(field)
369
378
370 # "me" value subsitution
379 # "me" value subsitution
371 if %w(assigned_to_id author_id watcher_id).include?(field)
380 if %w(assigned_to_id author_id watcher_id).include?(field)
372 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
381 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
373 end
382 end
374
383
375 sql = ''
384 sql = ''
376 if field =~ /^cf_(\d+)$/
385 if field =~ /^cf_(\d+)$/
377 # custom field
386 # custom field
378 db_table = CustomValue.table_name
387 db_table = CustomValue.table_name
379 db_field = 'value'
388 db_field = 'value'
380 is_custom_filter = true
389 is_custom_filter = true
381 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
390 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
382 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
391 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
383 elsif field == 'watcher_id'
392 elsif field == 'watcher_id'
384 db_table = Watcher.table_name
393 db_table = Watcher.table_name
385 db_field = 'user_id'
394 db_field = 'user_id'
386 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
395 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
387 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
396 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
388 else
397 else
389 # regular field
398 # regular field
390 db_table = Issue.table_name
399 db_table = Issue.table_name
391 db_field = field
400 db_field = field
392 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
401 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
393 end
402 end
394 filters_clauses << sql
403 filters_clauses << sql
395
404
396 end if filters and valid?
405 end if filters and valid?
397
406
398 (filters_clauses << project_statement).join(' AND ')
407 (filters_clauses << project_statement).join(' AND ')
399 end
408 end
400
409
401 # Returns the issue count
410 # Returns the issue count
402 def issue_count
411 def issue_count
403 Issue.count(:include => [:status, :project], :conditions => statement)
412 Issue.count(:include => [:status, :project], :conditions => statement)
404 rescue ::ActiveRecord::StatementInvalid => e
413 rescue ::ActiveRecord::StatementInvalid => e
405 raise StatementInvalid.new(e.message)
414 raise StatementInvalid.new(e.message)
406 end
415 end
407
416
408 # Returns the issue count by group or nil if query is not grouped
417 # Returns the issue count by group or nil if query is not grouped
409 def issue_count_by_group
418 def issue_count_by_group
419 r = nil
410 if grouped?
420 if grouped?
411 begin
421 begin
412 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
422 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
413 Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
423 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
414 rescue ActiveRecord::RecordNotFound
424 rescue ActiveRecord::RecordNotFound
415 {nil => issue_count}
425 r = {nil => issue_count}
426 end
427 c = group_by_column
428 if c.is_a?(QueryCustomFieldColumn)
429 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
416 end
430 end
417 else
418 nil
419 end
431 end
432 r
420 rescue ::ActiveRecord::StatementInvalid => e
433 rescue ::ActiveRecord::StatementInvalid => e
421 raise StatementInvalid.new(e.message)
434 raise StatementInvalid.new(e.message)
422 end
435 end
423
436
424 # Returns the issues
437 # Returns the issues
425 # Valid options are :order, :offset, :limit, :include, :conditions
438 # Valid options are :order, :offset, :limit, :include, :conditions
426 def issues(options={})
439 def issues(options={})
427 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
440 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
428 order_option = nil if order_option.blank?
441 order_option = nil if order_option.blank?
429
442
430 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
443 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
431 :conditions => Query.merge_conditions(statement, options[:conditions]),
444 :conditions => Query.merge_conditions(statement, options[:conditions]),
432 :order => order_option,
445 :order => order_option,
433 :limit => options[:limit],
446 :limit => options[:limit],
434 :offset => options[:offset]
447 :offset => options[:offset]
435 rescue ::ActiveRecord::StatementInvalid => e
448 rescue ::ActiveRecord::StatementInvalid => e
436 raise StatementInvalid.new(e.message)
449 raise StatementInvalid.new(e.message)
437 end
450 end
438
451
439 # Returns the journals
452 # Returns the journals
440 # Valid options are :order, :offset, :limit
453 # Valid options are :order, :offset, :limit
441 def journals(options={})
454 def journals(options={})
442 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
455 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
443 :conditions => statement,
456 :conditions => statement,
444 :order => options[:order],
457 :order => options[:order],
445 :limit => options[:limit],
458 :limit => options[:limit],
446 :offset => options[:offset]
459 :offset => options[:offset]
447 rescue ::ActiveRecord::StatementInvalid => e
460 rescue ::ActiveRecord::StatementInvalid => e
448 raise StatementInvalid.new(e.message)
461 raise StatementInvalid.new(e.message)
449 end
462 end
450
463
451 # Returns the versions
464 # Returns the versions
452 # Valid options are :conditions
465 # Valid options are :conditions
453 def versions(options={})
466 def versions(options={})
454 Version.find :all, :include => :project,
467 Version.find :all, :include => :project,
455 :conditions => Query.merge_conditions(project_statement, options[:conditions])
468 :conditions => Query.merge_conditions(project_statement, options[:conditions])
456 rescue ::ActiveRecord::StatementInvalid => e
469 rescue ::ActiveRecord::StatementInvalid => e
457 raise StatementInvalid.new(e.message)
470 raise StatementInvalid.new(e.message)
458 end
471 end
459
472
460 private
473 private
461
474
462 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
475 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
463 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
476 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
464 sql = ''
477 sql = ''
465 case operator
478 case operator
466 when "="
479 when "="
467 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
480 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
468 when "!"
481 when "!"
469 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
482 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
470 when "!*"
483 when "!*"
471 sql = "#{db_table}.#{db_field} IS NULL"
484 sql = "#{db_table}.#{db_field} IS NULL"
472 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
485 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
473 when "*"
486 when "*"
474 sql = "#{db_table}.#{db_field} IS NOT NULL"
487 sql = "#{db_table}.#{db_field} IS NOT NULL"
475 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
488 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
476 when ">="
489 when ">="
477 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
490 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
478 when "<="
491 when "<="
479 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
492 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
480 when "o"
493 when "o"
481 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
494 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
482 when "c"
495 when "c"
483 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
496 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
484 when ">t-"
497 when ">t-"
485 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
498 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
486 when "<t-"
499 when "<t-"
487 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
500 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
488 when "t-"
501 when "t-"
489 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
502 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
490 when ">t+"
503 when ">t+"
491 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
504 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
492 when "<t+"
505 when "<t+"
493 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
506 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
494 when "t+"
507 when "t+"
495 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
508 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
496 when "t"
509 when "t"
497 sql = date_range_clause(db_table, db_field, 0, 0)
510 sql = date_range_clause(db_table, db_field, 0, 0)
498 when "w"
511 when "w"
499 from = l(:general_first_day_of_week) == '7' ?
512 from = l(:general_first_day_of_week) == '7' ?
500 # week starts on sunday
513 # week starts on sunday
501 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
514 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
502 # week starts on monday (Rails default)
515 # week starts on monday (Rails default)
503 Time.now.at_beginning_of_week
516 Time.now.at_beginning_of_week
504 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
517 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
505 when "~"
518 when "~"
506 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
519 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
507 when "!~"
520 when "!~"
508 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
521 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
509 end
522 end
510
523
511 return sql
524 return sql
512 end
525 end
513
526
514 def add_custom_fields_filters(custom_fields)
527 def add_custom_fields_filters(custom_fields)
515 @available_filters ||= {}
528 @available_filters ||= {}
516
529
517 custom_fields.select(&:is_filter?).each do |field|
530 custom_fields.select(&:is_filter?).each do |field|
518 case field.field_format
531 case field.field_format
519 when "text"
532 when "text"
520 options = { :type => :text, :order => 20 }
533 options = { :type => :text, :order => 20 }
521 when "list"
534 when "list"
522 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
535 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
523 when "date"
536 when "date"
524 options = { :type => :date, :order => 20 }
537 options = { :type => :date, :order => 20 }
525 when "bool"
538 when "bool"
526 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
539 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
527 else
540 else
528 options = { :type => :string, :order => 20 }
541 options = { :type => :string, :order => 20 }
529 end
542 end
530 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
543 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
531 end
544 end
532 end
545 end
533
546
534 # Returns a SQL clause for a date or datetime field.
547 # Returns a SQL clause for a date or datetime field.
535 def date_range_clause(table, field, from, to)
548 def date_range_clause(table, field, from, to)
536 s = []
549 s = []
537 if from
550 if from
538 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
551 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
539 end
552 end
540 if to
553 if to
541 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
554 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
542 end
555 end
543 s.join(' AND ')
556 s.join(' AND ')
544 end
557 end
545 end
558 end
@@ -1,34 +1,34
1 <% form_tag({}) do -%>
1 <% form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 <table class="list issues">
3 <table class="list issues">
4 <thead><tr>
4 <thead><tr>
5 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
5 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
7 </th>
7 </th>
8 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
8 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
9 <% query.columns.each do |column| %>
9 <% query.columns.each do |column| %>
10 <%= column_header(column) %>
10 <%= column_header(column) %>
11 <% end %>
11 <% end %>
12 </tr></thead>
12 </tr></thead>
13 <% previous_group = false %>
13 <% previous_group = false %>
14 <tbody>
14 <tbody>
15 <% issues.each do |issue| -%>
15 <% issues.each do |issue| -%>
16 <% if @query.grouped? && (group = column_value(@query.group_by_column, issue) || '') != previous_group %>
16 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
17 <% reset_cycle %>
17 <% reset_cycle %>
18 <tr class="group open">
18 <tr class="group open">
19 <td colspan="<%= query.columns.size + 2 %>">
19 <td colspan="<%= query.columns.size + 2 %>">
20 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
20 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
21 <%= group.blank? ? 'None' : group %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
21 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
22 </td>
22 </td>
23 </tr>
23 </tr>
24 <% previous_group = group %>
24 <% previous_group = group %>
25 <% end %>
25 <% end %>
26 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
26 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
27 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
27 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
28 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
28 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
29 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
29 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
30 </tr>
30 </tr>
31 <% end -%>
31 <% end -%>
32 </tbody>
32 </tbody>
33 </table>
33 </table>
34 <% end -%>
34 <% end -%>
@@ -1,103 +1,117
1 ---
1 ---
2 custom_fields_001:
2 custom_fields_001:
3 name: Database
3 name: Database
4 min_length: 0
4 min_length: 0
5 regexp: ""
5 regexp: ""
6 is_for_all: true
6 is_for_all: true
7 is_filter: true
7 is_filter: true
8 type: IssueCustomField
8 type: IssueCustomField
9 max_length: 0
9 max_length: 0
10 possible_values:
10 possible_values:
11 - MySQL
11 - MySQL
12 - PostgreSQL
12 - PostgreSQL
13 - Oracle
13 - Oracle
14 id: 1
14 id: 1
15 is_required: false
15 is_required: false
16 field_format: list
16 field_format: list
17 default_value: ""
17 default_value: ""
18 editable: true
18 editable: true
19 custom_fields_002:
19 custom_fields_002:
20 name: Searchable field
20 name: Searchable field
21 min_length: 1
21 min_length: 1
22 regexp: ""
22 regexp: ""
23 is_for_all: true
23 is_for_all: true
24 type: IssueCustomField
24 type: IssueCustomField
25 max_length: 100
25 max_length: 100
26 possible_values: ""
26 possible_values: ""
27 id: 2
27 id: 2
28 is_required: false
28 is_required: false
29 field_format: string
29 field_format: string
30 searchable: true
30 searchable: true
31 default_value: "Default string"
31 default_value: "Default string"
32 editable: true
32 editable: true
33 custom_fields_003:
33 custom_fields_003:
34 name: Development status
34 name: Development status
35 min_length: 0
35 min_length: 0
36 regexp: ""
36 regexp: ""
37 is_for_all: false
37 is_for_all: false
38 is_filter: true
38 is_filter: true
39 type: ProjectCustomField
39 type: ProjectCustomField
40 max_length: 0
40 max_length: 0
41 possible_values:
41 possible_values:
42 - Stable
42 - Stable
43 - Beta
43 - Beta
44 - Alpha
44 - Alpha
45 - Planning
45 - Planning
46 id: 3
46 id: 3
47 is_required: true
47 is_required: true
48 field_format: list
48 field_format: list
49 default_value: ""
49 default_value: ""
50 editable: true
50 editable: true
51 custom_fields_004:
51 custom_fields_004:
52 name: Phone number
52 name: Phone number
53 min_length: 0
53 min_length: 0
54 regexp: ""
54 regexp: ""
55 is_for_all: false
55 is_for_all: false
56 type: UserCustomField
56 type: UserCustomField
57 max_length: 0
57 max_length: 0
58 possible_values: ""
58 possible_values: ""
59 id: 4
59 id: 4
60 is_required: false
60 is_required: false
61 field_format: string
61 field_format: string
62 default_value: ""
62 default_value: ""
63 editable: true
63 editable: true
64 custom_fields_005:
64 custom_fields_005:
65 name: Money
65 name: Money
66 min_length: 0
66 min_length: 0
67 regexp: ""
67 regexp: ""
68 is_for_all: false
68 is_for_all: false
69 type: UserCustomField
69 type: UserCustomField
70 max_length: 0
70 max_length: 0
71 possible_values: ""
71 possible_values: ""
72 id: 5
72 id: 5
73 is_required: false
73 is_required: false
74 field_format: float
74 field_format: float
75 default_value: ""
75 default_value: ""
76 editable: true
76 editable: true
77 custom_fields_006:
77 custom_fields_006:
78 name: Float field
78 name: Float field
79 min_length: 0
79 min_length: 0
80 regexp: ""
80 regexp: ""
81 is_for_all: true
81 is_for_all: true
82 type: IssueCustomField
82 type: IssueCustomField
83 max_length: 0
83 max_length: 0
84 possible_values: ""
84 possible_values: ""
85 id: 6
85 id: 6
86 is_required: false
86 is_required: false
87 field_format: float
87 field_format: float
88 default_value: ""
88 default_value: ""
89 editable: true
89 editable: true
90 custom_fields_007:
90 custom_fields_007:
91 name: Billable
91 name: Billable
92 min_length: 0
92 min_length: 0
93 regexp: ""
93 regexp: ""
94 is_for_all: false
94 is_for_all: false
95 is_filter: true
95 is_filter: true
96 type: TimeEntryActivityCustomField
96 type: TimeEntryActivityCustomField
97 max_length: 0
97 max_length: 0
98 possible_values: ""
98 possible_values: ""
99 id: 7
99 id: 7
100 is_required: false
100 is_required: false
101 field_format: bool
101 field_format: bool
102 default_value: ""
102 default_value: ""
103 editable: true
103 editable: true
104 custom_fields_008:
105 name: Custom date
106 min_length: 0
107 regexp: ""
108 is_for_all: true
109 is_filter: false
110 type: IssueCustomField
111 max_length: 0
112 possible_values: ""
113 id: 8
114 is_required: false
115 field_format: date
116 default_value: ""
117 editable: true
@@ -1,97 +1,103
1 ---
1 ---
2 custom_values_006:
2 custom_values_006:
3 customized_type: Issue
3 customized_type: Issue
4 custom_field_id: 2
4 custom_field_id: 2
5 customized_id: 3
5 customized_id: 3
6 id: 6
6 id: 6
7 value: "125"
7 value: "125"
8 custom_values_007:
8 custom_values_007:
9 customized_type: Project
9 customized_type: Project
10 custom_field_id: 3
10 custom_field_id: 3
11 customized_id: 1
11 customized_id: 1
12 id: 7
12 id: 7
13 value: Stable
13 value: Stable
14 custom_values_001:
14 custom_values_001:
15 customized_type: Principal
15 customized_type: Principal
16 custom_field_id: 4
16 custom_field_id: 4
17 customized_id: 3
17 customized_id: 3
18 id: 1
18 id: 1
19 value: ""
19 value: ""
20 custom_values_002:
20 custom_values_002:
21 customized_type: Principal
21 customized_type: Principal
22 custom_field_id: 4
22 custom_field_id: 4
23 customized_id: 4
23 customized_id: 4
24 id: 2
24 id: 2
25 value: 01 23 45 67 89
25 value: 01 23 45 67 89
26 custom_values_003:
26 custom_values_003:
27 customized_type: Principal
27 customized_type: Principal
28 custom_field_id: 4
28 custom_field_id: 4
29 customized_id: 2
29 customized_id: 2
30 id: 3
30 id: 3
31 value: ""
31 value: ""
32 custom_values_004:
32 custom_values_004:
33 customized_type: Issue
33 customized_type: Issue
34 custom_field_id: 2
34 custom_field_id: 2
35 customized_id: 1
35 customized_id: 1
36 id: 4
36 id: 4
37 value: "125"
37 value: "125"
38 custom_values_005:
38 custom_values_005:
39 customized_type: Issue
39 customized_type: Issue
40 custom_field_id: 2
40 custom_field_id: 2
41 customized_id: 2
41 customized_id: 2
42 id: 5
42 id: 5
43 value: ""
43 value: ""
44 custom_values_008:
44 custom_values_008:
45 customized_type: Issue
45 customized_type: Issue
46 custom_field_id: 1
46 custom_field_id: 1
47 customized_id: 3
47 customized_id: 3
48 id: 8
48 id: 8
49 value: "MySQL"
49 value: "MySQL"
50 custom_values_009:
50 custom_values_009:
51 customized_type: Issue
51 customized_type: Issue
52 custom_field_id: 2
52 custom_field_id: 2
53 customized_id: 3
53 customized_id: 3
54 id: 9
54 id: 9
55 value: "this is a stringforcustomfield search"
55 value: "this is a stringforcustomfield search"
56 custom_values_010:
56 custom_values_010:
57 customized_type: Issue
57 customized_type: Issue
58 custom_field_id: 6
58 custom_field_id: 6
59 customized_id: 1
59 customized_id: 1
60 id: 10
60 id: 10
61 value: "2.1"
61 value: "2.1"
62 custom_values_011:
62 custom_values_011:
63 customized_type: Issue
63 customized_type: Issue
64 custom_field_id: 6
64 custom_field_id: 6
65 customized_id: 2
65 customized_id: 2
66 id: 11
66 id: 11
67 value: "2.05"
67 value: "2.05"
68 custom_values_012:
68 custom_values_012:
69 customized_type: Issue
69 customized_type: Issue
70 custom_field_id: 6
70 custom_field_id: 6
71 customized_id: 3
71 customized_id: 3
72 id: 12
72 id: 12
73 value: "11.65"
73 value: "11.65"
74 custom_values_013:
74 custom_values_013:
75 customized_type: Issue
75 customized_type: Issue
76 custom_field_id: 6
76 custom_field_id: 6
77 customized_id: 7
77 customized_id: 7
78 id: 13
78 id: 13
79 value: ""
79 value: ""
80 custom_values_014:
80 custom_values_014:
81 customized_type: Issue
81 customized_type: Issue
82 custom_field_id: 6
82 custom_field_id: 6
83 customized_id: 5
83 customized_id: 5
84 id: 14
84 id: 14
85 value: "-7.6"
85 value: "-7.6"
86 custom_values_015:
86 custom_values_015:
87 customized_type: Enumeration
87 customized_type: Enumeration
88 custom_field_id: 7
88 custom_field_id: 7
89 customized_id: 10
89 customized_id: 10
90 id: 15
90 id: 15
91 value: true
91 value: true
92 custom_values_016:
92 custom_values_016:
93 customized_type: Enumeration
93 customized_type: Enumeration
94 custom_field_id: 7
94 custom_field_id: 7
95 customized_id: 11
95 customized_id: 11
96 id: 16
96 id: 16
97 value: '1'
97 value: '1'
98 custom_values_017:
99 customized_type: Issue
100 custom_field_id: 8
101 customized_id: 1
102 id: 17
103 value: '2009-12-01'
@@ -1,1200 +1,1194
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
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 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < ActionController::TestCase
24 class IssuesControllerTest < ActionController::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :member_roles,
29 :member_roles,
30 :issues,
30 :issues,
31 :issue_statuses,
31 :issue_statuses,
32 :versions,
32 :versions,
33 :trackers,
33 :trackers,
34 :projects_trackers,
34 :projects_trackers,
35 :issue_categories,
35 :issue_categories,
36 :enabled_modules,
36 :enabled_modules,
37 :enumerations,
37 :enumerations,
38 :attachments,
38 :attachments,
39 :workflows,
39 :workflows,
40 :custom_fields,
40 :custom_fields,
41 :custom_values,
41 :custom_values,
42 :custom_fields_projects,
42 :custom_fields_projects,
43 :custom_fields_trackers,
43 :custom_fields_trackers,
44 :time_entries,
44 :time_entries,
45 :journals,
45 :journals,
46 :journal_details,
46 :journal_details,
47 :queries
47 :queries
48
48
49 def setup
49 def setup
50 @controller = IssuesController.new
50 @controller = IssuesController.new
51 @request = ActionController::TestRequest.new
51 @request = ActionController::TestRequest.new
52 @response = ActionController::TestResponse.new
52 @response = ActionController::TestResponse.new
53 User.current = nil
53 User.current = nil
54 end
54 end
55
55
56 def test_index_routing
56 def test_index_routing
57 assert_routing(
57 assert_routing(
58 {:method => :get, :path => '/issues'},
58 {:method => :get, :path => '/issues'},
59 :controller => 'issues', :action => 'index'
59 :controller => 'issues', :action => 'index'
60 )
60 )
61 end
61 end
62
62
63 def test_index
63 def test_index
64 Setting.default_language = 'en'
64 Setting.default_language = 'en'
65
65
66 get :index
66 get :index
67 assert_response :success
67 assert_response :success
68 assert_template 'index.rhtml'
68 assert_template 'index.rhtml'
69 assert_not_nil assigns(:issues)
69 assert_not_nil assigns(:issues)
70 assert_nil assigns(:project)
70 assert_nil assigns(:project)
71 assert_tag :tag => 'a', :content => /Can't print recipes/
71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 assert_tag :tag => 'a', :content => /Subproject issue/
72 assert_tag :tag => 'a', :content => /Subproject issue/
73 # private projects hidden
73 # private projects hidden
74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
76 # project column
76 # project column
77 assert_tag :tag => 'th', :content => /Project/
77 assert_tag :tag => 'th', :content => /Project/
78 end
78 end
79
79
80 def test_index_should_not_list_issues_when_module_disabled
80 def test_index_should_not_list_issues_when_module_disabled
81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
82 get :index
82 get :index
83 assert_response :success
83 assert_response :success
84 assert_template 'index.rhtml'
84 assert_template 'index.rhtml'
85 assert_not_nil assigns(:issues)
85 assert_not_nil assigns(:issues)
86 assert_nil assigns(:project)
86 assert_nil assigns(:project)
87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
88 assert_tag :tag => 'a', :content => /Subproject issue/
88 assert_tag :tag => 'a', :content => /Subproject issue/
89 end
89 end
90
90
91 def test_index_with_project_routing
91 def test_index_with_project_routing
92 assert_routing(
92 assert_routing(
93 {:method => :get, :path => '/projects/23/issues'},
93 {:method => :get, :path => '/projects/23/issues'},
94 :controller => 'issues', :action => 'index', :project_id => '23'
94 :controller => 'issues', :action => 'index', :project_id => '23'
95 )
95 )
96 end
96 end
97
97
98 def test_index_should_not_list_issues_when_module_disabled
98 def test_index_should_not_list_issues_when_module_disabled
99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
100 get :index
100 get :index
101 assert_response :success
101 assert_response :success
102 assert_template 'index.rhtml'
102 assert_template 'index.rhtml'
103 assert_not_nil assigns(:issues)
103 assert_not_nil assigns(:issues)
104 assert_nil assigns(:project)
104 assert_nil assigns(:project)
105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
106 assert_tag :tag => 'a', :content => /Subproject issue/
106 assert_tag :tag => 'a', :content => /Subproject issue/
107 end
107 end
108
108
109 def test_index_with_project_routing
109 def test_index_with_project_routing
110 assert_routing(
110 assert_routing(
111 {:method => :get, :path => 'projects/23/issues'},
111 {:method => :get, :path => 'projects/23/issues'},
112 :controller => 'issues', :action => 'index', :project_id => '23'
112 :controller => 'issues', :action => 'index', :project_id => '23'
113 )
113 )
114 end
114 end
115
115
116 def test_index_with_project
116 def test_index_with_project
117 Setting.display_subprojects_issues = 0
117 Setting.display_subprojects_issues = 0
118 get :index, :project_id => 1
118 get :index, :project_id => 1
119 assert_response :success
119 assert_response :success
120 assert_template 'index.rhtml'
120 assert_template 'index.rhtml'
121 assert_not_nil assigns(:issues)
121 assert_not_nil assigns(:issues)
122 assert_tag :tag => 'a', :content => /Can't print recipes/
122 assert_tag :tag => 'a', :content => /Can't print recipes/
123 assert_no_tag :tag => 'a', :content => /Subproject issue/
123 assert_no_tag :tag => 'a', :content => /Subproject issue/
124 end
124 end
125
125
126 def test_index_with_project_and_subprojects
126 def test_index_with_project_and_subprojects
127 Setting.display_subprojects_issues = 1
127 Setting.display_subprojects_issues = 1
128 get :index, :project_id => 1
128 get :index, :project_id => 1
129 assert_response :success
129 assert_response :success
130 assert_template 'index.rhtml'
130 assert_template 'index.rhtml'
131 assert_not_nil assigns(:issues)
131 assert_not_nil assigns(:issues)
132 assert_tag :tag => 'a', :content => /Can't print recipes/
132 assert_tag :tag => 'a', :content => /Can't print recipes/
133 assert_tag :tag => 'a', :content => /Subproject issue/
133 assert_tag :tag => 'a', :content => /Subproject issue/
134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
135 end
135 end
136
136
137 def test_index_with_project_and_subprojects_should_show_private_subprojects
137 def test_index_with_project_and_subprojects_should_show_private_subprojects
138 @request.session[:user_id] = 2
138 @request.session[:user_id] = 2
139 Setting.display_subprojects_issues = 1
139 Setting.display_subprojects_issues = 1
140 get :index, :project_id => 1
140 get :index, :project_id => 1
141 assert_response :success
141 assert_response :success
142 assert_template 'index.rhtml'
142 assert_template 'index.rhtml'
143 assert_not_nil assigns(:issues)
143 assert_not_nil assigns(:issues)
144 assert_tag :tag => 'a', :content => /Can't print recipes/
144 assert_tag :tag => 'a', :content => /Can't print recipes/
145 assert_tag :tag => 'a', :content => /Subproject issue/
145 assert_tag :tag => 'a', :content => /Subproject issue/
146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
147 end
147 end
148
148
149 def test_index_with_project_routing_formatted
149 def test_index_with_project_routing_formatted
150 assert_routing(
150 assert_routing(
151 {:method => :get, :path => 'projects/23/issues.pdf'},
151 {:method => :get, :path => 'projects/23/issues.pdf'},
152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
153 )
153 )
154 assert_routing(
154 assert_routing(
155 {:method => :get, :path => 'projects/23/issues.atom'},
155 {:method => :get, :path => 'projects/23/issues.atom'},
156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
157 )
157 )
158 end
158 end
159
159
160 def test_index_with_project_and_filter
160 def test_index_with_project_and_filter
161 get :index, :project_id => 1, :set_filter => 1
161 get :index, :project_id => 1, :set_filter => 1
162 assert_response :success
162 assert_response :success
163 assert_template 'index.rhtml'
163 assert_template 'index.rhtml'
164 assert_not_nil assigns(:issues)
164 assert_not_nil assigns(:issues)
165 end
165 end
166
166
167 def test_index_with_query
167 def test_index_with_query
168 get :index, :project_id => 1, :query_id => 5
168 get :index, :project_id => 1, :query_id => 5
169 assert_response :success
169 assert_response :success
170 assert_template 'index.rhtml'
170 assert_template 'index.rhtml'
171 assert_not_nil assigns(:issues)
171 assert_not_nil assigns(:issues)
172 assert_nil assigns(:issue_count_by_group)
172 assert_nil assigns(:issue_count_by_group)
173 end
173 end
174
174
175 def test_index_with_query_grouped_by_tracker
175 def test_index_with_query_grouped_by_tracker
176 get :index, :project_id => 1, :query_id => 6
176 get :index, :project_id => 1, :query_id => 6
177 assert_response :success
177 assert_response :success
178 assert_template 'index.rhtml'
178 assert_template 'index.rhtml'
179 assert_not_nil assigns(:issues)
179 assert_not_nil assigns(:issues)
180 count_by_group = assigns(:issue_count_by_group)
180 assert_not_nil assigns(:issue_count_by_group)
181 assert_kind_of Hash, count_by_group
182 assert_kind_of Tracker, count_by_group.keys.first
183 assert_not_nil count_by_group[Tracker.find(1)]
184 end
181 end
185
182
186 def test_index_with_query_grouped_by_list_custom_field
183 def test_index_with_query_grouped_by_list_custom_field
187 get :index, :project_id => 1, :query_id => 9
184 get :index, :project_id => 1, :query_id => 9
188 assert_response :success
185 assert_response :success
189 assert_template 'index.rhtml'
186 assert_template 'index.rhtml'
190 assert_not_nil assigns(:issues)
187 assert_not_nil assigns(:issues)
191 count_by_group = assigns(:issue_count_by_group)
188 assert_not_nil assigns(:issue_count_by_group)
192 assert_kind_of Hash, count_by_group
193 assert_kind_of String, count_by_group.keys.first
194 assert_not_nil count_by_group['MySQL']
195 end
189 end
196
190
197 def test_index_sort_by_field_not_included_in_columns
191 def test_index_sort_by_field_not_included_in_columns
198 Setting.issue_list_default_columns = %w(subject author)
192 Setting.issue_list_default_columns = %w(subject author)
199 get :index, :sort => 'tracker'
193 get :index, :sort => 'tracker'
200 end
194 end
201
195
202 def test_index_csv_with_project
196 def test_index_csv_with_project
203 Setting.default_language = 'en'
197 Setting.default_language = 'en'
204
198
205 get :index, :format => 'csv'
199 get :index, :format => 'csv'
206 assert_response :success
200 assert_response :success
207 assert_not_nil assigns(:issues)
201 assert_not_nil assigns(:issues)
208 assert_equal 'text/csv', @response.content_type
202 assert_equal 'text/csv', @response.content_type
209 assert @response.body.starts_with?("#,")
203 assert @response.body.starts_with?("#,")
210
204
211 get :index, :project_id => 1, :format => 'csv'
205 get :index, :project_id => 1, :format => 'csv'
212 assert_response :success
206 assert_response :success
213 assert_not_nil assigns(:issues)
207 assert_not_nil assigns(:issues)
214 assert_equal 'text/csv', @response.content_type
208 assert_equal 'text/csv', @response.content_type
215 end
209 end
216
210
217 def test_index_formatted
211 def test_index_formatted
218 assert_routing(
212 assert_routing(
219 {:method => :get, :path => 'issues.pdf'},
213 {:method => :get, :path => 'issues.pdf'},
220 :controller => 'issues', :action => 'index', :format => 'pdf'
214 :controller => 'issues', :action => 'index', :format => 'pdf'
221 )
215 )
222 assert_routing(
216 assert_routing(
223 {:method => :get, :path => 'issues.atom'},
217 {:method => :get, :path => 'issues.atom'},
224 :controller => 'issues', :action => 'index', :format => 'atom'
218 :controller => 'issues', :action => 'index', :format => 'atom'
225 )
219 )
226 end
220 end
227
221
228 def test_index_pdf
222 def test_index_pdf
229 get :index, :format => 'pdf'
223 get :index, :format => 'pdf'
230 assert_response :success
224 assert_response :success
231 assert_not_nil assigns(:issues)
225 assert_not_nil assigns(:issues)
232 assert_equal 'application/pdf', @response.content_type
226 assert_equal 'application/pdf', @response.content_type
233
227
234 get :index, :project_id => 1, :format => 'pdf'
228 get :index, :project_id => 1, :format => 'pdf'
235 assert_response :success
229 assert_response :success
236 assert_not_nil assigns(:issues)
230 assert_not_nil assigns(:issues)
237 assert_equal 'application/pdf', @response.content_type
231 assert_equal 'application/pdf', @response.content_type
238
232
239 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
240 assert_response :success
234 assert_response :success
241 assert_not_nil assigns(:issues)
235 assert_not_nil assigns(:issues)
242 assert_equal 'application/pdf', @response.content_type
236 assert_equal 'application/pdf', @response.content_type
243 end
237 end
244
238
245 def test_index_sort
239 def test_index_sort
246 get :index, :sort => 'tracker,id:desc'
240 get :index, :sort => 'tracker,id:desc'
247 assert_response :success
241 assert_response :success
248
242
249 sort_params = @request.session['issues_index_sort']
243 sort_params = @request.session['issues_index_sort']
250 assert sort_params.is_a?(String)
244 assert sort_params.is_a?(String)
251 assert_equal 'tracker,id:desc', sort_params
245 assert_equal 'tracker,id:desc', sort_params
252
246
253 issues = assigns(:issues)
247 issues = assigns(:issues)
254 assert_not_nil issues
248 assert_not_nil issues
255 assert !issues.empty?
249 assert !issues.empty?
256 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
250 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
257 end
251 end
258
252
259 def test_index_with_columns
253 def test_index_with_columns
260 columns = ['tracker', 'subject', 'assigned_to']
254 columns = ['tracker', 'subject', 'assigned_to']
261 get :index, :set_filter => 1, :query => { 'column_names' => columns}
255 get :index, :set_filter => 1, :query => { 'column_names' => columns}
262 assert_response :success
256 assert_response :success
263
257
264 # query should use specified columns
258 # query should use specified columns
265 query = assigns(:query)
259 query = assigns(:query)
266 assert_kind_of Query, query
260 assert_kind_of Query, query
267 assert_equal columns, query.column_names.map(&:to_s)
261 assert_equal columns, query.column_names.map(&:to_s)
268
262
269 # columns should be stored in session
263 # columns should be stored in session
270 assert_kind_of Hash, session[:query]
264 assert_kind_of Hash, session[:query]
271 assert_kind_of Array, session[:query][:column_names]
265 assert_kind_of Array, session[:query][:column_names]
272 assert_equal columns, session[:query][:column_names].map(&:to_s)
266 assert_equal columns, session[:query][:column_names].map(&:to_s)
273 end
267 end
274
268
275 def test_gantt
269 def test_gantt
276 get :gantt, :project_id => 1
270 get :gantt, :project_id => 1
277 assert_response :success
271 assert_response :success
278 assert_template 'gantt.rhtml'
272 assert_template 'gantt.rhtml'
279 assert_not_nil assigns(:gantt)
273 assert_not_nil assigns(:gantt)
280 events = assigns(:gantt).events
274 events = assigns(:gantt).events
281 assert_not_nil events
275 assert_not_nil events
282 # Issue with start and due dates
276 # Issue with start and due dates
283 i = Issue.find(1)
277 i = Issue.find(1)
284 assert_not_nil i.due_date
278 assert_not_nil i.due_date
285 assert events.include?(Issue.find(1))
279 assert events.include?(Issue.find(1))
286 # Issue with without due date but targeted to a version with date
280 # Issue with without due date but targeted to a version with date
287 i = Issue.find(2)
281 i = Issue.find(2)
288 assert_nil i.due_date
282 assert_nil i.due_date
289 assert events.include?(i)
283 assert events.include?(i)
290 end
284 end
291
285
292 def test_cross_project_gantt
286 def test_cross_project_gantt
293 get :gantt
287 get :gantt
294 assert_response :success
288 assert_response :success
295 assert_template 'gantt.rhtml'
289 assert_template 'gantt.rhtml'
296 assert_not_nil assigns(:gantt)
290 assert_not_nil assigns(:gantt)
297 events = assigns(:gantt).events
291 events = assigns(:gantt).events
298 assert_not_nil events
292 assert_not_nil events
299 end
293 end
300
294
301 def test_gantt_export_to_pdf
295 def test_gantt_export_to_pdf
302 get :gantt, :project_id => 1, :format => 'pdf'
296 get :gantt, :project_id => 1, :format => 'pdf'
303 assert_response :success
297 assert_response :success
304 assert_equal 'application/pdf', @response.content_type
298 assert_equal 'application/pdf', @response.content_type
305 assert @response.body.starts_with?('%PDF')
299 assert @response.body.starts_with?('%PDF')
306 assert_not_nil assigns(:gantt)
300 assert_not_nil assigns(:gantt)
307 end
301 end
308
302
309 def test_cross_project_gantt_export_to_pdf
303 def test_cross_project_gantt_export_to_pdf
310 get :gantt, :format => 'pdf'
304 get :gantt, :format => 'pdf'
311 assert_response :success
305 assert_response :success
312 assert_equal 'application/pdf', @response.content_type
306 assert_equal 'application/pdf', @response.content_type
313 assert @response.body.starts_with?('%PDF')
307 assert @response.body.starts_with?('%PDF')
314 assert_not_nil assigns(:gantt)
308 assert_not_nil assigns(:gantt)
315 end
309 end
316
310
317 if Object.const_defined?(:Magick)
311 if Object.const_defined?(:Magick)
318 def test_gantt_image
312 def test_gantt_image
319 get :gantt, :project_id => 1, :format => 'png'
313 get :gantt, :project_id => 1, :format => 'png'
320 assert_response :success
314 assert_response :success
321 assert_equal 'image/png', @response.content_type
315 assert_equal 'image/png', @response.content_type
322 end
316 end
323 else
317 else
324 puts "RMagick not installed. Skipping tests !!!"
318 puts "RMagick not installed. Skipping tests !!!"
325 end
319 end
326
320
327 def test_calendar
321 def test_calendar
328 get :calendar, :project_id => 1
322 get :calendar, :project_id => 1
329 assert_response :success
323 assert_response :success
330 assert_template 'calendar'
324 assert_template 'calendar'
331 assert_not_nil assigns(:calendar)
325 assert_not_nil assigns(:calendar)
332 end
326 end
333
327
334 def test_cross_project_calendar
328 def test_cross_project_calendar
335 get :calendar
329 get :calendar
336 assert_response :success
330 assert_response :success
337 assert_template 'calendar'
331 assert_template 'calendar'
338 assert_not_nil assigns(:calendar)
332 assert_not_nil assigns(:calendar)
339 end
333 end
340
334
341 def test_changes
335 def test_changes
342 get :changes, :project_id => 1
336 get :changes, :project_id => 1
343 assert_response :success
337 assert_response :success
344 assert_not_nil assigns(:journals)
338 assert_not_nil assigns(:journals)
345 assert_equal 'application/atom+xml', @response.content_type
339 assert_equal 'application/atom+xml', @response.content_type
346 end
340 end
347
341
348 def test_show_routing
342 def test_show_routing
349 assert_routing(
343 assert_routing(
350 {:method => :get, :path => '/issues/64'},
344 {:method => :get, :path => '/issues/64'},
351 :controller => 'issues', :action => 'show', :id => '64'
345 :controller => 'issues', :action => 'show', :id => '64'
352 )
346 )
353 end
347 end
354
348
355 def test_show_routing_formatted
349 def test_show_routing_formatted
356 assert_routing(
350 assert_routing(
357 {:method => :get, :path => '/issues/2332.pdf'},
351 {:method => :get, :path => '/issues/2332.pdf'},
358 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
352 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
359 )
353 )
360 assert_routing(
354 assert_routing(
361 {:method => :get, :path => '/issues/23123.atom'},
355 {:method => :get, :path => '/issues/23123.atom'},
362 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
356 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
363 )
357 )
364 end
358 end
365
359
366 def test_show_by_anonymous
360 def test_show_by_anonymous
367 get :show, :id => 1
361 get :show, :id => 1
368 assert_response :success
362 assert_response :success
369 assert_template 'show.rhtml'
363 assert_template 'show.rhtml'
370 assert_not_nil assigns(:issue)
364 assert_not_nil assigns(:issue)
371 assert_equal Issue.find(1), assigns(:issue)
365 assert_equal Issue.find(1), assigns(:issue)
372
366
373 # anonymous role is allowed to add a note
367 # anonymous role is allowed to add a note
374 assert_tag :tag => 'form',
368 assert_tag :tag => 'form',
375 :descendant => { :tag => 'fieldset',
369 :descendant => { :tag => 'fieldset',
376 :child => { :tag => 'legend',
370 :child => { :tag => 'legend',
377 :content => /Notes/ } }
371 :content => /Notes/ } }
378 end
372 end
379
373
380 def test_show_by_manager
374 def test_show_by_manager
381 @request.session[:user_id] = 2
375 @request.session[:user_id] = 2
382 get :show, :id => 1
376 get :show, :id => 1
383 assert_response :success
377 assert_response :success
384
378
385 assert_tag :tag => 'form',
379 assert_tag :tag => 'form',
386 :descendant => { :tag => 'fieldset',
380 :descendant => { :tag => 'fieldset',
387 :child => { :tag => 'legend',
381 :child => { :tag => 'legend',
388 :content => /Change properties/ } },
382 :content => /Change properties/ } },
389 :descendant => { :tag => 'fieldset',
383 :descendant => { :tag => 'fieldset',
390 :child => { :tag => 'legend',
384 :child => { :tag => 'legend',
391 :content => /Log time/ } },
385 :content => /Log time/ } },
392 :descendant => { :tag => 'fieldset',
386 :descendant => { :tag => 'fieldset',
393 :child => { :tag => 'legend',
387 :child => { :tag => 'legend',
394 :content => /Notes/ } }
388 :content => /Notes/ } }
395 end
389 end
396
390
397 def test_show_should_deny_anonymous_access_without_permission
391 def test_show_should_deny_anonymous_access_without_permission
398 Role.anonymous.remove_permission!(:view_issues)
392 Role.anonymous.remove_permission!(:view_issues)
399 get :show, :id => 1
393 get :show, :id => 1
400 assert_response :redirect
394 assert_response :redirect
401 end
395 end
402
396
403 def test_show_should_deny_non_member_access_without_permission
397 def test_show_should_deny_non_member_access_without_permission
404 Role.non_member.remove_permission!(:view_issues)
398 Role.non_member.remove_permission!(:view_issues)
405 @request.session[:user_id] = 9
399 @request.session[:user_id] = 9
406 get :show, :id => 1
400 get :show, :id => 1
407 assert_response 403
401 assert_response 403
408 end
402 end
409
403
410 def test_show_should_deny_member_access_without_permission
404 def test_show_should_deny_member_access_without_permission
411 Role.find(1).remove_permission!(:view_issues)
405 Role.find(1).remove_permission!(:view_issues)
412 @request.session[:user_id] = 2
406 @request.session[:user_id] = 2
413 get :show, :id => 1
407 get :show, :id => 1
414 assert_response 403
408 assert_response 403
415 end
409 end
416
410
417 def test_show_should_not_disclose_relations_to_invisible_issues
411 def test_show_should_not_disclose_relations_to_invisible_issues
418 Setting.cross_project_issue_relations = '1'
412 Setting.cross_project_issue_relations = '1'
419 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
413 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
420 # Relation to a private project issue
414 # Relation to a private project issue
421 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
415 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
422
416
423 get :show, :id => 1
417 get :show, :id => 1
424 assert_response :success
418 assert_response :success
425
419
426 assert_tag :div, :attributes => { :id => 'relations' },
420 assert_tag :div, :attributes => { :id => 'relations' },
427 :descendant => { :tag => 'a', :content => /#2$/ }
421 :descendant => { :tag => 'a', :content => /#2$/ }
428 assert_no_tag :div, :attributes => { :id => 'relations' },
422 assert_no_tag :div, :attributes => { :id => 'relations' },
429 :descendant => { :tag => 'a', :content => /#4$/ }
423 :descendant => { :tag => 'a', :content => /#4$/ }
430 end
424 end
431
425
432 def test_show_atom
426 def test_show_atom
433 get :show, :id => 2, :format => 'atom'
427 get :show, :id => 2, :format => 'atom'
434 assert_response :success
428 assert_response :success
435 assert_template 'changes.rxml'
429 assert_template 'changes.rxml'
436 # Inline image
430 # Inline image
437 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
431 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
438 end
432 end
439
433
440 def test_new_routing
434 def test_new_routing
441 assert_routing(
435 assert_routing(
442 {:method => :get, :path => '/projects/1/issues/new'},
436 {:method => :get, :path => '/projects/1/issues/new'},
443 :controller => 'issues', :action => 'new', :project_id => '1'
437 :controller => 'issues', :action => 'new', :project_id => '1'
444 )
438 )
445 assert_recognizes(
439 assert_recognizes(
446 {:controller => 'issues', :action => 'new', :project_id => '1'},
440 {:controller => 'issues', :action => 'new', :project_id => '1'},
447 {:method => :post, :path => '/projects/1/issues'}
441 {:method => :post, :path => '/projects/1/issues'}
448 )
442 )
449 end
443 end
450
444
451 def test_show_export_to_pdf
445 def test_show_export_to_pdf
452 get :show, :id => 3, :format => 'pdf'
446 get :show, :id => 3, :format => 'pdf'
453 assert_response :success
447 assert_response :success
454 assert_equal 'application/pdf', @response.content_type
448 assert_equal 'application/pdf', @response.content_type
455 assert @response.body.starts_with?('%PDF')
449 assert @response.body.starts_with?('%PDF')
456 assert_not_nil assigns(:issue)
450 assert_not_nil assigns(:issue)
457 end
451 end
458
452
459 def test_get_new
453 def test_get_new
460 @request.session[:user_id] = 2
454 @request.session[:user_id] = 2
461 get :new, :project_id => 1, :tracker_id => 1
455 get :new, :project_id => 1, :tracker_id => 1
462 assert_response :success
456 assert_response :success
463 assert_template 'new'
457 assert_template 'new'
464
458
465 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
459 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
466 :value => 'Default string' }
460 :value => 'Default string' }
467 end
461 end
468
462
469 def test_get_new_without_tracker_id
463 def test_get_new_without_tracker_id
470 @request.session[:user_id] = 2
464 @request.session[:user_id] = 2
471 get :new, :project_id => 1
465 get :new, :project_id => 1
472 assert_response :success
466 assert_response :success
473 assert_template 'new'
467 assert_template 'new'
474
468
475 issue = assigns(:issue)
469 issue = assigns(:issue)
476 assert_not_nil issue
470 assert_not_nil issue
477 assert_equal Project.find(1).trackers.first, issue.tracker
471 assert_equal Project.find(1).trackers.first, issue.tracker
478 end
472 end
479
473
480 def test_get_new_with_no_default_status_should_display_an_error
474 def test_get_new_with_no_default_status_should_display_an_error
481 @request.session[:user_id] = 2
475 @request.session[:user_id] = 2
482 IssueStatus.delete_all
476 IssueStatus.delete_all
483
477
484 get :new, :project_id => 1
478 get :new, :project_id => 1
485 assert_response 500
479 assert_response 500
486 assert_not_nil flash[:error]
480 assert_not_nil flash[:error]
487 assert_tag :tag => 'div', :attributes => { :class => /error/ },
481 assert_tag :tag => 'div', :attributes => { :class => /error/ },
488 :content => /No default issue/
482 :content => /No default issue/
489 end
483 end
490
484
491 def test_get_new_with_no_tracker_should_display_an_error
485 def test_get_new_with_no_tracker_should_display_an_error
492 @request.session[:user_id] = 2
486 @request.session[:user_id] = 2
493 Tracker.delete_all
487 Tracker.delete_all
494
488
495 get :new, :project_id => 1
489 get :new, :project_id => 1
496 assert_response 500
490 assert_response 500
497 assert_not_nil flash[:error]
491 assert_not_nil flash[:error]
498 assert_tag :tag => 'div', :attributes => { :class => /error/ },
492 assert_tag :tag => 'div', :attributes => { :class => /error/ },
499 :content => /No tracker/
493 :content => /No tracker/
500 end
494 end
501
495
502 def test_update_new_form
496 def test_update_new_form
503 @request.session[:user_id] = 2
497 @request.session[:user_id] = 2
504 xhr :post, :update_form, :project_id => 1,
498 xhr :post, :update_form, :project_id => 1,
505 :issue => {:tracker_id => 2,
499 :issue => {:tracker_id => 2,
506 :subject => 'This is the test_new issue',
500 :subject => 'This is the test_new issue',
507 :description => 'This is the description',
501 :description => 'This is the description',
508 :priority_id => 5}
502 :priority_id => 5}
509 assert_response :success
503 assert_response :success
510 assert_template 'attributes'
504 assert_template 'attributes'
511
505
512 issue = assigns(:issue)
506 issue = assigns(:issue)
513 assert_kind_of Issue, issue
507 assert_kind_of Issue, issue
514 assert_equal 1, issue.project_id
508 assert_equal 1, issue.project_id
515 assert_equal 2, issue.tracker_id
509 assert_equal 2, issue.tracker_id
516 assert_equal 'This is the test_new issue', issue.subject
510 assert_equal 'This is the test_new issue', issue.subject
517 end
511 end
518
512
519 def test_post_new
513 def test_post_new
520 @request.session[:user_id] = 2
514 @request.session[:user_id] = 2
521 assert_difference 'Issue.count' do
515 assert_difference 'Issue.count' do
522 post :new, :project_id => 1,
516 post :new, :project_id => 1,
523 :issue => {:tracker_id => 3,
517 :issue => {:tracker_id => 3,
524 :subject => 'This is the test_new issue',
518 :subject => 'This is the test_new issue',
525 :description => 'This is the description',
519 :description => 'This is the description',
526 :priority_id => 5,
520 :priority_id => 5,
527 :estimated_hours => '',
521 :estimated_hours => '',
528 :custom_field_values => {'2' => 'Value for field 2'}}
522 :custom_field_values => {'2' => 'Value for field 2'}}
529 end
523 end
530 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
524 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
531
525
532 issue = Issue.find_by_subject('This is the test_new issue')
526 issue = Issue.find_by_subject('This is the test_new issue')
533 assert_not_nil issue
527 assert_not_nil issue
534 assert_equal 2, issue.author_id
528 assert_equal 2, issue.author_id
535 assert_equal 3, issue.tracker_id
529 assert_equal 3, issue.tracker_id
536 assert_nil issue.estimated_hours
530 assert_nil issue.estimated_hours
537 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
531 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
538 assert_not_nil v
532 assert_not_nil v
539 assert_equal 'Value for field 2', v.value
533 assert_equal 'Value for field 2', v.value
540 end
534 end
541
535
542 def test_post_new_and_continue
536 def test_post_new_and_continue
543 @request.session[:user_id] = 2
537 @request.session[:user_id] = 2
544 post :new, :project_id => 1,
538 post :new, :project_id => 1,
545 :issue => {:tracker_id => 3,
539 :issue => {:tracker_id => 3,
546 :subject => 'This is first issue',
540 :subject => 'This is first issue',
547 :priority_id => 5},
541 :priority_id => 5},
548 :continue => ''
542 :continue => ''
549 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
543 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
550 end
544 end
551
545
552 def test_post_new_without_custom_fields_param
546 def test_post_new_without_custom_fields_param
553 @request.session[:user_id] = 2
547 @request.session[:user_id] = 2
554 assert_difference 'Issue.count' do
548 assert_difference 'Issue.count' do
555 post :new, :project_id => 1,
549 post :new, :project_id => 1,
556 :issue => {:tracker_id => 1,
550 :issue => {:tracker_id => 1,
557 :subject => 'This is the test_new issue',
551 :subject => 'This is the test_new issue',
558 :description => 'This is the description',
552 :description => 'This is the description',
559 :priority_id => 5}
553 :priority_id => 5}
560 end
554 end
561 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
555 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
562 end
556 end
563
557
564 def test_post_new_with_required_custom_field_and_without_custom_fields_param
558 def test_post_new_with_required_custom_field_and_without_custom_fields_param
565 field = IssueCustomField.find_by_name('Database')
559 field = IssueCustomField.find_by_name('Database')
566 field.update_attribute(:is_required, true)
560 field.update_attribute(:is_required, true)
567
561
568 @request.session[:user_id] = 2
562 @request.session[:user_id] = 2
569 post :new, :project_id => 1,
563 post :new, :project_id => 1,
570 :issue => {:tracker_id => 1,
564 :issue => {:tracker_id => 1,
571 :subject => 'This is the test_new issue',
565 :subject => 'This is the test_new issue',
572 :description => 'This is the description',
566 :description => 'This is the description',
573 :priority_id => 5}
567 :priority_id => 5}
574 assert_response :success
568 assert_response :success
575 assert_template 'new'
569 assert_template 'new'
576 issue = assigns(:issue)
570 issue = assigns(:issue)
577 assert_not_nil issue
571 assert_not_nil issue
578 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
572 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
579 end
573 end
580
574
581 def test_post_new_with_watchers
575 def test_post_new_with_watchers
582 @request.session[:user_id] = 2
576 @request.session[:user_id] = 2
583 ActionMailer::Base.deliveries.clear
577 ActionMailer::Base.deliveries.clear
584
578
585 assert_difference 'Watcher.count', 2 do
579 assert_difference 'Watcher.count', 2 do
586 post :new, :project_id => 1,
580 post :new, :project_id => 1,
587 :issue => {:tracker_id => 1,
581 :issue => {:tracker_id => 1,
588 :subject => 'This is a new issue with watchers',
582 :subject => 'This is a new issue with watchers',
589 :description => 'This is the description',
583 :description => 'This is the description',
590 :priority_id => 5,
584 :priority_id => 5,
591 :watcher_user_ids => ['2', '3']}
585 :watcher_user_ids => ['2', '3']}
592 end
586 end
593 issue = Issue.find_by_subject('This is a new issue with watchers')
587 issue = Issue.find_by_subject('This is a new issue with watchers')
594 assert_not_nil issue
588 assert_not_nil issue
595 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
589 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
596
590
597 # Watchers added
591 # Watchers added
598 assert_equal [2, 3], issue.watcher_user_ids.sort
592 assert_equal [2, 3], issue.watcher_user_ids.sort
599 assert issue.watched_by?(User.find(3))
593 assert issue.watched_by?(User.find(3))
600 # Watchers notified
594 # Watchers notified
601 mail = ActionMailer::Base.deliveries.last
595 mail = ActionMailer::Base.deliveries.last
602 assert_kind_of TMail::Mail, mail
596 assert_kind_of TMail::Mail, mail
603 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
597 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
604 end
598 end
605
599
606 def test_post_new_should_send_a_notification
600 def test_post_new_should_send_a_notification
607 ActionMailer::Base.deliveries.clear
601 ActionMailer::Base.deliveries.clear
608 @request.session[:user_id] = 2
602 @request.session[:user_id] = 2
609 assert_difference 'Issue.count' do
603 assert_difference 'Issue.count' do
610 post :new, :project_id => 1,
604 post :new, :project_id => 1,
611 :issue => {:tracker_id => 3,
605 :issue => {:tracker_id => 3,
612 :subject => 'This is the test_new issue',
606 :subject => 'This is the test_new issue',
613 :description => 'This is the description',
607 :description => 'This is the description',
614 :priority_id => 5,
608 :priority_id => 5,
615 :estimated_hours => '',
609 :estimated_hours => '',
616 :custom_field_values => {'2' => 'Value for field 2'}}
610 :custom_field_values => {'2' => 'Value for field 2'}}
617 end
611 end
618 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
612 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
619
613
620 assert_equal 1, ActionMailer::Base.deliveries.size
614 assert_equal 1, ActionMailer::Base.deliveries.size
621 end
615 end
622
616
623 def test_post_should_preserve_fields_values_on_validation_failure
617 def test_post_should_preserve_fields_values_on_validation_failure
624 @request.session[:user_id] = 2
618 @request.session[:user_id] = 2
625 post :new, :project_id => 1,
619 post :new, :project_id => 1,
626 :issue => {:tracker_id => 1,
620 :issue => {:tracker_id => 1,
627 # empty subject
621 # empty subject
628 :subject => '',
622 :subject => '',
629 :description => 'This is a description',
623 :description => 'This is a description',
630 :priority_id => 6,
624 :priority_id => 6,
631 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
625 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
632 assert_response :success
626 assert_response :success
633 assert_template 'new'
627 assert_template 'new'
634
628
635 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
629 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
636 :content => 'This is a description'
630 :content => 'This is a description'
637 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
631 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
638 :child => { :tag => 'option', :attributes => { :selected => 'selected',
632 :child => { :tag => 'option', :attributes => { :selected => 'selected',
639 :value => '6' },
633 :value => '6' },
640 :content => 'High' }
634 :content => 'High' }
641 # Custom fields
635 # Custom fields
642 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
636 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
643 :child => { :tag => 'option', :attributes => { :selected => 'selected',
637 :child => { :tag => 'option', :attributes => { :selected => 'selected',
644 :value => 'Oracle' },
638 :value => 'Oracle' },
645 :content => 'Oracle' }
639 :content => 'Oracle' }
646 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
640 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
647 :value => 'Value for field 2'}
641 :value => 'Value for field 2'}
648 end
642 end
649
643
650 def test_copy_routing
644 def test_copy_routing
651 assert_routing(
645 assert_routing(
652 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
646 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
653 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
647 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
654 )
648 )
655 end
649 end
656
650
657 def test_copy_issue
651 def test_copy_issue
658 @request.session[:user_id] = 2
652 @request.session[:user_id] = 2
659 get :new, :project_id => 1, :copy_from => 1
653 get :new, :project_id => 1, :copy_from => 1
660 assert_template 'new'
654 assert_template 'new'
661 assert_not_nil assigns(:issue)
655 assert_not_nil assigns(:issue)
662 orig = Issue.find(1)
656 orig = Issue.find(1)
663 assert_equal orig.subject, assigns(:issue).subject
657 assert_equal orig.subject, assigns(:issue).subject
664 end
658 end
665
659
666 def test_edit_routing
660 def test_edit_routing
667 assert_routing(
661 assert_routing(
668 {:method => :get, :path => '/issues/1/edit'},
662 {:method => :get, :path => '/issues/1/edit'},
669 :controller => 'issues', :action => 'edit', :id => '1'
663 :controller => 'issues', :action => 'edit', :id => '1'
670 )
664 )
671 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
665 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
672 {:controller => 'issues', :action => 'edit', :id => '1'},
666 {:controller => 'issues', :action => 'edit', :id => '1'},
673 {:method => :post, :path => '/issues/1/edit'}
667 {:method => :post, :path => '/issues/1/edit'}
674 )
668 )
675 end
669 end
676
670
677 def test_get_edit
671 def test_get_edit
678 @request.session[:user_id] = 2
672 @request.session[:user_id] = 2
679 get :edit, :id => 1
673 get :edit, :id => 1
680 assert_response :success
674 assert_response :success
681 assert_template 'edit'
675 assert_template 'edit'
682 assert_not_nil assigns(:issue)
676 assert_not_nil assigns(:issue)
683 assert_equal Issue.find(1), assigns(:issue)
677 assert_equal Issue.find(1), assigns(:issue)
684 end
678 end
685
679
686 def test_get_edit_with_params
680 def test_get_edit_with_params
687 @request.session[:user_id] = 2
681 @request.session[:user_id] = 2
688 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
682 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
689 assert_response :success
683 assert_response :success
690 assert_template 'edit'
684 assert_template 'edit'
691
685
692 issue = assigns(:issue)
686 issue = assigns(:issue)
693 assert_not_nil issue
687 assert_not_nil issue
694
688
695 assert_equal 5, issue.status_id
689 assert_equal 5, issue.status_id
696 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
690 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
697 :child => { :tag => 'option',
691 :child => { :tag => 'option',
698 :content => 'Closed',
692 :content => 'Closed',
699 :attributes => { :selected => 'selected' } }
693 :attributes => { :selected => 'selected' } }
700
694
701 assert_equal 7, issue.priority_id
695 assert_equal 7, issue.priority_id
702 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
696 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
703 :child => { :tag => 'option',
697 :child => { :tag => 'option',
704 :content => 'Urgent',
698 :content => 'Urgent',
705 :attributes => { :selected => 'selected' } }
699 :attributes => { :selected => 'selected' } }
706 end
700 end
707
701
708 def test_update_edit_form
702 def test_update_edit_form
709 @request.session[:user_id] = 2
703 @request.session[:user_id] = 2
710 xhr :post, :update_form, :project_id => 1,
704 xhr :post, :update_form, :project_id => 1,
711 :id => 1,
705 :id => 1,
712 :issue => {:tracker_id => 2,
706 :issue => {:tracker_id => 2,
713 :subject => 'This is the test_new issue',
707 :subject => 'This is the test_new issue',
714 :description => 'This is the description',
708 :description => 'This is the description',
715 :priority_id => 5}
709 :priority_id => 5}
716 assert_response :success
710 assert_response :success
717 assert_template 'attributes'
711 assert_template 'attributes'
718
712
719 issue = assigns(:issue)
713 issue = assigns(:issue)
720 assert_kind_of Issue, issue
714 assert_kind_of Issue, issue
721 assert_equal 1, issue.id
715 assert_equal 1, issue.id
722 assert_equal 1, issue.project_id
716 assert_equal 1, issue.project_id
723 assert_equal 2, issue.tracker_id
717 assert_equal 2, issue.tracker_id
724 assert_equal 'This is the test_new issue', issue.subject
718 assert_equal 'This is the test_new issue', issue.subject
725 end
719 end
726
720
727 def test_reply_routing
721 def test_reply_routing
728 assert_routing(
722 assert_routing(
729 {:method => :post, :path => '/issues/1/quoted'},
723 {:method => :post, :path => '/issues/1/quoted'},
730 :controller => 'issues', :action => 'reply', :id => '1'
724 :controller => 'issues', :action => 'reply', :id => '1'
731 )
725 )
732 end
726 end
733
727
734 def test_reply_to_issue
728 def test_reply_to_issue
735 @request.session[:user_id] = 2
729 @request.session[:user_id] = 2
736 get :reply, :id => 1
730 get :reply, :id => 1
737 assert_response :success
731 assert_response :success
738 assert_select_rjs :show, "update"
732 assert_select_rjs :show, "update"
739 end
733 end
740
734
741 def test_reply_to_note
735 def test_reply_to_note
742 @request.session[:user_id] = 2
736 @request.session[:user_id] = 2
743 get :reply, :id => 1, :journal_id => 2
737 get :reply, :id => 1, :journal_id => 2
744 assert_response :success
738 assert_response :success
745 assert_select_rjs :show, "update"
739 assert_select_rjs :show, "update"
746 end
740 end
747
741
748 def test_post_edit_without_custom_fields_param
742 def test_post_edit_without_custom_fields_param
749 @request.session[:user_id] = 2
743 @request.session[:user_id] = 2
750 ActionMailer::Base.deliveries.clear
744 ActionMailer::Base.deliveries.clear
751
745
752 issue = Issue.find(1)
746 issue = Issue.find(1)
753 assert_equal '125', issue.custom_value_for(2).value
747 assert_equal '125', issue.custom_value_for(2).value
754 old_subject = issue.subject
748 old_subject = issue.subject
755 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
749 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
756
750
757 assert_difference('Journal.count') do
751 assert_difference('Journal.count') do
758 assert_difference('JournalDetail.count', 2) do
752 assert_difference('JournalDetail.count', 2) do
759 post :edit, :id => 1, :issue => {:subject => new_subject,
753 post :edit, :id => 1, :issue => {:subject => new_subject,
760 :priority_id => '6',
754 :priority_id => '6',
761 :category_id => '1' # no change
755 :category_id => '1' # no change
762 }
756 }
763 end
757 end
764 end
758 end
765 assert_redirected_to :action => 'show', :id => '1'
759 assert_redirected_to :action => 'show', :id => '1'
766 issue.reload
760 issue.reload
767 assert_equal new_subject, issue.subject
761 assert_equal new_subject, issue.subject
768 # Make sure custom fields were not cleared
762 # Make sure custom fields were not cleared
769 assert_equal '125', issue.custom_value_for(2).value
763 assert_equal '125', issue.custom_value_for(2).value
770
764
771 mail = ActionMailer::Base.deliveries.last
765 mail = ActionMailer::Base.deliveries.last
772 assert_kind_of TMail::Mail, mail
766 assert_kind_of TMail::Mail, mail
773 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
767 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
774 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
768 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
775 end
769 end
776
770
777 def test_post_edit_with_custom_field_change
771 def test_post_edit_with_custom_field_change
778 @request.session[:user_id] = 2
772 @request.session[:user_id] = 2
779 issue = Issue.find(1)
773 issue = Issue.find(1)
780 assert_equal '125', issue.custom_value_for(2).value
774 assert_equal '125', issue.custom_value_for(2).value
781
775
782 assert_difference('Journal.count') do
776 assert_difference('Journal.count') do
783 assert_difference('JournalDetail.count', 3) do
777 assert_difference('JournalDetail.count', 3) do
784 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
778 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
785 :priority_id => '6',
779 :priority_id => '6',
786 :category_id => '1', # no change
780 :category_id => '1', # no change
787 :custom_field_values => { '2' => 'New custom value' }
781 :custom_field_values => { '2' => 'New custom value' }
788 }
782 }
789 end
783 end
790 end
784 end
791 assert_redirected_to :action => 'show', :id => '1'
785 assert_redirected_to :action => 'show', :id => '1'
792 issue.reload
786 issue.reload
793 assert_equal 'New custom value', issue.custom_value_for(2).value
787 assert_equal 'New custom value', issue.custom_value_for(2).value
794
788
795 mail = ActionMailer::Base.deliveries.last
789 mail = ActionMailer::Base.deliveries.last
796 assert_kind_of TMail::Mail, mail
790 assert_kind_of TMail::Mail, mail
797 assert mail.body.include?("Searchable field changed from 125 to New custom value")
791 assert mail.body.include?("Searchable field changed from 125 to New custom value")
798 end
792 end
799
793
800 def test_post_edit_with_status_and_assignee_change
794 def test_post_edit_with_status_and_assignee_change
801 issue = Issue.find(1)
795 issue = Issue.find(1)
802 assert_equal 1, issue.status_id
796 assert_equal 1, issue.status_id
803 @request.session[:user_id] = 2
797 @request.session[:user_id] = 2
804 assert_difference('TimeEntry.count', 0) do
798 assert_difference('TimeEntry.count', 0) do
805 post :edit,
799 post :edit,
806 :id => 1,
800 :id => 1,
807 :issue => { :status_id => 2, :assigned_to_id => 3 },
801 :issue => { :status_id => 2, :assigned_to_id => 3 },
808 :notes => 'Assigned to dlopper',
802 :notes => 'Assigned to dlopper',
809 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
803 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
810 end
804 end
811 assert_redirected_to :action => 'show', :id => '1'
805 assert_redirected_to :action => 'show', :id => '1'
812 issue.reload
806 issue.reload
813 assert_equal 2, issue.status_id
807 assert_equal 2, issue.status_id
814 j = issue.journals.find(:first, :order => 'id DESC')
808 j = issue.journals.find(:first, :order => 'id DESC')
815 assert_equal 'Assigned to dlopper', j.notes
809 assert_equal 'Assigned to dlopper', j.notes
816 assert_equal 2, j.details.size
810 assert_equal 2, j.details.size
817
811
818 mail = ActionMailer::Base.deliveries.last
812 mail = ActionMailer::Base.deliveries.last
819 assert mail.body.include?("Status changed from New to Assigned")
813 assert mail.body.include?("Status changed from New to Assigned")
820 # subject should contain the new status
814 # subject should contain the new status
821 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
815 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
822 end
816 end
823
817
824 def test_post_edit_with_note_only
818 def test_post_edit_with_note_only
825 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
819 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
826 # anonymous user
820 # anonymous user
827 post :edit,
821 post :edit,
828 :id => 1,
822 :id => 1,
829 :notes => notes
823 :notes => notes
830 assert_redirected_to :action => 'show', :id => '1'
824 assert_redirected_to :action => 'show', :id => '1'
831 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
825 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
832 assert_equal notes, j.notes
826 assert_equal notes, j.notes
833 assert_equal 0, j.details.size
827 assert_equal 0, j.details.size
834 assert_equal User.anonymous, j.user
828 assert_equal User.anonymous, j.user
835
829
836 mail = ActionMailer::Base.deliveries.last
830 mail = ActionMailer::Base.deliveries.last
837 assert mail.body.include?(notes)
831 assert mail.body.include?(notes)
838 end
832 end
839
833
840 def test_post_edit_with_note_and_spent_time
834 def test_post_edit_with_note_and_spent_time
841 @request.session[:user_id] = 2
835 @request.session[:user_id] = 2
842 spent_hours_before = Issue.find(1).spent_hours
836 spent_hours_before = Issue.find(1).spent_hours
843 assert_difference('TimeEntry.count') do
837 assert_difference('TimeEntry.count') do
844 post :edit,
838 post :edit,
845 :id => 1,
839 :id => 1,
846 :notes => '2.5 hours added',
840 :notes => '2.5 hours added',
847 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
841 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
848 end
842 end
849 assert_redirected_to :action => 'show', :id => '1'
843 assert_redirected_to :action => 'show', :id => '1'
850
844
851 issue = Issue.find(1)
845 issue = Issue.find(1)
852
846
853 j = issue.journals.find(:first, :order => 'id DESC')
847 j = issue.journals.find(:first, :order => 'id DESC')
854 assert_equal '2.5 hours added', j.notes
848 assert_equal '2.5 hours added', j.notes
855 assert_equal 0, j.details.size
849 assert_equal 0, j.details.size
856
850
857 t = issue.time_entries.find(:first, :order => 'id DESC')
851 t = issue.time_entries.find(:first, :order => 'id DESC')
858 assert_not_nil t
852 assert_not_nil t
859 assert_equal 2.5, t.hours
853 assert_equal 2.5, t.hours
860 assert_equal spent_hours_before + 2.5, issue.spent_hours
854 assert_equal spent_hours_before + 2.5, issue.spent_hours
861 end
855 end
862
856
863 def test_post_edit_with_attachment_only
857 def test_post_edit_with_attachment_only
864 set_tmp_attachments_directory
858 set_tmp_attachments_directory
865
859
866 # Delete all fixtured journals, a race condition can occur causing the wrong
860 # Delete all fixtured journals, a race condition can occur causing the wrong
867 # journal to get fetched in the next find.
861 # journal to get fetched in the next find.
868 Journal.delete_all
862 Journal.delete_all
869
863
870 # anonymous user
864 # anonymous user
871 post :edit,
865 post :edit,
872 :id => 1,
866 :id => 1,
873 :notes => '',
867 :notes => '',
874 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
868 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
875 assert_redirected_to :action => 'show', :id => '1'
869 assert_redirected_to :action => 'show', :id => '1'
876 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
870 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
877 assert j.notes.blank?
871 assert j.notes.blank?
878 assert_equal 1, j.details.size
872 assert_equal 1, j.details.size
879 assert_equal 'testfile.txt', j.details.first.value
873 assert_equal 'testfile.txt', j.details.first.value
880 assert_equal User.anonymous, j.user
874 assert_equal User.anonymous, j.user
881
875
882 mail = ActionMailer::Base.deliveries.last
876 mail = ActionMailer::Base.deliveries.last
883 assert mail.body.include?('testfile.txt')
877 assert mail.body.include?('testfile.txt')
884 end
878 end
885
879
886 def test_post_edit_with_no_change
880 def test_post_edit_with_no_change
887 issue = Issue.find(1)
881 issue = Issue.find(1)
888 issue.journals.clear
882 issue.journals.clear
889 ActionMailer::Base.deliveries.clear
883 ActionMailer::Base.deliveries.clear
890
884
891 post :edit,
885 post :edit,
892 :id => 1,
886 :id => 1,
893 :notes => ''
887 :notes => ''
894 assert_redirected_to :action => 'show', :id => '1'
888 assert_redirected_to :action => 'show', :id => '1'
895
889
896 issue.reload
890 issue.reload
897 assert issue.journals.empty?
891 assert issue.journals.empty?
898 # No email should be sent
892 # No email should be sent
899 assert ActionMailer::Base.deliveries.empty?
893 assert ActionMailer::Base.deliveries.empty?
900 end
894 end
901
895
902 def test_post_edit_should_send_a_notification
896 def test_post_edit_should_send_a_notification
903 @request.session[:user_id] = 2
897 @request.session[:user_id] = 2
904 ActionMailer::Base.deliveries.clear
898 ActionMailer::Base.deliveries.clear
905 issue = Issue.find(1)
899 issue = Issue.find(1)
906 old_subject = issue.subject
900 old_subject = issue.subject
907 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
901 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
908
902
909 post :edit, :id => 1, :issue => {:subject => new_subject,
903 post :edit, :id => 1, :issue => {:subject => new_subject,
910 :priority_id => '6',
904 :priority_id => '6',
911 :category_id => '1' # no change
905 :category_id => '1' # no change
912 }
906 }
913 assert_equal 1, ActionMailer::Base.deliveries.size
907 assert_equal 1, ActionMailer::Base.deliveries.size
914 end
908 end
915
909
916 def test_post_edit_with_invalid_spent_time
910 def test_post_edit_with_invalid_spent_time
917 @request.session[:user_id] = 2
911 @request.session[:user_id] = 2
918 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
912 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
919
913
920 assert_no_difference('Journal.count') do
914 assert_no_difference('Journal.count') do
921 post :edit,
915 post :edit,
922 :id => 1,
916 :id => 1,
923 :notes => notes,
917 :notes => notes,
924 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
918 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
925 end
919 end
926 assert_response :success
920 assert_response :success
927 assert_template 'edit'
921 assert_template 'edit'
928
922
929 assert_tag :textarea, :attributes => { :name => 'notes' },
923 assert_tag :textarea, :attributes => { :name => 'notes' },
930 :content => notes
924 :content => notes
931 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
925 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
932 end
926 end
933
927
934 def test_get_bulk_edit
928 def test_get_bulk_edit
935 @request.session[:user_id] = 2
929 @request.session[:user_id] = 2
936 get :bulk_edit, :ids => [1, 2]
930 get :bulk_edit, :ids => [1, 2]
937 assert_response :success
931 assert_response :success
938 assert_template 'bulk_edit'
932 assert_template 'bulk_edit'
939 end
933 end
940
934
941 def test_bulk_edit
935 def test_bulk_edit
942 @request.session[:user_id] = 2
936 @request.session[:user_id] = 2
943 # update issues priority
937 # update issues priority
944 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
938 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
945 :assigned_to_id => '',
939 :assigned_to_id => '',
946 :custom_field_values => {'2' => ''},
940 :custom_field_values => {'2' => ''},
947 :notes => 'Bulk editing'
941 :notes => 'Bulk editing'
948 assert_response 302
942 assert_response 302
949 # check that the issues were updated
943 # check that the issues were updated
950 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
944 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
951
945
952 issue = Issue.find(1)
946 issue = Issue.find(1)
953 journal = issue.journals.find(:first, :order => 'created_on DESC')
947 journal = issue.journals.find(:first, :order => 'created_on DESC')
954 assert_equal '125', issue.custom_value_for(2).value
948 assert_equal '125', issue.custom_value_for(2).value
955 assert_equal 'Bulk editing', journal.notes
949 assert_equal 'Bulk editing', journal.notes
956 assert_equal 1, journal.details.size
950 assert_equal 1, journal.details.size
957 end
951 end
958
952
959 def test_bullk_edit_should_send_a_notification
953 def test_bullk_edit_should_send_a_notification
960 @request.session[:user_id] = 2
954 @request.session[:user_id] = 2
961 ActionMailer::Base.deliveries.clear
955 ActionMailer::Base.deliveries.clear
962 post(:bulk_edit,
956 post(:bulk_edit,
963 {
957 {
964 :ids => [1, 2],
958 :ids => [1, 2],
965 :priority_id => 7,
959 :priority_id => 7,
966 :assigned_to_id => '',
960 :assigned_to_id => '',
967 :custom_field_values => {'2' => ''},
961 :custom_field_values => {'2' => ''},
968 :notes => 'Bulk editing'
962 :notes => 'Bulk editing'
969 })
963 })
970
964
971 assert_response 302
965 assert_response 302
972 assert_equal 2, ActionMailer::Base.deliveries.size
966 assert_equal 2, ActionMailer::Base.deliveries.size
973 end
967 end
974
968
975 def test_bulk_edit_status
969 def test_bulk_edit_status
976 @request.session[:user_id] = 2
970 @request.session[:user_id] = 2
977 # update issues priority
971 # update issues priority
978 post :bulk_edit, :ids => [1, 2], :priority_id => '',
972 post :bulk_edit, :ids => [1, 2], :priority_id => '',
979 :assigned_to_id => '',
973 :assigned_to_id => '',
980 :status_id => '5',
974 :status_id => '5',
981 :notes => 'Bulk editing status'
975 :notes => 'Bulk editing status'
982 assert_response 302
976 assert_response 302
983 issue = Issue.find(1)
977 issue = Issue.find(1)
984 assert issue.closed?
978 assert issue.closed?
985 end
979 end
986
980
987 def test_bulk_edit_custom_field
981 def test_bulk_edit_custom_field
988 @request.session[:user_id] = 2
982 @request.session[:user_id] = 2
989 # update issues priority
983 # update issues priority
990 post :bulk_edit, :ids => [1, 2], :priority_id => '',
984 post :bulk_edit, :ids => [1, 2], :priority_id => '',
991 :assigned_to_id => '',
985 :assigned_to_id => '',
992 :custom_field_values => {'2' => '777'},
986 :custom_field_values => {'2' => '777'},
993 :notes => 'Bulk editing custom field'
987 :notes => 'Bulk editing custom field'
994 assert_response 302
988 assert_response 302
995
989
996 issue = Issue.find(1)
990 issue = Issue.find(1)
997 journal = issue.journals.find(:first, :order => 'created_on DESC')
991 journal = issue.journals.find(:first, :order => 'created_on DESC')
998 assert_equal '777', issue.custom_value_for(2).value
992 assert_equal '777', issue.custom_value_for(2).value
999 assert_equal 1, journal.details.size
993 assert_equal 1, journal.details.size
1000 assert_equal '125', journal.details.first.old_value
994 assert_equal '125', journal.details.first.old_value
1001 assert_equal '777', journal.details.first.value
995 assert_equal '777', journal.details.first.value
1002 end
996 end
1003
997
1004 def test_bulk_unassign
998 def test_bulk_unassign
1005 assert_not_nil Issue.find(2).assigned_to
999 assert_not_nil Issue.find(2).assigned_to
1006 @request.session[:user_id] = 2
1000 @request.session[:user_id] = 2
1007 # unassign issues
1001 # unassign issues
1008 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1002 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1009 assert_response 302
1003 assert_response 302
1010 # check that the issues were updated
1004 # check that the issues were updated
1011 assert_nil Issue.find(2).assigned_to
1005 assert_nil Issue.find(2).assigned_to
1012 end
1006 end
1013
1007
1014 def test_move_routing
1008 def test_move_routing
1015 assert_routing(
1009 assert_routing(
1016 {:method => :get, :path => '/issues/1/move'},
1010 {:method => :get, :path => '/issues/1/move'},
1017 :controller => 'issues', :action => 'move', :id => '1'
1011 :controller => 'issues', :action => 'move', :id => '1'
1018 )
1012 )
1019 assert_recognizes(
1013 assert_recognizes(
1020 {:controller => 'issues', :action => 'move', :id => '1'},
1014 {:controller => 'issues', :action => 'move', :id => '1'},
1021 {:method => :post, :path => '/issues/1/move'}
1015 {:method => :post, :path => '/issues/1/move'}
1022 )
1016 )
1023 end
1017 end
1024
1018
1025 def test_move_one_issue_to_another_project
1019 def test_move_one_issue_to_another_project
1026 @request.session[:user_id] = 2
1020 @request.session[:user_id] = 2
1027 post :move, :id => 1, :new_project_id => 2
1021 post :move, :id => 1, :new_project_id => 2
1028 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1022 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1029 assert_equal 2, Issue.find(1).project_id
1023 assert_equal 2, Issue.find(1).project_id
1030 end
1024 end
1031
1025
1032 def test_move_one_issue_to_another_project_should_follow_when_needed
1026 def test_move_one_issue_to_another_project_should_follow_when_needed
1033 @request.session[:user_id] = 2
1027 @request.session[:user_id] = 2
1034 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1028 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1035 assert_redirected_to '/issues/1'
1029 assert_redirected_to '/issues/1'
1036 end
1030 end
1037
1031
1038 def test_bulk_move_to_another_project
1032 def test_bulk_move_to_another_project
1039 @request.session[:user_id] = 2
1033 @request.session[:user_id] = 2
1040 post :move, :ids => [1, 2], :new_project_id => 2
1034 post :move, :ids => [1, 2], :new_project_id => 2
1041 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1035 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1042 # Issues moved to project 2
1036 # Issues moved to project 2
1043 assert_equal 2, Issue.find(1).project_id
1037 assert_equal 2, Issue.find(1).project_id
1044 assert_equal 2, Issue.find(2).project_id
1038 assert_equal 2, Issue.find(2).project_id
1045 # No tracker change
1039 # No tracker change
1046 assert_equal 1, Issue.find(1).tracker_id
1040 assert_equal 1, Issue.find(1).tracker_id
1047 assert_equal 2, Issue.find(2).tracker_id
1041 assert_equal 2, Issue.find(2).tracker_id
1048 end
1042 end
1049
1043
1050 def test_bulk_move_to_another_tracker
1044 def test_bulk_move_to_another_tracker
1051 @request.session[:user_id] = 2
1045 @request.session[:user_id] = 2
1052 post :move, :ids => [1, 2], :new_tracker_id => 2
1046 post :move, :ids => [1, 2], :new_tracker_id => 2
1053 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1047 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1054 assert_equal 2, Issue.find(1).tracker_id
1048 assert_equal 2, Issue.find(1).tracker_id
1055 assert_equal 2, Issue.find(2).tracker_id
1049 assert_equal 2, Issue.find(2).tracker_id
1056 end
1050 end
1057
1051
1058 def test_bulk_copy_to_another_project
1052 def test_bulk_copy_to_another_project
1059 @request.session[:user_id] = 2
1053 @request.session[:user_id] = 2
1060 assert_difference 'Issue.count', 2 do
1054 assert_difference 'Issue.count', 2 do
1061 assert_no_difference 'Project.find(1).issues.count' do
1055 assert_no_difference 'Project.find(1).issues.count' do
1062 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1056 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1063 end
1057 end
1064 end
1058 end
1065 assert_redirected_to 'projects/ecookbook/issues'
1059 assert_redirected_to 'projects/ecookbook/issues'
1066 end
1060 end
1067
1061
1068 def test_copy_to_another_project_should_follow_when_needed
1062 def test_copy_to_another_project_should_follow_when_needed
1069 @request.session[:user_id] = 2
1063 @request.session[:user_id] = 2
1070 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1064 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1071 issue = Issue.first(:order => 'id DESC')
1065 issue = Issue.first(:order => 'id DESC')
1072 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1066 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1073 end
1067 end
1074
1068
1075 def test_context_menu_one_issue
1069 def test_context_menu_one_issue
1076 @request.session[:user_id] = 2
1070 @request.session[:user_id] = 2
1077 get :context_menu, :ids => [1]
1071 get :context_menu, :ids => [1]
1078 assert_response :success
1072 assert_response :success
1079 assert_template 'context_menu'
1073 assert_template 'context_menu'
1080 assert_tag :tag => 'a', :content => 'Edit',
1074 assert_tag :tag => 'a', :content => 'Edit',
1081 :attributes => { :href => '/issues/1/edit',
1075 :attributes => { :href => '/issues/1/edit',
1082 :class => 'icon-edit' }
1076 :class => 'icon-edit' }
1083 assert_tag :tag => 'a', :content => 'Closed',
1077 assert_tag :tag => 'a', :content => 'Closed',
1084 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1078 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1085 :class => '' }
1079 :class => '' }
1086 assert_tag :tag => 'a', :content => 'Immediate',
1080 assert_tag :tag => 'a', :content => 'Immediate',
1087 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1081 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1088 :class => '' }
1082 :class => '' }
1089 assert_tag :tag => 'a', :content => 'Dave Lopper',
1083 assert_tag :tag => 'a', :content => 'Dave Lopper',
1090 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1084 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1091 :class => '' }
1085 :class => '' }
1092 assert_tag :tag => 'a', :content => 'Copy',
1086 assert_tag :tag => 'a', :content => 'Copy',
1093 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1087 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1094 :class => 'icon-copy' }
1088 :class => 'icon-copy' }
1095 assert_tag :tag => 'a', :content => 'Move',
1089 assert_tag :tag => 'a', :content => 'Move',
1096 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1090 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1097 :class => 'icon-move' }
1091 :class => 'icon-move' }
1098 assert_tag :tag => 'a', :content => 'Delete',
1092 assert_tag :tag => 'a', :content => 'Delete',
1099 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1093 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1100 :class => 'icon-del' }
1094 :class => 'icon-del' }
1101 end
1095 end
1102
1096
1103 def test_context_menu_one_issue_by_anonymous
1097 def test_context_menu_one_issue_by_anonymous
1104 get :context_menu, :ids => [1]
1098 get :context_menu, :ids => [1]
1105 assert_response :success
1099 assert_response :success
1106 assert_template 'context_menu'
1100 assert_template 'context_menu'
1107 assert_tag :tag => 'a', :content => 'Delete',
1101 assert_tag :tag => 'a', :content => 'Delete',
1108 :attributes => { :href => '#',
1102 :attributes => { :href => '#',
1109 :class => 'icon-del disabled' }
1103 :class => 'icon-del disabled' }
1110 end
1104 end
1111
1105
1112 def test_context_menu_multiple_issues_of_same_project
1106 def test_context_menu_multiple_issues_of_same_project
1113 @request.session[:user_id] = 2
1107 @request.session[:user_id] = 2
1114 get :context_menu, :ids => [1, 2]
1108 get :context_menu, :ids => [1, 2]
1115 assert_response :success
1109 assert_response :success
1116 assert_template 'context_menu'
1110 assert_template 'context_menu'
1117 assert_tag :tag => 'a', :content => 'Edit',
1111 assert_tag :tag => 'a', :content => 'Edit',
1118 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1112 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1119 :class => 'icon-edit' }
1113 :class => 'icon-edit' }
1120 assert_tag :tag => 'a', :content => 'Immediate',
1114 assert_tag :tag => 'a', :content => 'Immediate',
1121 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1115 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1122 :class => '' }
1116 :class => '' }
1123 assert_tag :tag => 'a', :content => 'Dave Lopper',
1117 assert_tag :tag => 'a', :content => 'Dave Lopper',
1124 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1118 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1125 :class => '' }
1119 :class => '' }
1126 assert_tag :tag => 'a', :content => 'Move',
1120 assert_tag :tag => 'a', :content => 'Move',
1127 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1121 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1128 :class => 'icon-move' }
1122 :class => 'icon-move' }
1129 assert_tag :tag => 'a', :content => 'Delete',
1123 assert_tag :tag => 'a', :content => 'Delete',
1130 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1124 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1131 :class => 'icon-del' }
1125 :class => 'icon-del' }
1132 end
1126 end
1133
1127
1134 def test_context_menu_multiple_issues_of_different_project
1128 def test_context_menu_multiple_issues_of_different_project
1135 @request.session[:user_id] = 2
1129 @request.session[:user_id] = 2
1136 get :context_menu, :ids => [1, 2, 4]
1130 get :context_menu, :ids => [1, 2, 4]
1137 assert_response :success
1131 assert_response :success
1138 assert_template 'context_menu'
1132 assert_template 'context_menu'
1139 assert_tag :tag => 'a', :content => 'Delete',
1133 assert_tag :tag => 'a', :content => 'Delete',
1140 :attributes => { :href => '#',
1134 :attributes => { :href => '#',
1141 :class => 'icon-del disabled' }
1135 :class => 'icon-del disabled' }
1142 end
1136 end
1143
1137
1144 def test_destroy_routing
1138 def test_destroy_routing
1145 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1139 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1146 {:controller => 'issues', :action => 'destroy', :id => '1'},
1140 {:controller => 'issues', :action => 'destroy', :id => '1'},
1147 {:method => :post, :path => '/issues/1/destroy'}
1141 {:method => :post, :path => '/issues/1/destroy'}
1148 )
1142 )
1149 end
1143 end
1150
1144
1151 def test_destroy_issue_with_no_time_entries
1145 def test_destroy_issue_with_no_time_entries
1152 assert_nil TimeEntry.find_by_issue_id(2)
1146 assert_nil TimeEntry.find_by_issue_id(2)
1153 @request.session[:user_id] = 2
1147 @request.session[:user_id] = 2
1154 post :destroy, :id => 2
1148 post :destroy, :id => 2
1155 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1149 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1156 assert_nil Issue.find_by_id(2)
1150 assert_nil Issue.find_by_id(2)
1157 end
1151 end
1158
1152
1159 def test_destroy_issues_with_time_entries
1153 def test_destroy_issues_with_time_entries
1160 @request.session[:user_id] = 2
1154 @request.session[:user_id] = 2
1161 post :destroy, :ids => [1, 3]
1155 post :destroy, :ids => [1, 3]
1162 assert_response :success
1156 assert_response :success
1163 assert_template 'destroy'
1157 assert_template 'destroy'
1164 assert_not_nil assigns(:hours)
1158 assert_not_nil assigns(:hours)
1165 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1159 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1166 end
1160 end
1167
1161
1168 def test_destroy_issues_and_destroy_time_entries
1162 def test_destroy_issues_and_destroy_time_entries
1169 @request.session[:user_id] = 2
1163 @request.session[:user_id] = 2
1170 post :destroy, :ids => [1, 3], :todo => 'destroy'
1164 post :destroy, :ids => [1, 3], :todo => 'destroy'
1171 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1165 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1172 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1166 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1173 assert_nil TimeEntry.find_by_id([1, 2])
1167 assert_nil TimeEntry.find_by_id([1, 2])
1174 end
1168 end
1175
1169
1176 def test_destroy_issues_and_assign_time_entries_to_project
1170 def test_destroy_issues_and_assign_time_entries_to_project
1177 @request.session[:user_id] = 2
1171 @request.session[:user_id] = 2
1178 post :destroy, :ids => [1, 3], :todo => 'nullify'
1172 post :destroy, :ids => [1, 3], :todo => 'nullify'
1179 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1173 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1180 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1174 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1181 assert_nil TimeEntry.find(1).issue_id
1175 assert_nil TimeEntry.find(1).issue_id
1182 assert_nil TimeEntry.find(2).issue_id
1176 assert_nil TimeEntry.find(2).issue_id
1183 end
1177 end
1184
1178
1185 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1179 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1186 @request.session[:user_id] = 2
1180 @request.session[:user_id] = 2
1187 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1181 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1188 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1182 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1189 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1183 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1190 assert_equal 2, TimeEntry.find(1).issue_id
1184 assert_equal 2, TimeEntry.find(1).issue_id
1191 assert_equal 2, TimeEntry.find(2).issue_id
1185 assert_equal 2, TimeEntry.find(2).issue_id
1192 end
1186 end
1193
1187
1194 def test_default_search_scope
1188 def test_default_search_scope
1195 get :index
1189 get :index
1196 assert_tag :div, :attributes => {:id => 'quick-search'},
1190 assert_tag :div, :attributes => {:id => 'quick-search'},
1197 :child => {:tag => 'form',
1191 :child => {:tag => 'form',
1198 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1192 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1199 end
1193 end
1200 end
1194 end
@@ -1,313 +1,339
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
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 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def find_issues_with_query(query)
29 def find_issues_with_query(query)
30 Issue.find :all,
30 Issue.find :all,
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
32 :conditions => query.statement
32 :conditions => query.statement
33 end
33 end
34
34
35 def test_query_with_multiple_custom_fields
35 def test_query_with_multiple_custom_fields
36 query = Query.find(1)
36 query = Query.find(1)
37 assert query.valid?
37 assert query.valid?
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
39 issues = find_issues_with_query(query)
39 issues = find_issues_with_query(query)
40 assert_equal 1, issues.length
40 assert_equal 1, issues.length
41 assert_equal Issue.find(3), issues.first
41 assert_equal Issue.find(3), issues.first
42 end
42 end
43
43
44 def test_operator_none
44 def test_operator_none
45 query = Query.new(:project => Project.find(1), :name => '_')
45 query = Query.new(:project => Project.find(1), :name => '_')
46 query.add_filter('fixed_version_id', '!*', [''])
46 query.add_filter('fixed_version_id', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
50 find_issues_with_query(query)
50 find_issues_with_query(query)
51 end
51 end
52
52
53 def test_operator_none_for_integer
53 def test_operator_none_for_integer
54 query = Query.new(:project => Project.find(1), :name => '_')
54 query = Query.new(:project => Project.find(1), :name => '_')
55 query.add_filter('estimated_hours', '!*', [''])
55 query.add_filter('estimated_hours', '!*', [''])
56 issues = find_issues_with_query(query)
56 issues = find_issues_with_query(query)
57 assert !issues.empty?
57 assert !issues.empty?
58 assert issues.all? {|i| !i.estimated_hours}
58 assert issues.all? {|i| !i.estimated_hours}
59 end
59 end
60
60
61 def test_operator_all
61 def test_operator_all
62 query = Query.new(:project => Project.find(1), :name => '_')
62 query = Query.new(:project => Project.find(1), :name => '_')
63 query.add_filter('fixed_version_id', '*', [''])
63 query.add_filter('fixed_version_id', '*', [''])
64 query.add_filter('cf_1', '*', [''])
64 query.add_filter('cf_1', '*', [''])
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
67 find_issues_with_query(query)
67 find_issues_with_query(query)
68 end
68 end
69
69
70 def test_operator_greater_than
70 def test_operator_greater_than
71 query = Query.new(:project => Project.find(1), :name => '_')
71 query = Query.new(:project => Project.find(1), :name => '_')
72 query.add_filter('done_ratio', '>=', ['40'])
72 query.add_filter('done_ratio', '>=', ['40'])
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
74 find_issues_with_query(query)
74 find_issues_with_query(query)
75 end
75 end
76
76
77 def test_operator_in_more_than
77 def test_operator_in_more_than
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('due_date', '>t+', ['15'])
80 query.add_filter('due_date', '>t+', ['15'])
81 issues = find_issues_with_query(query)
81 issues = find_issues_with_query(query)
82 assert !issues.empty?
82 assert !issues.empty?
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
84 end
84 end
85
85
86 def test_operator_in_less_than
86 def test_operator_in_less_than
87 query = Query.new(:project => Project.find(1), :name => '_')
87 query = Query.new(:project => Project.find(1), :name => '_')
88 query.add_filter('due_date', '<t+', ['15'])
88 query.add_filter('due_date', '<t+', ['15'])
89 issues = find_issues_with_query(query)
89 issues = find_issues_with_query(query)
90 assert !issues.empty?
90 assert !issues.empty?
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
92 end
92 end
93
93
94 def test_operator_less_than_ago
94 def test_operator_less_than_ago
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('due_date', '>t-', ['3'])
97 query.add_filter('due_date', '>t-', ['3'])
98 issues = find_issues_with_query(query)
98 issues = find_issues_with_query(query)
99 assert !issues.empty?
99 assert !issues.empty?
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
101 end
101 end
102
102
103 def test_operator_more_than_ago
103 def test_operator_more_than_ago
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('due_date', '<t-', ['10'])
106 query.add_filter('due_date', '<t-', ['10'])
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
108 issues = find_issues_with_query(query)
108 issues = find_issues_with_query(query)
109 assert !issues.empty?
109 assert !issues.empty?
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
111 end
111 end
112
112
113 def test_operator_in
113 def test_operator_in
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
115 query = Query.new(:project => Project.find(1), :name => '_')
115 query = Query.new(:project => Project.find(1), :name => '_')
116 query.add_filter('due_date', 't+', ['2'])
116 query.add_filter('due_date', 't+', ['2'])
117 issues = find_issues_with_query(query)
117 issues = find_issues_with_query(query)
118 assert !issues.empty?
118 assert !issues.empty?
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
120 end
120 end
121
121
122 def test_operator_ago
122 def test_operator_ago
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
124 query = Query.new(:project => Project.find(1), :name => '_')
124 query = Query.new(:project => Project.find(1), :name => '_')
125 query.add_filter('due_date', 't-', ['3'])
125 query.add_filter('due_date', 't-', ['3'])
126 issues = find_issues_with_query(query)
126 issues = find_issues_with_query(query)
127 assert !issues.empty?
127 assert !issues.empty?
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
129 end
129 end
130
130
131 def test_operator_today
131 def test_operator_today
132 query = Query.new(:project => Project.find(1), :name => '_')
132 query = Query.new(:project => Project.find(1), :name => '_')
133 query.add_filter('due_date', 't', [''])
133 query.add_filter('due_date', 't', [''])
134 issues = find_issues_with_query(query)
134 issues = find_issues_with_query(query)
135 assert !issues.empty?
135 assert !issues.empty?
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
137 end
137 end
138
138
139 def test_operator_this_week_on_date
139 def test_operator_this_week_on_date
140 query = Query.new(:project => Project.find(1), :name => '_')
140 query = Query.new(:project => Project.find(1), :name => '_')
141 query.add_filter('due_date', 'w', [''])
141 query.add_filter('due_date', 'w', [''])
142 find_issues_with_query(query)
142 find_issues_with_query(query)
143 end
143 end
144
144
145 def test_operator_this_week_on_datetime
145 def test_operator_this_week_on_datetime
146 query = Query.new(:project => Project.find(1), :name => '_')
146 query = Query.new(:project => Project.find(1), :name => '_')
147 query.add_filter('created_on', 'w', [''])
147 query.add_filter('created_on', 'w', [''])
148 find_issues_with_query(query)
148 find_issues_with_query(query)
149 end
149 end
150
150
151 def test_operator_contains
151 def test_operator_contains
152 query = Query.new(:project => Project.find(1), :name => '_')
152 query = Query.new(:project => Project.find(1), :name => '_')
153 query.add_filter('subject', '~', ['uNable'])
153 query.add_filter('subject', '~', ['uNable'])
154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
155 result = find_issues_with_query(query)
155 result = find_issues_with_query(query)
156 assert result.empty?
156 assert result.empty?
157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
158 end
158 end
159
159
160 def test_operator_does_not_contains
160 def test_operator_does_not_contains
161 query = Query.new(:project => Project.find(1), :name => '_')
161 query = Query.new(:project => Project.find(1), :name => '_')
162 query.add_filter('subject', '!~', ['uNable'])
162 query.add_filter('subject', '!~', ['uNable'])
163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
164 find_issues_with_query(query)
164 find_issues_with_query(query)
165 end
165 end
166
166
167 def test_filter_watched_issues
167 def test_filter_watched_issues
168 User.current = User.find(1)
168 User.current = User.find(1)
169 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
169 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
170 result = find_issues_with_query(query)
170 result = find_issues_with_query(query)
171 assert_not_nil result
171 assert_not_nil result
172 assert !result.empty?
172 assert !result.empty?
173 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
173 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
174 User.current = nil
174 User.current = nil
175 end
175 end
176
176
177 def test_filter_unwatched_issues
177 def test_filter_unwatched_issues
178 User.current = User.find(1)
178 User.current = User.find(1)
179 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
179 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
180 result = find_issues_with_query(query)
180 result = find_issues_with_query(query)
181 assert_not_nil result
181 assert_not_nil result
182 assert !result.empty?
182 assert !result.empty?
183 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
183 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
184 User.current = nil
184 User.current = nil
185 end
185 end
186
186
187 def test_default_columns
187 def test_default_columns
188 q = Query.new
188 q = Query.new
189 assert !q.columns.empty?
189 assert !q.columns.empty?
190 end
190 end
191
191
192 def test_set_column_names
192 def test_set_column_names
193 q = Query.new
193 q = Query.new
194 q.column_names = ['tracker', :subject, '', 'unknonw_column']
194 q.column_names = ['tracker', :subject, '', 'unknonw_column']
195 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
195 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
196 c = q.columns.first
196 c = q.columns.first
197 assert q.has_column?(c)
197 assert q.has_column?(c)
198 end
198 end
199
199
200 def test_groupable_columns_should_include_custom_fields
200 def test_groupable_columns_should_include_custom_fields
201 q = Query.new
201 q = Query.new
202 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
202 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
203 end
203 end
204
204
205 def test_default_sort
205 def test_default_sort
206 q = Query.new
206 q = Query.new
207 assert_equal [], q.sort_criteria
207 assert_equal [], q.sort_criteria
208 end
208 end
209
209
210 def test_set_sort_criteria_with_hash
210 def test_set_sort_criteria_with_hash
211 q = Query.new
211 q = Query.new
212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
214 end
214 end
215
215
216 def test_set_sort_criteria_with_array
216 def test_set_sort_criteria_with_array
217 q = Query.new
217 q = Query.new
218 q.sort_criteria = [['priority', 'desc'], 'tracker']
218 q.sort_criteria = [['priority', 'desc'], 'tracker']
219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
220 end
220 end
221
221
222 def test_create_query_with_sort
222 def test_create_query_with_sort
223 q = Query.new(:name => 'Sorted')
223 q = Query.new(:name => 'Sorted')
224 q.sort_criteria = [['priority', 'desc'], 'tracker']
224 q.sort_criteria = [['priority', 'desc'], 'tracker']
225 assert q.save
225 assert q.save
226 q.reload
226 q.reload
227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
228 end
228 end
229
229
230 def test_sort_by_string_custom_field_asc
230 def test_sort_by_string_custom_field_asc
231 q = Query.new
231 q = Query.new
232 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
232 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
233 assert c
233 assert c
234 assert c.sortable
234 assert c.sortable
235 issues = Issue.find :all,
235 issues = Issue.find :all,
236 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
236 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
237 :conditions => q.statement,
237 :conditions => q.statement,
238 :order => "#{c.sortable} ASC"
238 :order => "#{c.sortable} ASC"
239 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
239 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
240 assert !values.empty?
240 assert !values.empty?
241 assert_equal values.sort, values
241 assert_equal values.sort, values
242 end
242 end
243
243
244 def test_sort_by_string_custom_field_desc
244 def test_sort_by_string_custom_field_desc
245 q = Query.new
245 q = Query.new
246 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
246 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
247 assert c
247 assert c
248 assert c.sortable
248 assert c.sortable
249 issues = Issue.find :all,
249 issues = Issue.find :all,
250 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
250 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
251 :conditions => q.statement,
251 :conditions => q.statement,
252 :order => "#{c.sortable} DESC"
252 :order => "#{c.sortable} DESC"
253 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
253 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
254 assert !values.empty?
254 assert !values.empty?
255 assert_equal values.sort.reverse, values
255 assert_equal values.sort.reverse, values
256 end
256 end
257
257
258 def test_sort_by_float_custom_field_asc
258 def test_sort_by_float_custom_field_asc
259 q = Query.new
259 q = Query.new
260 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
260 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
261 assert c
261 assert c
262 assert c.sortable
262 assert c.sortable
263 issues = Issue.find :all,
263 issues = Issue.find :all,
264 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
264 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
265 :conditions => q.statement,
265 :conditions => q.statement,
266 :order => "#{c.sortable} ASC"
266 :order => "#{c.sortable} ASC"
267 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
267 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
268 assert !values.empty?
268 assert !values.empty?
269 assert_equal values.sort, values
269 assert_equal values.sort, values
270 end
270 end
271
271
272 def test_invalid_query_should_raise_query_statement_invalid_error
272 def test_invalid_query_should_raise_query_statement_invalid_error
273 q = Query.new
273 q = Query.new
274 assert_raise Query::StatementInvalid do
274 assert_raise Query::StatementInvalid do
275 q.issues(:conditions => "foo = 1")
275 q.issues(:conditions => "foo = 1")
276 end
276 end
277 end
277 end
278
278
279 def test_issue_count_by_association_group
280 q = Query.new(:name => '_', :group_by => 'assigned_to')
281 count_by_group = q.issue_count_by_group
282 assert_kind_of Hash, count_by_group
283 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
284 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
285 assert count_by_group.has_key?(User.find(3))
286 end
287
288 def test_issue_count_by_list_custom_field_group
289 q = Query.new(:name => '_', :group_by => 'cf_1')
290 count_by_group = q.issue_count_by_group
291 assert_kind_of Hash, count_by_group
292 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
293 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
294 assert count_by_group.has_key?('MySQL')
295 end
296
297 def test_issue_count_by_date_custom_field_group
298 q = Query.new(:name => '_', :group_by => 'cf_8')
299 count_by_group = q.issue_count_by_group
300 assert_kind_of Hash, count_by_group
301 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
302 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
303 end
304
279 def test_label_for
305 def test_label_for
280 q = Query.new
306 q = Query.new
281 assert_equal 'assigned_to', q.label_for('assigned_to_id')
307 assert_equal 'assigned_to', q.label_for('assigned_to_id')
282 end
308 end
283
309
284 def test_editable_by
310 def test_editable_by
285 admin = User.find(1)
311 admin = User.find(1)
286 manager = User.find(2)
312 manager = User.find(2)
287 developer = User.find(3)
313 developer = User.find(3)
288
314
289 # Public query on project 1
315 # Public query on project 1
290 q = Query.find(1)
316 q = Query.find(1)
291 assert q.editable_by?(admin)
317 assert q.editable_by?(admin)
292 assert q.editable_by?(manager)
318 assert q.editable_by?(manager)
293 assert !q.editable_by?(developer)
319 assert !q.editable_by?(developer)
294
320
295 # Private query on project 1
321 # Private query on project 1
296 q = Query.find(2)
322 q = Query.find(2)
297 assert q.editable_by?(admin)
323 assert q.editable_by?(admin)
298 assert !q.editable_by?(manager)
324 assert !q.editable_by?(manager)
299 assert q.editable_by?(developer)
325 assert q.editable_by?(developer)
300
326
301 # Private query for all projects
327 # Private query for all projects
302 q = Query.find(3)
328 q = Query.find(3)
303 assert q.editable_by?(admin)
329 assert q.editable_by?(admin)
304 assert !q.editable_by?(manager)
330 assert !q.editable_by?(manager)
305 assert q.editable_by?(developer)
331 assert q.editable_by?(developer)
306
332
307 # Public query for all projects
333 # Public query for all projects
308 q = Query.find(4)
334 q = Query.find(4)
309 assert q.editable_by?(admin)
335 assert q.editable_by?(admin)
310 assert !q.editable_by?(manager)
336 assert !q.editable_by?(manager)
311 assert !q.editable_by?(developer)
337 assert !q.editable_by?(developer)
312 end
338 end
313 end
339 end
General Comments 0
You need to be logged in to leave comments. Login now