Mercurial > hg > soundsoftware-site
comparison app/models/query.rb @ 514:7eba09d624db live
Merge
author | Chris Cannam |
---|---|
date | Thu, 14 Jul 2011 10:50:53 +0100 |
parents | 0c939c159af4 |
children | cbb26bc654de |
comparison
equal
deleted
inserted
replaced
512:b9aebdd7dd40 | 514:7eba09d624db |
---|---|
1 # Redmine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006-2008 Jean-Philippe Lang | 2 # Copyright (C) 2006-2011 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 | |
45 | |
46 def css_classes | |
47 name | |
44 end | 48 end |
45 end | 49 end |
46 | 50 |
47 class QueryCustomFieldColumn < QueryColumn | 51 class QueryCustomFieldColumn < QueryColumn |
48 | 52 |
53 self.groupable = custom_field.order_statement | 57 self.groupable = custom_field.order_statement |
54 end | 58 end |
55 self.groupable ||= false | 59 self.groupable ||= false |
56 @cf = custom_field | 60 @cf = custom_field |
57 end | 61 end |
58 | 62 |
59 def caption | 63 def caption |
60 @cf.name | 64 @cf.name |
61 end | 65 end |
62 | 66 |
63 def custom_field | 67 def custom_field |
64 @cf | 68 @cf |
65 end | 69 end |
66 | 70 |
67 def value(issue) | 71 def value(issue) |
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id} | 72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id} |
69 cv && @cf.cast_value(cv.value) | 73 cv && @cf.cast_value(cv.value) |
70 end | 74 end |
75 | |
76 def css_classes | |
77 @css_classes ||= "#{name} #{@cf.field_format}" | |
78 end | |
71 end | 79 end |
72 | 80 |
73 class Query < ActiveRecord::Base | 81 class Query < ActiveRecord::Base |
74 class StatementInvalid < ::ActiveRecord::StatementInvalid | 82 class StatementInvalid < ::ActiveRecord::StatementInvalid |
75 end | 83 end |
76 | 84 |
77 belongs_to :project | 85 belongs_to :project |
78 belongs_to :user | 86 belongs_to :user |
79 serialize :filters | 87 serialize :filters |
80 serialize :column_names | 88 serialize :column_names |
81 serialize :sort_criteria, Array | 89 serialize :sort_criteria, Array |
82 | 90 |
83 attr_protected :project_id, :user_id | 91 attr_protected :project_id, :user_id |
84 | 92 |
85 validates_presence_of :name, :on => :save | 93 validates_presence_of :name, :on => :save |
86 validates_length_of :name, :maximum => 255 | 94 validates_length_of :name, :maximum => 255 |
87 | 95 |
88 @@operators = { "=" => :label_equals, | 96 @@operators = { "=" => :label_equals, |
89 "!" => :label_not_equals, | 97 "!" => :label_not_equals, |
90 "o" => :label_open_issues, | 98 "o" => :label_open_issues, |
91 "c" => :label_closed_issues, | 99 "c" => :label_closed_issues, |
92 "!*" => :label_none, | 100 "!*" => :label_none, |
93 "*" => :label_all, | 101 "*" => :label_all, |
103 "t-" => :label_ago, | 111 "t-" => :label_ago, |
104 "~" => :label_contains, | 112 "~" => :label_contains, |
105 "!~" => :label_not_contains } | 113 "!~" => :label_not_contains } |
106 | 114 |
107 cattr_reader :operators | 115 cattr_reader :operators |
108 | 116 |
109 @@operators_by_filter_type = { :list => [ "=", "!" ], | 117 @@operators_by_filter_type = { :list => [ "=", "!" ], |
110 :list_status => [ "o", "=", "!", "c", "*" ], | 118 :list_status => [ "o", "=", "!", "c", "*" ], |
111 :list_optional => [ "=", "!", "!*", "*" ], | 119 :list_optional => [ "=", "!", "!*", "*" ], |
112 :list_subprojects => [ "*", "!*", "=" ], | 120 :list_subprojects => [ "*", "!*", "=" ], |
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ], | 121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ], |
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), | 143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), |
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), | 144 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'), | 145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), |
138 ] | 146 ] |
139 cattr_reader :available_columns | 147 cattr_reader :available_columns |
140 | 148 |
141 def initialize(attributes = nil) | 149 def initialize(attributes = nil) |
142 super attributes | 150 super attributes |
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } | 151 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } |
144 end | 152 end |
145 | 153 |
146 def after_initialize | 154 def after_initialize |
147 # Store the fact that project is nil (used in #editable_by?) | 155 # Store the fact that project is nil (used in #editable_by?) |
148 @is_for_all = project.nil? | 156 @is_for_all = project.nil? |
149 end | 157 end |
150 | 158 |
151 def validate | 159 def validate |
152 filters.each_key do |field| | 160 filters.each_key do |field| |
153 errors.add label_for(field), :blank unless | 161 errors.add label_for(field), :blank unless |
154 # filter requires one or more values | 162 # filter requires one or more values |
155 (values_for(field) and !values_for(field).first.blank?) or | 163 (values_for(field) and !values_for(field).first.blank?) or |
156 # filter doesn't require any value | 164 # filter doesn't require any value |
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) | 165 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) |
158 end if filters | 166 end if filters |
159 end | 167 end |
160 | 168 |
169 # Returns true if the query is visible to +user+ or the current user. | |
170 def visible?(user=User.current) | |
171 self.is_public? || self.user_id == user.id | |
172 end | |
173 | |
161 def editable_by?(user) | 174 def editable_by?(user) |
162 return false unless user | 175 return false unless user |
163 # Admin can edit them all and regular users can edit their private queries | 176 # 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) | 177 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) | 178 # 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) | 179 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) |
167 end | 180 end |
168 | 181 |
169 def available_filters | 182 def available_filters |
170 return @available_filters if @available_filters | 183 return @available_filters if @available_filters |
171 | 184 |
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers | 185 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers |
173 | 186 |
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, | 187 @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] } }, | 188 "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] } }, | 189 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } }, |
177 "subject" => { :type => :text, :order => 8 }, | 190 "subject" => { :type => :text, :order => 8 }, |
178 "created_on" => { :type => :date_past, :order => 9 }, | 191 "created_on" => { :type => :date_past, :order => 9 }, |
179 "updated_on" => { :type => :date_past, :order => 10 }, | 192 "updated_on" => { :type => :date_past, :order => 10 }, |
180 "start_date" => { :type => :date, :order => 11 }, | 193 "start_date" => { :type => :date, :order => 11 }, |
181 "due_date" => { :type => :date, :order => 12 }, | 194 "due_date" => { :type => :date, :order => 12 }, |
182 "estimated_hours" => { :type => :integer, :order => 13 }, | 195 "estimated_hours" => { :type => :integer, :order => 13 }, |
183 "done_ratio" => { :type => :integer, :order => 14 }} | 196 "done_ratio" => { :type => :integer, :order => 14 }} |
184 | 197 |
185 user_values = [] | 198 user_values = [] |
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? | 199 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? |
187 if project | 200 if project |
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } | 201 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } |
189 else | 202 else |
190 project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id) | 203 all_projects = Project.visible.all |
191 if project_ids.any? | 204 if all_projects.any? |
192 # members of the user's projects | 205 # members of visible 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] } | 206 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] } |
207 | |
208 # project filter | |
209 project_values = [] | |
210 Project.project_tree(all_projects) do |p, level| | |
211 prefix = (level > 0 ? ('--' * level + ' ') : '') | |
212 project_values << ["#{prefix}#{p.name}", p.id.to_s] | |
213 end | |
214 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty? | |
194 end | 215 end |
195 end | 216 end |
196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? | 217 @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? | 218 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty? |
198 | 219 |
199 group_values = Group.all.collect {|g| [g.name, g.id.to_s] } | 220 group_values = Group.all.collect {|g| [g.name, g.id.to_s] } |
200 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? | 221 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? |
201 | 222 |
202 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } | 223 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } |
203 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? | 224 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? |
204 | 225 |
205 if User.current.logged? | 226 if User.current.logged? |
206 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } | 227 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } |
207 end | 228 end |
208 | 229 |
209 if project | 230 if project |
210 # project specific filters | 231 # project specific filters |
211 unless @project.issue_categories.empty? | 232 categories = @project.issue_categories.all |
212 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } | 233 unless categories.empty? |
213 end | 234 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } } |
214 unless @project.shared_versions.empty? | 235 end |
215 @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] } } | 236 versions = @project.shared_versions.all |
216 end | 237 unless versions.empty? |
217 unless @project.descendants.active.empty? | 238 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } |
218 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } } | 239 end |
240 unless @project.leaf? | |
241 subprojects = @project.descendants.visible.all | |
242 unless subprojects.empty? | |
243 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } } | |
244 end | |
219 end | 245 end |
220 add_custom_fields_filters(@project.all_issue_custom_fields) | 246 add_custom_fields_filters(@project.all_issue_custom_fields) |
221 else | 247 else |
222 # global filters for cross project issue list | 248 # global filters for cross project issue list |
223 system_shared_versions = Version.visible.find_all_by_sharing('system') | 249 system_shared_versions = Version.visible.find_all_by_sharing('system') |
224 unless system_shared_versions.empty? | 250 unless system_shared_versions.empty? |
225 @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] } } | 251 @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] } } |
226 end | 252 end |
227 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) | 253 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) |
228 # project filter | |
229 project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p| | |
230 pre = (p.level > 0 ? ('--' * p.level + ' ') : '') | |
231 ["#{pre}#{p.name}",p.id.to_s] | |
232 end | |
233 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} | |
234 end | 254 end |
235 @available_filters | 255 @available_filters |
236 end | 256 end |
237 | 257 |
238 def add_filter(field, operator, values) | 258 def add_filter(field, operator, values) |
239 # values must be an array | 259 # values must be an array |
240 return unless values and values.is_a? Array # and !values.first.empty? | 260 return unless values and values.is_a? Array # and !values.first.empty? |
241 # check if field is defined as an available filter | 261 # check if field is defined as an available filter |
242 if available_filters.has_key? field | 262 if available_filters.has_key? field |
247 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator | 267 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator |
248 #end | 268 #end |
249 filters[field] = {:operator => operator, :values => values } | 269 filters[field] = {:operator => operator, :values => values } |
250 end | 270 end |
251 end | 271 end |
252 | 272 |
253 def add_short_filter(field, expression) | 273 def add_short_filter(field, expression) |
254 return unless expression | 274 return unless expression |
255 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first | 275 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first |
256 add_filter field, (parms[0] || "="), [parms[1] || ""] | 276 add_filter field, (parms[0] || "="), [parms[1] || ""] |
257 end | 277 end |
262 fields.each do |field| | 282 fields.each do |field| |
263 add_filter(field, operators[field], values[field]) | 283 add_filter(field, operators[field], values[field]) |
264 end | 284 end |
265 end | 285 end |
266 end | 286 end |
267 | 287 |
268 def has_filter?(field) | 288 def has_filter?(field) |
269 filters and filters[field] | 289 filters and filters[field] |
270 end | 290 end |
271 | 291 |
272 def operator_for(field) | 292 def operator_for(field) |
273 has_filter?(field) ? filters[field][:operator] : nil | 293 has_filter?(field) ? filters[field][:operator] : nil |
274 end | 294 end |
275 | 295 |
276 def values_for(field) | 296 def values_for(field) |
277 has_filter?(field) ? filters[field][:values] : nil | 297 has_filter?(field) ? filters[field][:values] : nil |
278 end | 298 end |
279 | 299 |
280 def label_for(field) | 300 def label_for(field) |
281 label = available_filters[field][:name] if available_filters.has_key?(field) | 301 label = available_filters[field][:name] if available_filters.has_key?(field) |
282 label ||= field.gsub(/\_id$/, "") | 302 label ||= field.gsub(/\_id$/, "") |
283 end | 303 end |
284 | 304 |
286 return @available_columns if @available_columns | 306 return @available_columns if @available_columns |
287 @available_columns = Query.available_columns | 307 @available_columns = Query.available_columns |
288 @available_columns += (project ? | 308 @available_columns += (project ? |
289 project.all_issue_custom_fields : | 309 project.all_issue_custom_fields : |
290 IssueCustomField.find(:all) | 310 IssueCustomField.find(:all) |
291 ).collect {|cf| QueryCustomFieldColumn.new(cf) } | 311 ).collect {|cf| QueryCustomFieldColumn.new(cf) } |
292 end | 312 end |
293 | 313 |
294 def self.available_columns=(v) | 314 def self.available_columns=(v) |
295 self.available_columns = (v) | 315 self.available_columns = (v) |
296 end | 316 end |
297 | 317 |
298 def self.add_available_column(column) | 318 def self.add_available_column(column) |
299 self.available_columns << (column) if column.is_a?(QueryColumn) | 319 self.available_columns << (column) if column.is_a?(QueryColumn) |
300 end | 320 end |
301 | 321 |
302 # Returns an array of columns that can be used to group the results | 322 # Returns an array of columns that can be used to group the results |
303 def groupable_columns | 323 def groupable_columns |
304 available_columns.select {|c| c.groupable} | 324 available_columns.select {|c| c.groupable} |
305 end | 325 end |
306 | 326 |
309 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column| | 329 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column| |
310 h[column.name.to_s] = column.sortable | 330 h[column.name.to_s] = column.sortable |
311 h | 331 h |
312 }) | 332 }) |
313 end | 333 end |
314 | 334 |
315 def columns | 335 def columns |
316 if has_default_columns? | 336 if has_default_columns? |
317 available_columns.select do |c| | 337 available_columns.select do |c| |
318 # Adds the project column by default for cross-project lists | 338 # Adds the project column by default for cross-project lists |
319 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?) | 339 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?) |
321 else | 341 else |
322 # preserve the column_names order | 342 # preserve the column_names order |
323 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact | 343 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact |
324 end | 344 end |
325 end | 345 end |
326 | 346 |
327 def column_names=(names) | 347 def column_names=(names) |
328 if names | 348 if names |
329 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } | 349 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } |
330 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } | 350 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } |
331 # Set column_names to nil if default columns | 351 # Set column_names to nil if default columns |
333 names = nil | 353 names = nil |
334 end | 354 end |
335 end | 355 end |
336 write_attribute(:column_names, names) | 356 write_attribute(:column_names, names) |
337 end | 357 end |
338 | 358 |
339 def has_column?(column) | 359 def has_column?(column) |
340 column_names && column_names.include?(column.name) | 360 column_names && column_names.include?(column.name) |
341 end | 361 end |
342 | 362 |
343 def has_default_columns? | 363 def has_default_columns? |
344 column_names.nil? || column_names.empty? | 364 column_names.nil? || column_names.empty? |
345 end | 365 end |
346 | 366 |
347 def sort_criteria=(arg) | 367 def sort_criteria=(arg) |
348 c = [] | 368 c = [] |
349 if arg.is_a?(Hash) | 369 if arg.is_a?(Hash) |
350 arg = arg.keys.sort.collect {|k| arg[k]} | 370 arg = arg.keys.sort.collect {|k| arg[k]} |
351 end | 371 end |
352 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} | 372 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} |
353 write_attribute(:sort_criteria, c) | 373 write_attribute(:sort_criteria, c) |
354 end | 374 end |
355 | 375 |
356 def sort_criteria | 376 def sort_criteria |
357 read_attribute(:sort_criteria) || [] | 377 read_attribute(:sort_criteria) || [] |
358 end | 378 end |
359 | 379 |
360 def sort_criteria_key(arg) | 380 def sort_criteria_key(arg) |
361 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first | 381 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first |
362 end | 382 end |
363 | 383 |
364 def sort_criteria_order(arg) | 384 def sort_criteria_order(arg) |
365 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last | 385 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last |
366 end | 386 end |
367 | 387 |
368 # Returns the SQL sort order that should be prepended for grouping | 388 # Returns the SQL sort order that should be prepended for grouping |
369 def group_by_sort_order | 389 def group_by_sort_order |
370 if grouped? && (column = group_by_column) | 390 if grouped? && (column = group_by_column) |
371 column.sortable.is_a?(Array) ? | 391 column.sortable.is_a?(Array) ? |
372 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : | 392 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : |
373 "#{column.sortable} #{column.default_order}" | 393 "#{column.sortable} #{column.default_order}" |
374 end | 394 end |
375 end | 395 end |
376 | 396 |
377 # Returns true if the query is a grouped query | 397 # Returns true if the query is a grouped query |
378 def grouped? | 398 def grouped? |
379 !group_by.blank? | 399 !group_by_column.nil? |
380 end | 400 end |
381 | 401 |
382 def group_by_column | 402 def group_by_column |
383 groupable_columns.detect {|c| c.name.to_s == group_by} | 403 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by} |
384 end | 404 end |
385 | 405 |
386 def group_by_statement | 406 def group_by_statement |
387 group_by_column.groupable | 407 group_by_column.try(:groupable) |
388 end | 408 end |
389 | 409 |
390 def project_statement | 410 def project_statement |
391 project_clauses = [] | 411 project_clauses = [] |
392 if project && !@project.descendants.active.empty? | 412 if project && !@project.descendants.active.empty? |
393 ids = [project.id] | 413 ids = [project.id] |
394 if has_filter?("subproject_id") | 414 if has_filter?("subproject_id") |
407 end | 427 end |
408 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') | 428 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') |
409 elsif project | 429 elsif project |
410 project_clauses << "#{Project.table_name}.id = %d" % project.id | 430 project_clauses << "#{Project.table_name}.id = %d" % project.id |
411 end | 431 end |
412 project_clauses << Project.allowed_to_condition(User.current, :view_issues) | 432 project_clauses.any? ? project_clauses.join(' AND ') : nil |
413 project_clauses.join(' AND ') | |
414 end | 433 end |
415 | 434 |
416 def statement | 435 def statement |
417 # filters clauses | 436 # filters clauses |
418 filters_clauses = [] | 437 filters_clauses = [] |
419 filters.each_key do |field| | 438 filters.each_key do |field| |
420 next if field == "subproject_id" | 439 next if field == "subproject_id" |
421 v = values_for(field).clone | 440 v = values_for(field).clone |
422 next unless v and !v.empty? | 441 next unless v and !v.empty? |
423 operator = operator_for(field) | 442 operator = operator_for(field) |
424 | 443 |
425 # "me" value subsitution | 444 # "me" value subsitution |
426 if %w(assigned_to_id author_id watcher_id).include?(field) | 445 if %w(assigned_to_id author_id watcher_id).include?(field) |
427 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") | 446 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") |
428 end | 447 end |
429 | 448 |
430 sql = '' | 449 sql = '' |
431 if field =~ /^cf_(\d+)$/ | 450 if field =~ /^cf_(\d+)$/ |
432 # custom field | 451 # custom field |
433 db_table = CustomValue.table_name | 452 db_table = CustomValue.table_name |
434 db_field = 'value' | 453 db_field = 'value' |
456 if group && group.user_ids.present? | 475 if group && group.user_ids.present? |
457 user_ids << group.user_ids | 476 user_ids << group.user_ids |
458 end | 477 end |
459 user_ids.flatten.uniq.compact | 478 user_ids.flatten.uniq.compact |
460 }.sort.collect(&:to_s) | 479 }.sort.collect(&:to_s) |
461 | 480 |
462 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' | 481 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' |
463 | 482 |
464 elsif field == "assigned_to_role" # named field | 483 elsif field == "assigned_to_role" # named field |
465 if operator == "*" # Any Role | 484 if operator == "*" # Any Role |
466 roles = Role.givable | 485 roles = Role.givable |
470 operator = '!' # Override the operator since we want to find by assigned_to | 489 operator = '!' # Override the operator since we want to find by assigned_to |
471 else | 490 else |
472 roles = Role.givable.find_all_by_id(v) | 491 roles = Role.givable.find_all_by_id(v) |
473 end | 492 end |
474 roles ||= [] | 493 roles ||= [] |
475 | 494 |
476 members_of_roles = roles.inject([]) {|user_ids, role| | 495 members_of_roles = roles.inject([]) {|user_ids, role| |
477 if role && role.members | 496 if role && role.members |
478 user_ids << role.members.collect(&:user_id) | 497 user_ids << role.members.collect(&:user_id) |
479 end | 498 end |
480 user_ids.flatten.uniq.compact | 499 user_ids.flatten.uniq.compact |
481 }.sort.collect(&:to_s) | 500 }.sort.collect(&:to_s) |
482 | 501 |
483 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')' | 502 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')' |
484 else | 503 else |
485 # regular field | 504 # regular field |
486 db_table = Issue.table_name | 505 db_table = Issue.table_name |
487 db_field = field | 506 db_field = field |
488 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')' | 507 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')' |
489 end | 508 end |
490 filters_clauses << sql | 509 filters_clauses << sql |
491 | 510 |
492 end if filters and valid? | 511 end if filters and valid? |
493 | 512 |
494 (filters_clauses << project_statement).join(' AND ') | 513 filters_clauses << project_statement |
495 end | 514 filters_clauses.reject!(&:blank?) |
496 | 515 |
516 filters_clauses.any? ? filters_clauses.join(' AND ') : nil | |
517 end | |
518 | |
497 # Returns the issue count | 519 # Returns the issue count |
498 def issue_count | 520 def issue_count |
499 Issue.count(:include => [:status, :project], :conditions => statement) | 521 Issue.count(:include => [:status, :project], :conditions => statement) |
500 rescue ::ActiveRecord::StatementInvalid => e | 522 rescue ::ActiveRecord::StatementInvalid => e |
501 raise StatementInvalid.new(e.message) | 523 raise StatementInvalid.new(e.message) |
502 end | 524 end |
503 | 525 |
504 # Returns the issue count by group or nil if query is not grouped | 526 # Returns the issue count by group or nil if query is not grouped |
505 def issue_count_by_group | 527 def issue_count_by_group |
506 r = nil | 528 r = nil |
507 if grouped? | 529 if grouped? |
508 begin | 530 begin |
509 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value | 531 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value |
510 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) | 532 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) |
511 rescue ActiveRecord::RecordNotFound | 533 rescue ActiveRecord::RecordNotFound |
512 r = {nil => issue_count} | 534 r = {nil => issue_count} |
513 end | 535 end |
514 c = group_by_column | 536 c = group_by_column |
515 if c.is_a?(QueryCustomFieldColumn) | 537 if c.is_a?(QueryCustomFieldColumn) |
518 end | 540 end |
519 r | 541 r |
520 rescue ::ActiveRecord::StatementInvalid => e | 542 rescue ::ActiveRecord::StatementInvalid => e |
521 raise StatementInvalid.new(e.message) | 543 raise StatementInvalid.new(e.message) |
522 end | 544 end |
523 | 545 |
524 # Returns the issues | 546 # Returns the issues |
525 # Valid options are :order, :offset, :limit, :include, :conditions | 547 # Valid options are :order, :offset, :limit, :include, :conditions |
526 def issues(options={}) | 548 def issues(options={}) |
527 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') | 549 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') |
528 order_option = nil if order_option.blank? | 550 order_option = nil if order_option.blank? |
529 | 551 |
530 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, | 552 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, |
531 :conditions => Query.merge_conditions(statement, options[:conditions]), | 553 :conditions => Query.merge_conditions(statement, options[:conditions]), |
532 :order => order_option, | 554 :order => order_option, |
533 :limit => options[:limit], | 555 :limit => options[:limit], |
534 :offset => options[:offset] | 556 :offset => options[:offset] |
535 rescue ::ActiveRecord::StatementInvalid => e | 557 rescue ::ActiveRecord::StatementInvalid => e |
537 end | 559 end |
538 | 560 |
539 # Returns the journals | 561 # Returns the journals |
540 # Valid options are :order, :offset, :limit | 562 # Valid options are :order, :offset, :limit |
541 def journals(options={}) | 563 def journals(options={}) |
542 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}], | 564 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}], |
543 :conditions => statement, | 565 :conditions => statement, |
544 :order => options[:order], | 566 :order => options[:order], |
545 :limit => options[:limit], | 567 :limit => options[:limit], |
546 :offset => options[:offset] | 568 :offset => options[:offset] |
547 rescue ::ActiveRecord::StatementInvalid => e | 569 rescue ::ActiveRecord::StatementInvalid => e |
548 raise StatementInvalid.new(e.message) | 570 raise StatementInvalid.new(e.message) |
549 end | 571 end |
550 | 572 |
551 # Returns the versions | 573 # Returns the versions |
552 # Valid options are :conditions | 574 # Valid options are :conditions |
553 def versions(options={}) | 575 def versions(options={}) |
554 Version.find :all, :include => :project, | 576 Version.visible.find :all, :include => :project, |
555 :conditions => Query.merge_conditions(project_statement, options[:conditions]) | 577 :conditions => Query.merge_conditions(project_statement, options[:conditions]) |
556 rescue ::ActiveRecord::StatementInvalid => e | 578 rescue ::ActiveRecord::StatementInvalid => e |
557 raise StatementInvalid.new(e.message) | 579 raise StatementInvalid.new(e.message) |
558 end | 580 end |
559 | 581 |
560 private | 582 private |
561 | 583 |
562 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ | 584 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ |
563 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) | 585 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) |
564 sql = '' | 586 sql = '' |
565 case operator | 587 case operator |
566 when "=" | 588 when "=" |
567 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" | 589 if value.any? |
590 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" | |
591 else | |
592 # IN an empty set | |
593 sql = "1=0" | |
594 end | |
568 when "!" | 595 when "!" |
569 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" | 596 if value.any? |
597 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" | |
598 else | |
599 # NOT IN an empty set | |
600 sql = "1=1" | |
601 end | |
570 when "!*" | 602 when "!*" |
571 sql = "#{db_table}.#{db_field} IS NULL" | 603 sql = "#{db_table}.#{db_field} IS NULL" |
572 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter | 604 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter |
573 when "*" | 605 when "*" |
574 sql = "#{db_table}.#{db_field} IS NOT NULL" | 606 sql = "#{db_table}.#{db_field} IS NOT NULL" |
594 when "t+" | 626 when "t+" |
595 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i) | 627 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i) |
596 when "t" | 628 when "t" |
597 sql = date_range_clause(db_table, db_field, 0, 0) | 629 sql = date_range_clause(db_table, db_field, 0, 0) |
598 when "w" | 630 when "w" |
599 from = l(:general_first_day_of_week) == '7' ? | 631 first_day_of_week = l(:general_first_day_of_week).to_i |
600 # week starts on sunday | 632 day_of_week = Date.today.cwday |
601 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) : | 633 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) |
602 # week starts on monday (Rails default) | 634 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6) |
603 Time.now.at_beginning_of_week | |
604 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)] | |
605 when "~" | 635 when "~" |
606 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" | 636 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" |
607 when "!~" | 637 when "!~" |
608 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" | 638 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" |
609 end | 639 end |
610 | 640 |
611 return sql | 641 return sql |
612 end | 642 end |
613 | 643 |
614 def add_custom_fields_filters(custom_fields) | 644 def add_custom_fields_filters(custom_fields) |
615 @available_filters ||= {} | 645 @available_filters ||= {} |
616 | 646 |
617 custom_fields.select(&:is_filter?).each do |field| | 647 custom_fields.select(&:is_filter?).each do |field| |
618 case field.field_format | 648 case field.field_format |
619 when "text" | 649 when "text" |
620 options = { :type => :text, :order => 20 } | 650 options = { :type => :text, :order => 20 } |
621 when "list" | 651 when "list" |
622 options = { :type => :list_optional, :values => field.possible_values, :order => 20} | 652 options = { :type => :list_optional, :values => field.possible_values, :order => 20} |
623 when "date" | 653 when "date" |
624 options = { :type => :date, :order => 20 } | 654 options = { :type => :date, :order => 20 } |
625 when "bool" | 655 when "bool" |
626 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } | 656 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } |
657 when "user", "version" | |
658 next unless project | |
659 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20} | |
627 else | 660 else |
628 options = { :type => :string, :order => 20 } | 661 options = { :type => :string, :order => 20 } |
629 end | 662 end |
630 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) | 663 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) |
631 end | 664 end |
632 end | 665 end |
633 | 666 |
634 # Returns a SQL clause for a date or datetime field. | 667 # Returns a SQL clause for a date or datetime field. |
635 def date_range_clause(table, field, from, to) | 668 def date_range_clause(table, field, from, to) |
636 s = [] | 669 s = [] |
637 if from | 670 if from |
638 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)]) | 671 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)]) |