##// END OF EJS Templates
Ability to sort the issue list by text, list, date and boolean custom fields (#1139)....
Jean-Philippe Lang -
r2255:3a28848ca03a
parent child
Show More
@@ -1,75 +1,96
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 # remove empty values
44 # remove empty values
45 self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
45 self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
46 # make sure these fields are not searchable
46 # make sure these fields are not searchable
47 self.searchable = false if %w(int float date bool).include?(field_format)
47 self.searchable = false if %w(int float date bool).include?(field_format)
48 true
48 true
49 end
49 end
50
50
51 def validate
51 def validate
52 if self.field_format == "list"
52 if self.field_format == "list"
53 errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
53 errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
54 errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
54 errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
55 end
55 end
56
56
57 # validate default value
57 # validate default value
58 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
58 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
59 v.custom_field.is_required = false
59 v.custom_field.is_required = false
60 errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
60 errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
61 end
61 end
62
63 # Returns a ORDER BY clause that can used to sort customized
64 # objects by their value of the custom field.
65 # Returns false, if the custom field can not be used for sorting.
66 def order_statement
67 case field_format
68 when 'string', 'list', 'date', 'bool'
69 # COALESCE is here to make sure that blank and NULL values are sorted equally
70 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
71 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
72 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
73 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
74 else
75 nil
76 end
77 end
62
78
63 def <=>(field)
79 def <=>(field)
64 position <=> field.position
80 position <=> field.position
65 end
81 end
66
82
83 def self.customized_class
84 self.name =~ /^(.+)CustomField$/
85 begin; $1.constantize; rescue nil; end
86 end
87
67 # to move in project_custom_field
88 # to move in project_custom_field
68 def self.for_all
89 def self.for_all
69 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
90 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
70 end
91 end
71
92
72 def type_name
93 def type_name
73 nil
94 nil
74 end
95 end
75 end
96 end
@@ -1,55 +1,59
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 CustomValue < ActiveRecord::Base
18 class CustomValue < ActiveRecord::Base
19 belongs_to :custom_field
19 belongs_to :custom_field
20 belongs_to :customized, :polymorphic => true
20 belongs_to :customized, :polymorphic => true
21
21
22 def after_initialize
22 def after_initialize
23 if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
23 if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
24 self.value ||= custom_field.default_value
24 self.value ||= custom_field.default_value
25 end
25 end
26 end
26 end
27
27
28 # Returns true if the boolean custom value is true
28 # Returns true if the boolean custom value is true
29 def true?
29 def true?
30 self.value == '1'
30 self.value == '1'
31 end
31 end
32
32
33 def to_s
34 value.to_s
35 end
36
33 protected
37 protected
34 def validate
38 def validate
35 if value.blank?
39 if value.blank?
36 errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank?
40 errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank?
37 else
41 else
38 errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
42 errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
39 errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
43 errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
40 errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
44 errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
41
45
42 # Format specific validations
46 # Format specific validations
43 case custom_field.field_format
47 case custom_field.field_format
44 when 'int'
48 when 'int'
45 errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/
49 errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/
46 when 'float'
50 when 'float'
47 begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
51 begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
48 when 'date'
52 when 'date'
49 errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
53 errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
50 when 'list'
54 when 'list'
51 errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value)
55 errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value)
52 end
56 end
53 end
57 end
54 end
58 end
55 end
59 end
@@ -1,406 +1,406
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, :default_order
19 attr_accessor :name, :sortable, :default_order
20 include GLoc
20 include GLoc
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.default_order = options[:default_order]
25 self.default_order = options[:default_order]
26 end
26 end
27
27
28 def caption
28 def caption
29 set_language_if_valid(User.current.language)
29 set_language_if_valid(User.current.language)
30 l("field_#{name}")
30 l("field_#{name}")
31 end
31 end
32 end
32 end
33
33
34 class QueryCustomFieldColumn < QueryColumn
34 class QueryCustomFieldColumn < QueryColumn
35
35
36 def initialize(custom_field)
36 def initialize(custom_field)
37 self.name = "cf_#{custom_field.id}".to_sym
37 self.name = "cf_#{custom_field.id}".to_sym
38 self.sortable = false
38 self.sortable = custom_field.order_statement || false
39 @cf = custom_field
39 @cf = custom_field
40 end
40 end
41
41
42 def caption
42 def caption
43 @cf.name
43 @cf.name
44 end
44 end
45
45
46 def custom_field
46 def custom_field
47 @cf
47 @cf
48 end
48 end
49 end
49 end
50
50
51 class Query < ActiveRecord::Base
51 class Query < ActiveRecord::Base
52 belongs_to :project
52 belongs_to :project
53 belongs_to :user
53 belongs_to :user
54 serialize :filters
54 serialize :filters
55 serialize :column_names
55 serialize :column_names
56
56
57 attr_protected :project_id, :user_id
57 attr_protected :project_id, :user_id
58
58
59 validates_presence_of :name, :on => :save
59 validates_presence_of :name, :on => :save
60 validates_length_of :name, :maximum => 255
60 validates_length_of :name, :maximum => 255
61
61
62 @@operators = { "=" => :label_equals,
62 @@operators = { "=" => :label_equals,
63 "!" => :label_not_equals,
63 "!" => :label_not_equals,
64 "o" => :label_open_issues,
64 "o" => :label_open_issues,
65 "c" => :label_closed_issues,
65 "c" => :label_closed_issues,
66 "!*" => :label_none,
66 "!*" => :label_none,
67 "*" => :label_all,
67 "*" => :label_all,
68 ">=" => '>=',
68 ">=" => '>=',
69 "<=" => '<=',
69 "<=" => '<=',
70 "<t+" => :label_in_less_than,
70 "<t+" => :label_in_less_than,
71 ">t+" => :label_in_more_than,
71 ">t+" => :label_in_more_than,
72 "t+" => :label_in,
72 "t+" => :label_in,
73 "t" => :label_today,
73 "t" => :label_today,
74 "w" => :label_this_week,
74 "w" => :label_this_week,
75 ">t-" => :label_less_than_ago,
75 ">t-" => :label_less_than_ago,
76 "<t-" => :label_more_than_ago,
76 "<t-" => :label_more_than_ago,
77 "t-" => :label_ago,
77 "t-" => :label_ago,
78 "~" => :label_contains,
78 "~" => :label_contains,
79 "!~" => :label_not_contains }
79 "!~" => :label_not_contains }
80
80
81 cattr_reader :operators
81 cattr_reader :operators
82
82
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
86 :list_subprojects => [ "*", "!*", "=" ],
86 :list_subprojects => [ "*", "!*", "=" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 :string => [ "=", "~", "!", "!~" ],
89 :string => [ "=", "~", "!", "!~" ],
90 :text => [ "~", "!~" ],
90 :text => [ "~", "!~" ],
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
92
92
93 cattr_reader :operators_by_filter_type
93 cattr_reader :operators_by_filter_type
94
94
95 @@available_columns = [
95 @@available_columns = [
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
96 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
97 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
98 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
99 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 QueryColumn.new(:author),
100 QueryColumn.new(:author),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
101 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
102 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
103 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
104 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
105 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
106 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
107 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
108 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
109 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 ]
110 ]
111 cattr_reader :available_columns
111 cattr_reader :available_columns
112
112
113 def initialize(attributes = nil)
113 def initialize(attributes = nil)
114 super attributes
114 super attributes
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
115 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 set_language_if_valid(User.current.language)
116 set_language_if_valid(User.current.language)
117 end
117 end
118
118
119 def after_initialize
119 def after_initialize
120 # Store the fact that project is nil (used in #editable_by?)
120 # Store the fact that project is nil (used in #editable_by?)
121 @is_for_all = project.nil?
121 @is_for_all = project.nil?
122 end
122 end
123
123
124 def validate
124 def validate
125 filters.each_key do |field|
125 filters.each_key do |field|
126 errors.add label_for(field), :activerecord_error_blank unless
126 errors.add label_for(field), :activerecord_error_blank unless
127 # filter requires one or more values
127 # filter requires one or more values
128 (values_for(field) and !values_for(field).first.blank?) or
128 (values_for(field) and !values_for(field).first.blank?) or
129 # filter doesn't require any value
129 # filter doesn't require any value
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
130 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
131 end if filters
131 end if filters
132 end
132 end
133
133
134 def editable_by?(user)
134 def editable_by?(user)
135 return false unless user
135 return false unless user
136 # Admin can edit them all and regular users can edit their private queries
136 # Admin can edit them all and regular users can edit their private queries
137 return true if user.admin? || (!is_public && self.user_id == user.id)
137 return true if user.admin? || (!is_public && self.user_id == user.id)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
138 # Members can not edit public queries that are for all project (only admin is allowed to)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
139 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
140 end
140 end
141
141
142 def available_filters
142 def available_filters
143 return @available_filters if @available_filters
143 return @available_filters if @available_filters
144
144
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
145 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
146
146
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
147 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
148 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
149 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
150 "subject" => { :type => :text, :order => 8 },
150 "subject" => { :type => :text, :order => 8 },
151 "created_on" => { :type => :date_past, :order => 9 },
151 "created_on" => { :type => :date_past, :order => 9 },
152 "updated_on" => { :type => :date_past, :order => 10 },
152 "updated_on" => { :type => :date_past, :order => 10 },
153 "start_date" => { :type => :date, :order => 11 },
153 "start_date" => { :type => :date, :order => 11 },
154 "due_date" => { :type => :date, :order => 12 },
154 "due_date" => { :type => :date, :order => 12 },
155 "estimated_hours" => { :type => :integer, :order => 13 },
155 "estimated_hours" => { :type => :integer, :order => 13 },
156 "done_ratio" => { :type => :integer, :order => 14 }}
156 "done_ratio" => { :type => :integer, :order => 14 }}
157
157
158 user_values = []
158 user_values = []
159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
159 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
160 if project
160 if project
161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
161 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
162 else
162 else
163 # members of the user's projects
163 # members of the user's projects
164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
164 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
165 end
165 end
166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
166 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
167 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
168
168
169 if project
169 if project
170 # project specific filters
170 # project specific filters
171 unless @project.issue_categories.empty?
171 unless @project.issue_categories.empty?
172 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
172 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
173 end
173 end
174 unless @project.versions.empty?
174 unless @project.versions.empty?
175 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
175 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
176 end
176 end
177 unless @project.active_children.empty?
177 unless @project.active_children.empty?
178 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
178 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
179 end
179 end
180 add_custom_fields_filters(@project.all_issue_custom_fields)
180 add_custom_fields_filters(@project.all_issue_custom_fields)
181 else
181 else
182 # global filters for cross project issue list
182 # global filters for cross project issue list
183 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
183 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
184 end
184 end
185 @available_filters
185 @available_filters
186 end
186 end
187
187
188 def add_filter(field, operator, values)
188 def add_filter(field, operator, values)
189 # values must be an array
189 # values must be an array
190 return unless values and values.is_a? Array # and !values.first.empty?
190 return unless values and values.is_a? Array # and !values.first.empty?
191 # check if field is defined as an available filter
191 # check if field is defined as an available filter
192 if available_filters.has_key? field
192 if available_filters.has_key? field
193 filter_options = available_filters[field]
193 filter_options = available_filters[field]
194 # check if operator is allowed for that filter
194 # check if operator is allowed for that filter
195 #if @@operators_by_filter_type[filter_options[:type]].include? operator
195 #if @@operators_by_filter_type[filter_options[:type]].include? operator
196 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
196 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
197 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
197 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
198 #end
198 #end
199 filters[field] = {:operator => operator, :values => values }
199 filters[field] = {:operator => operator, :values => values }
200 end
200 end
201 end
201 end
202
202
203 def add_short_filter(field, expression)
203 def add_short_filter(field, expression)
204 return unless expression
204 return unless expression
205 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
205 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
206 add_filter field, (parms[0] || "="), [parms[1] || ""]
206 add_filter field, (parms[0] || "="), [parms[1] || ""]
207 end
207 end
208
208
209 def has_filter?(field)
209 def has_filter?(field)
210 filters and filters[field]
210 filters and filters[field]
211 end
211 end
212
212
213 def operator_for(field)
213 def operator_for(field)
214 has_filter?(field) ? filters[field][:operator] : nil
214 has_filter?(field) ? filters[field][:operator] : nil
215 end
215 end
216
216
217 def values_for(field)
217 def values_for(field)
218 has_filter?(field) ? filters[field][:values] : nil
218 has_filter?(field) ? filters[field][:values] : nil
219 end
219 end
220
220
221 def label_for(field)
221 def label_for(field)
222 label = available_filters[field][:name] if available_filters.has_key?(field)
222 label = available_filters[field][:name] if available_filters.has_key?(field)
223 label ||= field.gsub(/\_id$/, "")
223 label ||= field.gsub(/\_id$/, "")
224 end
224 end
225
225
226 def available_columns
226 def available_columns
227 return @available_columns if @available_columns
227 return @available_columns if @available_columns
228 @available_columns = Query.available_columns
228 @available_columns = Query.available_columns
229 @available_columns += (project ?
229 @available_columns += (project ?
230 project.all_issue_custom_fields :
230 project.all_issue_custom_fields :
231 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
231 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
232 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
232 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
233 end
233 end
234
234
235 def columns
235 def columns
236 if has_default_columns?
236 if has_default_columns?
237 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
237 available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
238 else
238 else
239 # preserve the column_names order
239 # preserve the column_names order
240 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
240 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
241 end
241 end
242 end
242 end
243
243
244 def column_names=(names)
244 def column_names=(names)
245 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
245 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
246 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
246 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
247 write_attribute(:column_names, names)
247 write_attribute(:column_names, names)
248 end
248 end
249
249
250 def has_column?(column)
250 def has_column?(column)
251 column_names && column_names.include?(column.name)
251 column_names && column_names.include?(column.name)
252 end
252 end
253
253
254 def has_default_columns?
254 def has_default_columns?
255 column_names.nil? || column_names.empty?
255 column_names.nil? || column_names.empty?
256 end
256 end
257
257
258 def project_statement
258 def project_statement
259 project_clauses = []
259 project_clauses = []
260 if project && !@project.active_children.empty?
260 if project && !@project.active_children.empty?
261 ids = [project.id]
261 ids = [project.id]
262 if has_filter?("subproject_id")
262 if has_filter?("subproject_id")
263 case operator_for("subproject_id")
263 case operator_for("subproject_id")
264 when '='
264 when '='
265 # include the selected subprojects
265 # include the selected subprojects
266 ids += values_for("subproject_id").each(&:to_i)
266 ids += values_for("subproject_id").each(&:to_i)
267 when '!*'
267 when '!*'
268 # main project only
268 # main project only
269 else
269 else
270 # all subprojects
270 # all subprojects
271 ids += project.child_ids
271 ids += project.child_ids
272 end
272 end
273 elsif Setting.display_subprojects_issues?
273 elsif Setting.display_subprojects_issues?
274 ids += project.child_ids
274 ids += project.child_ids
275 end
275 end
276 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
276 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
277 elsif project
277 elsif project
278 project_clauses << "#{Project.table_name}.id = %d" % project.id
278 project_clauses << "#{Project.table_name}.id = %d" % project.id
279 end
279 end
280 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
280 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
281 project_clauses.join(' AND ')
281 project_clauses.join(' AND ')
282 end
282 end
283
283
284 def statement
284 def statement
285 # filters clauses
285 # filters clauses
286 filters_clauses = []
286 filters_clauses = []
287 filters.each_key do |field|
287 filters.each_key do |field|
288 next if field == "subproject_id"
288 next if field == "subproject_id"
289 v = values_for(field).clone
289 v = values_for(field).clone
290 next unless v and !v.empty?
290 next unless v and !v.empty?
291
291
292 sql = ''
292 sql = ''
293 is_custom_filter = false
293 is_custom_filter = false
294 if field =~ /^cf_(\d+)$/
294 if field =~ /^cf_(\d+)$/
295 # custom field
295 # custom field
296 db_table = CustomValue.table_name
296 db_table = CustomValue.table_name
297 db_field = 'value'
297 db_field = 'value'
298 is_custom_filter = true
298 is_custom_filter = true
299 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 "
299 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 "
300 else
300 else
301 # regular field
301 # regular field
302 db_table = Issue.table_name
302 db_table = Issue.table_name
303 db_field = field
303 db_field = field
304 sql << '('
304 sql << '('
305 end
305 end
306
306
307 # "me" value subsitution
307 # "me" value subsitution
308 if %w(assigned_to_id author_id).include?(field)
308 if %w(assigned_to_id author_id).include?(field)
309 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
309 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
310 end
310 end
311
311
312 sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
312 sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
313
313
314 sql << ')'
314 sql << ')'
315 filters_clauses << sql
315 filters_clauses << sql
316 end if filters and valid?
316 end if filters and valid?
317
317
318 (filters_clauses << project_statement).join(' AND ')
318 (filters_clauses << project_statement).join(' AND ')
319 end
319 end
320
320
321 private
321 private
322
322
323 # Helper method to generate the WHERE sql for a +field+ with a +value+
323 # Helper method to generate the WHERE sql for a +field+ with a +value+
324 def sql_for_field(field, value, db_table, db_field, is_custom_filter)
324 def sql_for_field(field, value, db_table, db_field, is_custom_filter)
325 sql = ''
325 sql = ''
326 case operator_for field
326 case operator_for field
327 when "="
327 when "="
328 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
328 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
329 when "!"
329 when "!"
330 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
330 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
331 when "!*"
331 when "!*"
332 sql = "#{db_table}.#{db_field} IS NULL"
332 sql = "#{db_table}.#{db_field} IS NULL"
333 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
333 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
334 when "*"
334 when "*"
335 sql = "#{db_table}.#{db_field} IS NOT NULL"
335 sql = "#{db_table}.#{db_field} IS NOT NULL"
336 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
336 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
337 when ">="
337 when ">="
338 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
338 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
339 when "<="
339 when "<="
340 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
340 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
341 when "o"
341 when "o"
342 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
342 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
343 when "c"
343 when "c"
344 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
344 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
345 when ">t-"
345 when ">t-"
346 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
346 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
347 when "<t-"
347 when "<t-"
348 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
348 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
349 when "t-"
349 when "t-"
350 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
350 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
351 when ">t+"
351 when ">t+"
352 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
352 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
353 when "<t+"
353 when "<t+"
354 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
354 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
355 when "t+"
355 when "t+"
356 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
356 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
357 when "t"
357 when "t"
358 sql = date_range_clause(db_table, db_field, 0, 0)
358 sql = date_range_clause(db_table, db_field, 0, 0)
359 when "w"
359 when "w"
360 from = l(:general_first_day_of_week) == '7' ?
360 from = l(:general_first_day_of_week) == '7' ?
361 # week starts on sunday
361 # week starts on sunday
362 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
362 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
363 # week starts on monday (Rails default)
363 # week starts on monday (Rails default)
364 Time.now.at_beginning_of_week
364 Time.now.at_beginning_of_week
365 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
365 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
366 when "~"
366 when "~"
367 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
367 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
368 when "!~"
368 when "!~"
369 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
369 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
370 end
370 end
371
371
372 return sql
372 return sql
373 end
373 end
374
374
375 def add_custom_fields_filters(custom_fields)
375 def add_custom_fields_filters(custom_fields)
376 @available_filters ||= {}
376 @available_filters ||= {}
377
377
378 custom_fields.select(&:is_filter?).each do |field|
378 custom_fields.select(&:is_filter?).each do |field|
379 case field.field_format
379 case field.field_format
380 when "text"
380 when "text"
381 options = { :type => :text, :order => 20 }
381 options = { :type => :text, :order => 20 }
382 when "list"
382 when "list"
383 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
383 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
384 when "date"
384 when "date"
385 options = { :type => :date, :order => 20 }
385 options = { :type => :date, :order => 20 }
386 when "bool"
386 when "bool"
387 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
387 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
388 else
388 else
389 options = { :type => :string, :order => 20 }
389 options = { :type => :string, :order => 20 }
390 end
390 end
391 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
391 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
392 end
392 end
393 end
393 end
394
394
395 # Returns a SQL clause for a date or datetime field.
395 # Returns a SQL clause for a date or datetime field.
396 def date_range_clause(table, field, from, to)
396 def date_range_clause(table, field, from, to)
397 s = []
397 s = []
398 if from
398 if from
399 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
399 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
400 end
400 end
401 if to
401 if to
402 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
402 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
403 end
403 end
404 s.join(' AND ')
404 s.join(' AND ')
405 end
405 end
406 end
406 end
@@ -1,212 +1,238
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 < Test::Unit::TestCase
20 class QueryTest < Test::Unit::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :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', '~', ['string'])
153 query.add_filter('subject', '~', ['string'])
154 assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'")
154 assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'")
155 find_issues_with_query(query)
155 find_issues_with_query(query)
156 end
156 end
157
157
158 def test_operator_does_not_contains
158 def test_operator_does_not_contains
159 query = Query.new(:project => Project.find(1), :name => '_')
159 query = Query.new(:project => Project.find(1), :name => '_')
160 query.add_filter('subject', '!~', ['string'])
160 query.add_filter('subject', '!~', ['string'])
161 assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'")
161 assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'")
162 find_issues_with_query(query)
162 find_issues_with_query(query)
163 end
163 end
164
164
165 def test_default_columns
165 def test_default_columns
166 q = Query.new
166 q = Query.new
167 assert !q.columns.empty?
167 assert !q.columns.empty?
168 end
168 end
169
169
170 def test_set_column_names
170 def test_set_column_names
171 q = Query.new
171 q = Query.new
172 q.column_names = ['tracker', :subject, '', 'unknonw_column']
172 q.column_names = ['tracker', :subject, '', 'unknonw_column']
173 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
173 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
174 c = q.columns.first
174 c = q.columns.first
175 assert q.has_column?(c)
175 assert q.has_column?(c)
176 end
176 end
177
177
178 def test_sort_by_string_custom_field_asc
179 q = Query.new
180 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
181 assert c
182 assert c.sortable
183 issues = Issue.find :all,
184 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
185 :conditions => q.statement,
186 :order => "#{c.sortable} ASC"
187 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
188 assert_equal values.sort, values
189 end
190
191 def test_sort_by_string_custom_field_desc
192 q = Query.new
193 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
194 assert c
195 assert c.sortable
196 issues = Issue.find :all,
197 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
198 :conditions => q.statement,
199 :order => "#{c.sortable} DESC"
200 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
201 assert_equal values.sort.reverse, values
202 end
203
178 def test_label_for
204 def test_label_for
179 q = Query.new
205 q = Query.new
180 assert_equal 'assigned_to', q.label_for('assigned_to_id')
206 assert_equal 'assigned_to', q.label_for('assigned_to_id')
181 end
207 end
182
208
183 def test_editable_by
209 def test_editable_by
184 admin = User.find(1)
210 admin = User.find(1)
185 manager = User.find(2)
211 manager = User.find(2)
186 developer = User.find(3)
212 developer = User.find(3)
187
213
188 # Public query on project 1
214 # Public query on project 1
189 q = Query.find(1)
215 q = Query.find(1)
190 assert q.editable_by?(admin)
216 assert q.editable_by?(admin)
191 assert q.editable_by?(manager)
217 assert q.editable_by?(manager)
192 assert !q.editable_by?(developer)
218 assert !q.editable_by?(developer)
193
219
194 # Private query on project 1
220 # Private query on project 1
195 q = Query.find(2)
221 q = Query.find(2)
196 assert q.editable_by?(admin)
222 assert q.editable_by?(admin)
197 assert !q.editable_by?(manager)
223 assert !q.editable_by?(manager)
198 assert q.editable_by?(developer)
224 assert q.editable_by?(developer)
199
225
200 # Private query for all projects
226 # Private query for all projects
201 q = Query.find(3)
227 q = Query.find(3)
202 assert q.editable_by?(admin)
228 assert q.editable_by?(admin)
203 assert !q.editable_by?(manager)
229 assert !q.editable_by?(manager)
204 assert q.editable_by?(developer)
230 assert q.editable_by?(developer)
205
231
206 # Public query for all projects
232 # Public query for all projects
207 q = Query.find(4)
233 q = Query.find(4)
208 assert q.editable_by?(admin)
234 assert q.editable_by?(admin)
209 assert !q.editable_by?(manager)
235 assert !q.editable_by?(manager)
210 assert !q.editable_by?(developer)
236 assert !q.editable_by?(developer)
211 end
237 end
212 end
238 end
General Comments 0
You need to be logged in to leave comments. Login now