##// END OF EJS Templates
Adding missing setter for Query#available_columns...
Eric Davis -
r3571:657aa624a4e3
parent child
Show More
@@ -1,582 +1,590
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 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !sortable.nil?
39 !sortable.nil?
40 end
40 end
41
41
42 def value(issue)
42 def value(issue)
43 issue.send name
43 issue.send name
44 end
44 end
45 end
45 end
46
46
47 class QueryCustomFieldColumn < QueryColumn
47 class QueryCustomFieldColumn < QueryColumn
48
48
49 def initialize(custom_field)
49 def initialize(custom_field)
50 self.name = "cf_#{custom_field.id}".to_sym
50 self.name = "cf_#{custom_field.id}".to_sym
51 self.sortable = custom_field.order_statement || false
51 self.sortable = custom_field.order_statement || false
52 if %w(list date bool int).include?(custom_field.field_format)
52 if %w(list date bool int).include?(custom_field.field_format)
53 self.groupable = custom_field.order_statement
53 self.groupable = custom_field.order_statement
54 end
54 end
55 self.groupable ||= false
55 self.groupable ||= false
56 @cf = custom_field
56 @cf = custom_field
57 end
57 end
58
58
59 def caption
59 def caption
60 @cf.name
60 @cf.name
61 end
61 end
62
62
63 def custom_field
63 def custom_field
64 @cf
64 @cf
65 end
65 end
66
66
67 def value(issue)
67 def value(issue)
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 cv && @cf.cast_value(cv.value)
69 cv && @cf.cast_value(cv.value)
70 end
70 end
71 end
71 end
72
72
73 class Query < ActiveRecord::Base
73 class Query < ActiveRecord::Base
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 end
75 end
76
76
77 belongs_to :project
77 belongs_to :project
78 belongs_to :user
78 belongs_to :user
79 serialize :filters
79 serialize :filters
80 serialize :column_names
80 serialize :column_names
81 serialize :sort_criteria, Array
81 serialize :sort_criteria, Array
82
82
83 attr_protected :project_id, :user_id
83 attr_protected :project_id, :user_id
84
84
85 validates_presence_of :name, :on => :save
85 validates_presence_of :name, :on => :save
86 validates_length_of :name, :maximum => 255
86 validates_length_of :name, :maximum => 255
87
87
88 @@operators = { "=" => :label_equals,
88 @@operators = { "=" => :label_equals,
89 "!" => :label_not_equals,
89 "!" => :label_not_equals,
90 "o" => :label_open_issues,
90 "o" => :label_open_issues,
91 "c" => :label_closed_issues,
91 "c" => :label_closed_issues,
92 "!*" => :label_none,
92 "!*" => :label_none,
93 "*" => :label_all,
93 "*" => :label_all,
94 ">=" => :label_greater_or_equal,
94 ">=" => :label_greater_or_equal,
95 "<=" => :label_less_or_equal,
95 "<=" => :label_less_or_equal,
96 "<t+" => :label_in_less_than,
96 "<t+" => :label_in_less_than,
97 ">t+" => :label_in_more_than,
97 ">t+" => :label_in_more_than,
98 "t+" => :label_in,
98 "t+" => :label_in,
99 "t" => :label_today,
99 "t" => :label_today,
100 "w" => :label_this_week,
100 "w" => :label_this_week,
101 ">t-" => :label_less_than_ago,
101 ">t-" => :label_less_than_ago,
102 "<t-" => :label_more_than_ago,
102 "<t-" => :label_more_than_ago,
103 "t-" => :label_ago,
103 "t-" => :label_ago,
104 "~" => :label_contains,
104 "~" => :label_contains,
105 "!~" => :label_not_contains }
105 "!~" => :label_not_contains }
106
106
107 cattr_reader :operators
107 cattr_reader :operators
108
108
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
112 :list_subprojects => [ "*", "!*", "=" ],
112 :list_subprojects => [ "*", "!*", "=" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115 :string => [ "=", "~", "!", "!~" ],
115 :string => [ "=", "~", "!", "!~" ],
116 :text => [ "~", "!~" ],
116 :text => [ "~", "!~" ],
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118
118
119 cattr_reader :operators_by_filter_type
119 cattr_reader :operators_by_filter_type
120
120
121 @@available_columns = [
121 @@available_columns = [
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128 QueryColumn.new(:author),
128 QueryColumn.new(:author),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 ]
138 ]
139 cattr_reader :available_columns
139 cattr_reader :available_columns
140
140
141 def initialize(attributes = nil)
141 def initialize(attributes = nil)
142 super attributes
142 super attributes
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 end
144 end
145
145
146 def after_initialize
146 def after_initialize
147 # Store the fact that project is nil (used in #editable_by?)
147 # Store the fact that project is nil (used in #editable_by?)
148 @is_for_all = project.nil?
148 @is_for_all = project.nil?
149 end
149 end
150
150
151 def validate
151 def validate
152 filters.each_key do |field|
152 filters.each_key do |field|
153 errors.add label_for(field), :blank unless
153 errors.add label_for(field), :blank unless
154 # filter requires one or more values
154 # filter requires one or more values
155 (values_for(field) and !values_for(field).first.blank?) or
155 (values_for(field) and !values_for(field).first.blank?) or
156 # filter doesn't require any value
156 # filter doesn't require any value
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 end if filters
158 end if filters
159 end
159 end
160
160
161 def editable_by?(user)
161 def editable_by?(user)
162 return false unless user
162 return false unless user
163 # Admin can edit them all and regular users can edit their private queries
163 # Admin can edit them all and regular users can edit their private queries
164 return true if user.admin? || (!is_public && self.user_id == user.id)
164 return true if user.admin? || (!is_public && self.user_id == user.id)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 end
167 end
168
168
169 def available_filters
169 def available_filters
170 return @available_filters if @available_filters
170 return @available_filters if @available_filters
171
171
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173
173
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 "subject" => { :type => :text, :order => 8 },
177 "subject" => { :type => :text, :order => 8 },
178 "created_on" => { :type => :date_past, :order => 9 },
178 "created_on" => { :type => :date_past, :order => 9 },
179 "updated_on" => { :type => :date_past, :order => 10 },
179 "updated_on" => { :type => :date_past, :order => 10 },
180 "start_date" => { :type => :date, :order => 11 },
180 "start_date" => { :type => :date, :order => 11 },
181 "due_date" => { :type => :date, :order => 12 },
181 "due_date" => { :type => :date, :order => 12 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
183 "done_ratio" => { :type => :integer, :order => 14 }}
183 "done_ratio" => { :type => :integer, :order => 14 }}
184
184
185 user_values = []
185 user_values = []
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 if project
187 if project
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 else
189 else
190 project_ids = User.current.projects.collect(&:id)
190 project_ids = User.current.projects.collect(&:id)
191 if project_ids.any?
191 if project_ids.any?
192 # members of the user's projects
192 # members of the user's projects
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
194 end
194 end
195 end
195 end
196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
198
198
199 if User.current.logged?
199 if User.current.logged?
200 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
200 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
201 end
201 end
202
202
203 if project
203 if project
204 # project specific filters
204 # project specific filters
205 unless @project.issue_categories.empty?
205 unless @project.issue_categories.empty?
206 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
206 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
207 end
207 end
208 unless @project.shared_versions.empty?
208 unless @project.shared_versions.empty?
209 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
209 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
210 end
210 end
211 unless @project.descendants.active.empty?
211 unless @project.descendants.active.empty?
212 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
212 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
213 end
213 end
214 add_custom_fields_filters(@project.all_issue_custom_fields)
214 add_custom_fields_filters(@project.all_issue_custom_fields)
215 else
215 else
216 # global filters for cross project issue list
216 # global filters for cross project issue list
217 system_shared_versions = Version.visible.find_all_by_sharing('system')
217 system_shared_versions = Version.visible.find_all_by_sharing('system')
218 unless system_shared_versions.empty?
218 unless system_shared_versions.empty?
219 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
219 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
220 end
220 end
221 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
221 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
222 end
222 end
223 @available_filters
223 @available_filters
224 end
224 end
225
225
226 def add_filter(field, operator, values)
226 def add_filter(field, operator, values)
227 # values must be an array
227 # values must be an array
228 return unless values and values.is_a? Array # and !values.first.empty?
228 return unless values and values.is_a? Array # and !values.first.empty?
229 # check if field is defined as an available filter
229 # check if field is defined as an available filter
230 if available_filters.has_key? field
230 if available_filters.has_key? field
231 filter_options = available_filters[field]
231 filter_options = available_filters[field]
232 # check if operator is allowed for that filter
232 # check if operator is allowed for that filter
233 #if @@operators_by_filter_type[filter_options[:type]].include? operator
233 #if @@operators_by_filter_type[filter_options[:type]].include? operator
234 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
234 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
235 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
235 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
236 #end
236 #end
237 filters[field] = {:operator => operator, :values => values }
237 filters[field] = {:operator => operator, :values => values }
238 end
238 end
239 end
239 end
240
240
241 def add_short_filter(field, expression)
241 def add_short_filter(field, expression)
242 return unless expression
242 return unless expression
243 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
243 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
244 add_filter field, (parms[0] || "="), [parms[1] || ""]
244 add_filter field, (parms[0] || "="), [parms[1] || ""]
245 end
245 end
246
246
247 # Add multiple filters using +add_filter+
247 # Add multiple filters using +add_filter+
248 def add_filters(fields, operators, values)
248 def add_filters(fields, operators, values)
249 fields.each do |field|
249 fields.each do |field|
250 add_filter(field, operators[field], values[field])
250 add_filter(field, operators[field], values[field])
251 end
251 end
252 end
252 end
253
253
254 def has_filter?(field)
254 def has_filter?(field)
255 filters and filters[field]
255 filters and filters[field]
256 end
256 end
257
257
258 def operator_for(field)
258 def operator_for(field)
259 has_filter?(field) ? filters[field][:operator] : nil
259 has_filter?(field) ? filters[field][:operator] : nil
260 end
260 end
261
261
262 def values_for(field)
262 def values_for(field)
263 has_filter?(field) ? filters[field][:values] : nil
263 has_filter?(field) ? filters[field][:values] : nil
264 end
264 end
265
265
266 def label_for(field)
266 def label_for(field)
267 label = available_filters[field][:name] if available_filters.has_key?(field)
267 label = available_filters[field][:name] if available_filters.has_key?(field)
268 label ||= field.gsub(/\_id$/, "")
268 label ||= field.gsub(/\_id$/, "")
269 end
269 end
270
270
271 def available_columns
271 def available_columns
272 return @available_columns if @available_columns
272 return @available_columns if @available_columns
273 @available_columns = Query.available_columns
273 @available_columns = Query.available_columns
274 @available_columns += (project ?
274 @available_columns += (project ?
275 project.all_issue_custom_fields :
275 project.all_issue_custom_fields :
276 IssueCustomField.find(:all)
276 IssueCustomField.find(:all)
277 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
277 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
278 end
278 end
279
280 def self.available_columns=(v)
281 self.available_columns = (v)
282 end
283
284 def self.add_available_column(column)
285 self.available_columns << (column) if column.is_a?(QueryColumn)
286 end
279
287
280 # Returns an array of columns that can be used to group the results
288 # Returns an array of columns that can be used to group the results
281 def groupable_columns
289 def groupable_columns
282 available_columns.select {|c| c.groupable}
290 available_columns.select {|c| c.groupable}
283 end
291 end
284
292
285 # Returns a Hash of columns and the key for sorting
293 # Returns a Hash of columns and the key for sorting
286 def sortable_columns
294 def sortable_columns
287 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
295 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
288 h[column.name.to_s] = column.sortable
296 h[column.name.to_s] = column.sortable
289 h
297 h
290 })
298 })
291 end
299 end
292
300
293 def columns
301 def columns
294 if has_default_columns?
302 if has_default_columns?
295 available_columns.select do |c|
303 available_columns.select do |c|
296 # Adds the project column by default for cross-project lists
304 # Adds the project column by default for cross-project lists
297 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
305 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
298 end
306 end
299 else
307 else
300 # preserve the column_names order
308 # preserve the column_names order
301 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
309 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
302 end
310 end
303 end
311 end
304
312
305 def column_names=(names)
313 def column_names=(names)
306 if names
314 if names
307 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
315 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
308 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
316 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
309 # Set column_names to nil if default columns
317 # Set column_names to nil if default columns
310 if names.map(&:to_s) == Setting.issue_list_default_columns
318 if names.map(&:to_s) == Setting.issue_list_default_columns
311 names = nil
319 names = nil
312 end
320 end
313 end
321 end
314 write_attribute(:column_names, names)
322 write_attribute(:column_names, names)
315 end
323 end
316
324
317 def has_column?(column)
325 def has_column?(column)
318 column_names && column_names.include?(column.name)
326 column_names && column_names.include?(column.name)
319 end
327 end
320
328
321 def has_default_columns?
329 def has_default_columns?
322 column_names.nil? || column_names.empty?
330 column_names.nil? || column_names.empty?
323 end
331 end
324
332
325 def sort_criteria=(arg)
333 def sort_criteria=(arg)
326 c = []
334 c = []
327 if arg.is_a?(Hash)
335 if arg.is_a?(Hash)
328 arg = arg.keys.sort.collect {|k| arg[k]}
336 arg = arg.keys.sort.collect {|k| arg[k]}
329 end
337 end
330 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
338 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
331 write_attribute(:sort_criteria, c)
339 write_attribute(:sort_criteria, c)
332 end
340 end
333
341
334 def sort_criteria
342 def sort_criteria
335 read_attribute(:sort_criteria) || []
343 read_attribute(:sort_criteria) || []
336 end
344 end
337
345
338 def sort_criteria_key(arg)
346 def sort_criteria_key(arg)
339 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
347 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
340 end
348 end
341
349
342 def sort_criteria_order(arg)
350 def sort_criteria_order(arg)
343 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
351 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
344 end
352 end
345
353
346 # Returns the SQL sort order that should be prepended for grouping
354 # Returns the SQL sort order that should be prepended for grouping
347 def group_by_sort_order
355 def group_by_sort_order
348 if grouped? && (column = group_by_column)
356 if grouped? && (column = group_by_column)
349 column.sortable.is_a?(Array) ?
357 column.sortable.is_a?(Array) ?
350 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
358 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
351 "#{column.sortable} #{column.default_order}"
359 "#{column.sortable} #{column.default_order}"
352 end
360 end
353 end
361 end
354
362
355 # Returns true if the query is a grouped query
363 # Returns true if the query is a grouped query
356 def grouped?
364 def grouped?
357 !group_by.blank?
365 !group_by.blank?
358 end
366 end
359
367
360 def group_by_column
368 def group_by_column
361 groupable_columns.detect {|c| c.name.to_s == group_by}
369 groupable_columns.detect {|c| c.name.to_s == group_by}
362 end
370 end
363
371
364 def group_by_statement
372 def group_by_statement
365 group_by_column.groupable
373 group_by_column.groupable
366 end
374 end
367
375
368 def project_statement
376 def project_statement
369 project_clauses = []
377 project_clauses = []
370 if project && !@project.descendants.active.empty?
378 if project && !@project.descendants.active.empty?
371 ids = [project.id]
379 ids = [project.id]
372 if has_filter?("subproject_id")
380 if has_filter?("subproject_id")
373 case operator_for("subproject_id")
381 case operator_for("subproject_id")
374 when '='
382 when '='
375 # include the selected subprojects
383 # include the selected subprojects
376 ids += values_for("subproject_id").each(&:to_i)
384 ids += values_for("subproject_id").each(&:to_i)
377 when '!*'
385 when '!*'
378 # main project only
386 # main project only
379 else
387 else
380 # all subprojects
388 # all subprojects
381 ids += project.descendants.collect(&:id)
389 ids += project.descendants.collect(&:id)
382 end
390 end
383 elsif Setting.display_subprojects_issues?
391 elsif Setting.display_subprojects_issues?
384 ids += project.descendants.collect(&:id)
392 ids += project.descendants.collect(&:id)
385 end
393 end
386 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
394 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
387 elsif project
395 elsif project
388 project_clauses << "#{Project.table_name}.id = %d" % project.id
396 project_clauses << "#{Project.table_name}.id = %d" % project.id
389 end
397 end
390 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
398 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
391 project_clauses.join(' AND ')
399 project_clauses.join(' AND ')
392 end
400 end
393
401
394 def statement
402 def statement
395 # filters clauses
403 # filters clauses
396 filters_clauses = []
404 filters_clauses = []
397 filters.each_key do |field|
405 filters.each_key do |field|
398 next if field == "subproject_id"
406 next if field == "subproject_id"
399 v = values_for(field).clone
407 v = values_for(field).clone
400 next unless v and !v.empty?
408 next unless v and !v.empty?
401 operator = operator_for(field)
409 operator = operator_for(field)
402
410
403 # "me" value subsitution
411 # "me" value subsitution
404 if %w(assigned_to_id author_id watcher_id).include?(field)
412 if %w(assigned_to_id author_id watcher_id).include?(field)
405 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
413 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
406 end
414 end
407
415
408 sql = ''
416 sql = ''
409 if field =~ /^cf_(\d+)$/
417 if field =~ /^cf_(\d+)$/
410 # custom field
418 # custom field
411 db_table = CustomValue.table_name
419 db_table = CustomValue.table_name
412 db_field = 'value'
420 db_field = 'value'
413 is_custom_filter = true
421 is_custom_filter = true
414 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 "
422 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 "
415 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
423 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
416 elsif field == 'watcher_id'
424 elsif field == 'watcher_id'
417 db_table = Watcher.table_name
425 db_table = Watcher.table_name
418 db_field = 'user_id'
426 db_field = 'user_id'
419 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
427 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
420 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
428 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
421 else
429 else
422 # regular field
430 # regular field
423 db_table = Issue.table_name
431 db_table = Issue.table_name
424 db_field = field
432 db_field = field
425 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
433 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
426 end
434 end
427 filters_clauses << sql
435 filters_clauses << sql
428
436
429 end if filters and valid?
437 end if filters and valid?
430
438
431 (filters_clauses << project_statement).join(' AND ')
439 (filters_clauses << project_statement).join(' AND ')
432 end
440 end
433
441
434 # Returns the issue count
442 # Returns the issue count
435 def issue_count
443 def issue_count
436 Issue.count(:include => [:status, :project], :conditions => statement)
444 Issue.count(:include => [:status, :project], :conditions => statement)
437 rescue ::ActiveRecord::StatementInvalid => e
445 rescue ::ActiveRecord::StatementInvalid => e
438 raise StatementInvalid.new(e.message)
446 raise StatementInvalid.new(e.message)
439 end
447 end
440
448
441 # Returns the issue count by group or nil if query is not grouped
449 # Returns the issue count by group or nil if query is not grouped
442 def issue_count_by_group
450 def issue_count_by_group
443 r = nil
451 r = nil
444 if grouped?
452 if grouped?
445 begin
453 begin
446 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
454 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
447 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
455 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
448 rescue ActiveRecord::RecordNotFound
456 rescue ActiveRecord::RecordNotFound
449 r = {nil => issue_count}
457 r = {nil => issue_count}
450 end
458 end
451 c = group_by_column
459 c = group_by_column
452 if c.is_a?(QueryCustomFieldColumn)
460 if c.is_a?(QueryCustomFieldColumn)
453 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
461 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
454 end
462 end
455 end
463 end
456 r
464 r
457 rescue ::ActiveRecord::StatementInvalid => e
465 rescue ::ActiveRecord::StatementInvalid => e
458 raise StatementInvalid.new(e.message)
466 raise StatementInvalid.new(e.message)
459 end
467 end
460
468
461 # Returns the issues
469 # Returns the issues
462 # Valid options are :order, :offset, :limit, :include, :conditions
470 # Valid options are :order, :offset, :limit, :include, :conditions
463 def issues(options={})
471 def issues(options={})
464 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
472 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
465 order_option = nil if order_option.blank?
473 order_option = nil if order_option.blank?
466
474
467 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
475 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
468 :conditions => Query.merge_conditions(statement, options[:conditions]),
476 :conditions => Query.merge_conditions(statement, options[:conditions]),
469 :order => order_option,
477 :order => order_option,
470 :limit => options[:limit],
478 :limit => options[:limit],
471 :offset => options[:offset]
479 :offset => options[:offset]
472 rescue ::ActiveRecord::StatementInvalid => e
480 rescue ::ActiveRecord::StatementInvalid => e
473 raise StatementInvalid.new(e.message)
481 raise StatementInvalid.new(e.message)
474 end
482 end
475
483
476 # Returns the journals
484 # Returns the journals
477 # Valid options are :order, :offset, :limit
485 # Valid options are :order, :offset, :limit
478 def journals(options={})
486 def journals(options={})
479 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
487 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
480 :conditions => statement,
488 :conditions => statement,
481 :order => options[:order],
489 :order => options[:order],
482 :limit => options[:limit],
490 :limit => options[:limit],
483 :offset => options[:offset]
491 :offset => options[:offset]
484 rescue ::ActiveRecord::StatementInvalid => e
492 rescue ::ActiveRecord::StatementInvalid => e
485 raise StatementInvalid.new(e.message)
493 raise StatementInvalid.new(e.message)
486 end
494 end
487
495
488 # Returns the versions
496 # Returns the versions
489 # Valid options are :conditions
497 # Valid options are :conditions
490 def versions(options={})
498 def versions(options={})
491 Version.find :all, :include => :project,
499 Version.find :all, :include => :project,
492 :conditions => Query.merge_conditions(project_statement, options[:conditions])
500 :conditions => Query.merge_conditions(project_statement, options[:conditions])
493 rescue ::ActiveRecord::StatementInvalid => e
501 rescue ::ActiveRecord::StatementInvalid => e
494 raise StatementInvalid.new(e.message)
502 raise StatementInvalid.new(e.message)
495 end
503 end
496
504
497 private
505 private
498
506
499 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
507 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
500 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
508 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
501 sql = ''
509 sql = ''
502 case operator
510 case operator
503 when "="
511 when "="
504 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
512 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
505 when "!"
513 when "!"
506 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
514 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
507 when "!*"
515 when "!*"
508 sql = "#{db_table}.#{db_field} IS NULL"
516 sql = "#{db_table}.#{db_field} IS NULL"
509 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
517 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
510 when "*"
518 when "*"
511 sql = "#{db_table}.#{db_field} IS NOT NULL"
519 sql = "#{db_table}.#{db_field} IS NOT NULL"
512 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
520 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
513 when ">="
521 when ">="
514 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
522 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
515 when "<="
523 when "<="
516 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
524 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
517 when "o"
525 when "o"
518 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
526 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
519 when "c"
527 when "c"
520 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
528 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
521 when ">t-"
529 when ">t-"
522 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
530 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
523 when "<t-"
531 when "<t-"
524 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
532 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
525 when "t-"
533 when "t-"
526 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
534 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
527 when ">t+"
535 when ">t+"
528 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
536 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
529 when "<t+"
537 when "<t+"
530 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
538 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
531 when "t+"
539 when "t+"
532 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
540 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
533 when "t"
541 when "t"
534 sql = date_range_clause(db_table, db_field, 0, 0)
542 sql = date_range_clause(db_table, db_field, 0, 0)
535 when "w"
543 when "w"
536 from = l(:general_first_day_of_week) == '7' ?
544 from = l(:general_first_day_of_week) == '7' ?
537 # week starts on sunday
545 # week starts on sunday
538 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
546 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
539 # week starts on monday (Rails default)
547 # week starts on monday (Rails default)
540 Time.now.at_beginning_of_week
548 Time.now.at_beginning_of_week
541 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
549 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
542 when "~"
550 when "~"
543 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
551 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
544 when "!~"
552 when "!~"
545 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
553 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
546 end
554 end
547
555
548 return sql
556 return sql
549 end
557 end
550
558
551 def add_custom_fields_filters(custom_fields)
559 def add_custom_fields_filters(custom_fields)
552 @available_filters ||= {}
560 @available_filters ||= {}
553
561
554 custom_fields.select(&:is_filter?).each do |field|
562 custom_fields.select(&:is_filter?).each do |field|
555 case field.field_format
563 case field.field_format
556 when "text"
564 when "text"
557 options = { :type => :text, :order => 20 }
565 options = { :type => :text, :order => 20 }
558 when "list"
566 when "list"
559 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
567 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
560 when "date"
568 when "date"
561 options = { :type => :date, :order => 20 }
569 options = { :type => :date, :order => 20 }
562 when "bool"
570 when "bool"
563 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
571 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
564 else
572 else
565 options = { :type => :string, :order => 20 }
573 options = { :type => :string, :order => 20 }
566 end
574 end
567 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
575 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
568 end
576 end
569 end
577 end
570
578
571 # Returns a SQL clause for a date or datetime field.
579 # Returns a SQL clause for a date or datetime field.
572 def date_range_clause(table, field, from, to)
580 def date_range_clause(table, field, from, to)
573 s = []
581 s = []
574 if from
582 if from
575 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
583 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
576 end
584 end
577 if to
585 if to
578 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
586 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
579 end
587 end
580 s.join(' AND ')
588 s.join(' AND ')
581 end
589 end
582 end
590 end
General Comments 0
You need to be logged in to leave comments. Login now