Mercurial > hg > soundsoftware-site
comparison app/models/query.rb @ 1338:25603efa57b5
Merge from live branch
author | Chris Cannam |
---|---|
date | Thu, 20 Jun 2013 13:14:14 +0100 |
parents | 433d4f72a19b |
children | 622f24f53b42 261b3d9a4903 |
comparison
equal
deleted
inserted
replaced
1209:1b1138f6f55e | 1338:25603efa57b5 |
---|---|
1 # Redmine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006-2011 Jean-Philippe Lang | 2 # Copyright (C) 2006-2012 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. |
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 @inline = options.key?(:inline) ? options[:inline] : true | |
30 @caption_key = options[:caption] || "field_#{name}" | 31 @caption_key = options[:caption] || "field_#{name}" |
31 end | 32 end |
32 | 33 |
33 def caption | 34 def caption |
34 l(@caption_key) | 35 l(@caption_key) |
36 | 37 |
37 # Returns true if the column is sortable, otherwise false | 38 # Returns true if the column is sortable, otherwise false |
38 def sortable? | 39 def sortable? |
39 !@sortable.nil? | 40 !@sortable.nil? |
40 end | 41 end |
41 | 42 |
42 def sortable | 43 def sortable |
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable | 44 @sortable.is_a?(Proc) ? @sortable.call : @sortable |
45 end | |
46 | |
47 def inline? | |
48 @inline | |
44 end | 49 end |
45 | 50 |
46 def value(issue) | 51 def value(issue) |
47 issue.send name | 52 issue.send name |
48 end | 53 end |
55 class QueryCustomFieldColumn < QueryColumn | 60 class QueryCustomFieldColumn < QueryColumn |
56 | 61 |
57 def initialize(custom_field) | 62 def initialize(custom_field) |
58 self.name = "cf_#{custom_field.id}".to_sym | 63 self.name = "cf_#{custom_field.id}".to_sym |
59 self.sortable = custom_field.order_statement || false | 64 self.sortable = custom_field.order_statement || false |
60 if %w(list date bool int).include?(custom_field.field_format) | 65 self.groupable = custom_field.group_statement || false |
61 self.groupable = custom_field.order_statement | 66 @inline = true |
62 end | |
63 self.groupable ||= false | |
64 @cf = custom_field | 67 @cf = custom_field |
65 end | 68 end |
66 | 69 |
67 def caption | 70 def caption |
68 @cf.name | 71 @cf.name |
71 def custom_field | 74 def custom_field |
72 @cf | 75 @cf |
73 end | 76 end |
74 | 77 |
75 def value(issue) | 78 def value(issue) |
76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id} | 79 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)} |
77 cv && @cf.cast_value(cv.value) | 80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first |
78 end | 81 end |
79 | 82 |
80 def css_classes | 83 def css_classes |
81 @css_classes ||= "#{name} #{@cf.field_format}" | 84 @css_classes ||= "#{name} #{@cf.field_format}" |
82 end | 85 end |
92 serialize :column_names | 95 serialize :column_names |
93 serialize :sort_criteria, Array | 96 serialize :sort_criteria, Array |
94 | 97 |
95 attr_protected :project_id, :user_id | 98 attr_protected :project_id, :user_id |
96 | 99 |
97 validates_presence_of :name, :on => :save | 100 validates_presence_of :name |
98 validates_length_of :name, :maximum => 255 | 101 validates_length_of :name, :maximum => 255 |
99 validate :validate_query_filters | 102 validate :validate_query_filters |
100 | 103 |
101 @@operators = { "=" => :label_equals, | 104 @@operators = { "=" => :label_equals, |
102 "!" => :label_not_equals, | 105 "!" => :label_not_equals, |
103 "o" => :label_open_issues, | 106 "o" => :label_open_issues, |
104 "c" => :label_closed_issues, | 107 "c" => :label_closed_issues, |
105 "!*" => :label_none, | 108 "!*" => :label_none, |
106 "*" => :label_all, | 109 "*" => :label_any, |
107 ">=" => :label_greater_or_equal, | 110 ">=" => :label_greater_or_equal, |
108 "<=" => :label_less_or_equal, | 111 "<=" => :label_less_or_equal, |
109 "><" => :label_between, | 112 "><" => :label_between, |
110 "<t+" => :label_in_less_than, | 113 "<t+" => :label_in_less_than, |
111 ">t+" => :label_in_more_than, | 114 ">t+" => :label_in_more_than, |
115 "><t+"=> :label_in_the_next_days, | |
112 "t+" => :label_in, | 116 "t+" => :label_in, |
113 "t" => :label_today, | 117 "t" => :label_today, |
114 "w" => :label_this_week, | 118 "w" => :label_this_week, |
115 ">t-" => :label_less_than_ago, | 119 ">t-" => :label_less_than_ago, |
116 "<t-" => :label_more_than_ago, | 120 "<t-" => :label_more_than_ago, |
121 "><t-"=> :label_in_the_past_days, | |
117 "t-" => :label_ago, | 122 "t-" => :label_ago, |
118 "~" => :label_contains, | 123 "~" => :label_contains, |
119 "!~" => :label_not_contains } | 124 "!~" => :label_not_contains, |
125 "=p" => :label_any_issues_in_project, | |
126 "=!p" => :label_any_issues_not_in_project, | |
127 "!p" => :label_no_issues_in_project} | |
120 | 128 |
121 cattr_reader :operators | 129 cattr_reader :operators |
122 | 130 |
123 @@operators_by_filter_type = { :list => [ "=", "!" ], | 131 @@operators_by_filter_type = { :list => [ "=", "!" ], |
124 :list_status => [ "o", "=", "!", "c", "*" ], | 132 :list_status => [ "o", "=", "!", "c", "*" ], |
125 :list_optional => [ "=", "!", "!*", "*" ], | 133 :list_optional => [ "=", "!", "!*", "*" ], |
126 :list_subprojects => [ "*", "!*", "=" ], | 134 :list_subprojects => [ "*", "!*", "=" ], |
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ], | 135 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ], |
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ], | 136 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ], |
129 :string => [ "=", "~", "!", "!~" ], | 137 :string => [ "=", "~", "!", "!~", "!*", "*" ], |
130 :text => [ "~", "!~" ], | 138 :text => [ "~", "!~", "!*", "*" ], |
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ], | 139 :integer => [ "=", ">=", "<=", "><", "!*", "*" ], |
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] } | 140 :float => [ "=", ">=", "<=", "><", "!*", "*" ], |
141 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]} | |
133 | 142 |
134 cattr_reader :operators_by_filter_type | 143 cattr_reader :operators_by_filter_type |
135 | 144 |
136 @@available_columns = [ | 145 @@available_columns = [ |
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), | 146 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), |
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), | 151 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), |
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true), | 152 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true), |
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), | 153 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), |
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), | 154 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), |
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), | 155 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), |
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), | 156 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true), |
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), | 157 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), |
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), | 158 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), |
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), | 159 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), |
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), | 160 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), |
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), | 161 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), |
162 QueryColumn.new(:relations, :caption => :label_related_issues), | |
163 QueryColumn.new(:description, :inline => false) | |
153 ] | 164 ] |
154 cattr_reader :available_columns | 165 cattr_reader :available_columns |
155 | 166 |
156 named_scope :visible, lambda {|*args| | 167 scope :visible, lambda {|*args| |
157 user = args.shift || User.current | 168 user = args.shift || User.current |
158 base = Project.allowed_to_condition(user, :view_issues, *args) | 169 base = Project.allowed_to_condition(user, :view_issues, *args) |
159 user_id = user.logged? ? user.id : 0 | 170 user_id = user.logged? ? user.id : 0 |
160 { | 171 { |
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id], | 172 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id], |
162 :include => :project | 173 :include => :project |
163 } | 174 } |
164 } | 175 } |
165 | 176 |
166 def initialize(attributes = nil) | 177 def initialize(attributes=nil, *args) |
167 super attributes | 178 super attributes |
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } | 179 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } |
169 end | |
170 | |
171 def after_initialize | |
172 # Store the fact that project is nil (used in #editable_by?) | |
173 @is_for_all = project.nil? | 180 @is_for_all = project.nil? |
174 end | 181 end |
175 | 182 |
176 def validate_query_filters | 183 def validate_query_filters |
177 filters.each_key do |field| | 184 filters.each_key do |field| |
178 if values_for(field) | 185 if values_for(field) |
179 case type_for(field) | 186 case type_for(field) |
180 when :integer | 187 when :integer |
181 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } | 188 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) } |
182 when :float | 189 when :float |
183 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) } | 190 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) } |
184 when :date, :date_past | 191 when :date, :date_past |
185 case operator_for(field) | 192 case operator_for(field) |
186 when "=", ">=", "<=", "><" | 193 when "=", ">=", "<=", "><" |
187 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) } | 194 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) } |
188 when ">t-", "<t-", "t-" | 195 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-" |
189 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } | 196 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } |
190 end | 197 end |
191 end | 198 end |
192 end | 199 end |
193 | 200 |
194 errors.add label_for(field), :blank unless | 201 add_filter_error(field, :blank) unless |
195 # filter requires one or more values | 202 # filter requires one or more values |
196 (values_for(field) and !values_for(field).first.blank?) or | 203 (values_for(field) and !values_for(field).first.blank?) or |
197 # filter doesn't require any value | 204 # filter doesn't require any value |
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) | 205 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) |
199 end if filters | 206 end if filters |
207 end | |
208 | |
209 def add_filter_error(field, message) | |
210 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages') | |
211 errors.add(:base, m) | |
200 end | 212 end |
201 | 213 |
202 # Returns true if the query is visible to +user+ or the current user. | 214 # Returns true if the query is visible to +user+ or the current user. |
203 def visible?(user=User.current) | 215 def visible?(user=User.current) |
204 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) | 216 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) |
210 return true if user.admin? || (!is_public && self.user_id == user.id) | 222 return true if user.admin? || (!is_public && self.user_id == user.id) |
211 # Members can not edit public queries that are for all project (only admin is allowed to) | 223 # Members can not edit public queries that are for all project (only admin is allowed to) |
212 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) | 224 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) |
213 end | 225 end |
214 | 226 |
227 def trackers | |
228 @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers | |
229 end | |
230 | |
231 # Returns a hash of localized labels for all filter operators | |
232 def self.operators_labels | |
233 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h} | |
234 end | |
235 | |
215 def available_filters | 236 def available_filters |
216 return @available_filters if @available_filters | 237 return @available_filters if @available_filters |
217 | 238 @available_filters = { |
218 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers | 239 "status_id" => { |
219 | 240 :type => :list_status, :order => 0, |
220 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, | 241 :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } |
221 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } }, | 242 }, |
222 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } }, | 243 "tracker_id" => { |
223 "subject" => { :type => :text, :order => 8 }, | 244 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } |
224 "created_on" => { :type => :date_past, :order => 9 }, | 245 }, |
225 "updated_on" => { :type => :date_past, :order => 10 }, | 246 "priority_id" => { |
226 "start_date" => { :type => :date, :order => 11 }, | 247 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } |
227 "due_date" => { :type => :date, :order => 12 }, | 248 }, |
228 "estimated_hours" => { :type => :float, :order => 13 }, | 249 "subject" => { :type => :text, :order => 8 }, |
229 "done_ratio" => { :type => :integer, :order => 14 }} | 250 "created_on" => { :type => :date_past, :order => 9 }, |
230 | 251 "updated_on" => { :type => :date_past, :order => 10 }, |
252 "start_date" => { :type => :date, :order => 11 }, | |
253 "due_date" => { :type => :date, :order => 12 }, | |
254 "estimated_hours" => { :type => :float, :order => 13 }, | |
255 "done_ratio" => { :type => :integer, :order => 14 } | |
256 } | |
257 IssueRelation::TYPES.each do |relation_type, options| | |
258 @available_filters[relation_type] = { | |
259 :type => :relation, :order => @available_filters.size + 100, | |
260 :label => options[:name] | |
261 } | |
262 end | |
231 principals = [] | 263 principals = [] |
232 if project | 264 if project |
233 principals += project.principals.sort | 265 principals += project.principals.sort |
266 unless project.leaf? | |
267 subprojects = project.descendants.visible.all | |
268 if subprojects.any? | |
269 @available_filters["subproject_id"] = { | |
270 :type => :list_subprojects, :order => 13, | |
271 :values => subprojects.collect{|s| [s.name, s.id.to_s] } | |
272 } | |
273 principals += Principal.member_of(subprojects) | |
274 end | |
275 end | |
234 else | 276 else |
235 all_projects = Project.visible.all | |
236 if all_projects.any? | 277 if all_projects.any? |
237 # members of visible projects | 278 # members of visible projects |
238 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort | 279 principals += Principal.member_of(all_projects) |
239 | |
240 # project filter | 280 # project filter |
241 project_values = [] | 281 project_values = [] |
242 Project.project_tree(all_projects) do |p, level| | 282 if User.current.logged? && User.current.memberships.any? |
243 prefix = (level > 0 ? ('--' * level + ' ') : '') | 283 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] |
244 project_values << ["#{prefix}#{p.name}", p.id.to_s] | 284 end |
245 end | 285 project_values += all_projects_values |
246 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty? | 286 @available_filters["project_id"] = { |
247 end | 287 :type => :list, :order => 1, :values => project_values |
248 end | 288 } unless project_values.empty? |
289 end | |
290 end | |
291 principals.uniq! | |
292 principals.sort! | |
249 users = principals.select {|p| p.is_a?(User)} | 293 users = principals.select {|p| p.is_a?(User)} |
250 | 294 |
251 assigned_to_values = [] | 295 assigned_to_values = [] |
252 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? | 296 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? |
253 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] } | 297 assigned_to_values += (Setting.issue_group_assignment? ? |
254 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty? | 298 principals : users).collect{|s| [s.name, s.id.to_s] } |
299 @available_filters["assigned_to_id"] = { | |
300 :type => :list_optional, :order => 4, :values => assigned_to_values | |
301 } unless assigned_to_values.empty? | |
255 | 302 |
256 author_values = [] | 303 author_values = [] |
257 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? | 304 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? |
258 author_values += users.collect{|s| [s.name, s.id.to_s] } | 305 author_values += users.collect{|s| [s.name, s.id.to_s] } |
259 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty? | 306 @available_filters["author_id"] = { |
307 :type => :list, :order => 5, :values => author_values | |
308 } unless author_values.empty? | |
260 | 309 |
261 group_values = Group.all.collect {|g| [g.name, g.id.to_s] } | 310 group_values = Group.all.collect {|g| [g.name, g.id.to_s] } |
262 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? | 311 @available_filters["member_of_group"] = { |
312 :type => :list_optional, :order => 6, :values => group_values | |
313 } unless group_values.empty? | |
263 | 314 |
264 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } | 315 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } |
265 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? | 316 @available_filters["assigned_to_role"] = { |
317 :type => :list_optional, :order => 7, :values => role_values | |
318 } unless role_values.empty? | |
266 | 319 |
267 if User.current.logged? | 320 if User.current.logged? |
268 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } | 321 @available_filters["watcher_id"] = { |
322 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] | |
323 } | |
269 end | 324 end |
270 | 325 |
271 if project | 326 if project |
272 # project specific filters | 327 # project specific filters |
273 categories = project.issue_categories.all | 328 categories = project.issue_categories.all |
274 unless categories.empty? | 329 unless categories.empty? |
275 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } } | 330 @available_filters["category_id"] = { |
331 :type => :list_optional, :order => 6, | |
332 :values => categories.collect{|s| [s.name, s.id.to_s] } | |
333 } | |
276 end | 334 end |
277 versions = project.shared_versions.all | 335 versions = project.shared_versions.all |
278 unless versions.empty? | 336 unless versions.empty? |
279 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } | 337 @available_filters["fixed_version_id"] = { |
280 end | 338 :type => :list_optional, :order => 7, |
281 unless project.leaf? | 339 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } |
282 subprojects = project.descendants.visible.all | 340 } |
283 unless subprojects.empty? | |
284 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } } | |
285 end | |
286 end | 341 end |
287 add_custom_fields_filters(project.all_issue_custom_fields) | 342 add_custom_fields_filters(project.all_issue_custom_fields) |
288 else | 343 else |
289 # global filters for cross project issue list | 344 # global filters for cross project issue list |
290 system_shared_versions = Version.visible.find_all_by_sharing('system') | 345 system_shared_versions = Version.visible.find_all_by_sharing('system') |
291 unless system_shared_versions.empty? | 346 unless system_shared_versions.empty? |
292 @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] } } | 347 @available_filters["fixed_version_id"] = { |
293 end | 348 :type => :list_optional, :order => 7, |
294 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) | 349 :values => system_shared_versions.sort.collect{|s| |
350 ["#{s.project.name} - #{s.name}", s.id.to_s] | |
351 } | |
352 } | |
353 end | |
354 add_custom_fields_filters( | |
355 IssueCustomField.find(:all, | |
356 :conditions => { | |
357 :is_filter => true, | |
358 :is_for_all => true | |
359 })) | |
360 end | |
361 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version | |
362 if User.current.allowed_to?(:set_issues_private, nil, :global => true) || | |
363 User.current.allowed_to?(:set_own_issues_private, nil, :global => true) | |
364 @available_filters["is_private"] = { | |
365 :type => :list, :order => 16, | |
366 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] | |
367 } | |
368 end | |
369 Tracker.disabled_core_fields(trackers).each {|field| | |
370 @available_filters.delete field | |
371 } | |
372 @available_filters.each do |field, options| | |
373 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) | |
295 end | 374 end |
296 @available_filters | 375 @available_filters |
376 end | |
377 | |
378 # Returns a representation of the available filters for JSON serialization | |
379 def available_filters_as_json | |
380 json = {} | |
381 available_filters.each do |field, options| | |
382 json[field] = options.slice(:type, :name, :values).stringify_keys | |
383 end | |
384 json | |
385 end | |
386 | |
387 def all_projects | |
388 @all_projects ||= Project.visible.all | |
389 end | |
390 | |
391 def all_projects_values | |
392 return @all_projects_values if @all_projects_values | |
393 | |
394 values = [] | |
395 Project.project_tree(all_projects) do |p, level| | |
396 prefix = (level > 0 ? ('--' * level + ' ') : '') | |
397 values << ["#{prefix}#{p.name}", p.id.to_s] | |
398 end | |
399 @all_projects_values = values | |
297 end | 400 end |
298 | 401 |
299 def add_filter(field, operator, values) | 402 def add_filter(field, operator, values) |
300 # values must be an array | 403 # values must be an array |
301 return unless values.nil? || values.is_a?(Array) | 404 return unless values.nil? || values.is_a?(Array) |
349 (values_for(field) || [])[index] | 452 (values_for(field) || [])[index] |
350 end | 453 end |
351 | 454 |
352 def label_for(field) | 455 def label_for(field) |
353 label = available_filters[field][:name] if available_filters.has_key?(field) | 456 label = available_filters[field][:name] if available_filters.has_key?(field) |
354 label ||= field.gsub(/\_id$/, "") | 457 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field) |
355 end | 458 end |
356 | 459 |
357 def available_columns | 460 def available_columns |
358 return @available_columns if @available_columns | 461 return @available_columns if @available_columns |
359 @available_columns = ::Query.available_columns | 462 @available_columns = ::Query.available_columns.dup |
360 @available_columns += (project ? | 463 @available_columns += (project ? |
361 project.all_issue_custom_fields : | 464 project.all_issue_custom_fields : |
362 IssueCustomField.find(:all) | 465 IssueCustomField.find(:all) |
363 ).collect {|cf| QueryCustomFieldColumn.new(cf) } | 466 ).collect {|cf| QueryCustomFieldColumn.new(cf) } |
467 | |
468 if User.current.allowed_to?(:view_time_entries, project, :global => true) | |
469 index = nil | |
470 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours} | |
471 index = (index ? index + 1 : -1) | |
472 # insert the column after estimated_hours or at the end | |
473 @available_columns.insert index, QueryColumn.new(:spent_hours, | |
474 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)", | |
475 :default_order => 'desc', | |
476 :caption => :label_spent_time | |
477 ) | |
478 end | |
479 | |
480 if User.current.allowed_to?(:set_issues_private, nil, :global => true) || | |
481 User.current.allowed_to?(:set_own_issues_private, nil, :global => true) | |
482 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private") | |
483 end | |
484 | |
485 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} | |
486 @available_columns.reject! {|column| | |
487 disabled_fields.include?(column.name.to_s) | |
488 } | |
489 | |
490 @available_columns | |
364 end | 491 end |
365 | 492 |
366 def self.available_columns=(v) | 493 def self.available_columns=(v) |
367 self.available_columns = (v) | 494 self.available_columns = (v) |
368 end | 495 end |
387 def columns | 514 def columns |
388 # preserve the column_names order | 515 # preserve the column_names order |
389 (has_default_columns? ? default_columns_names : column_names).collect do |name| | 516 (has_default_columns? ? default_columns_names : column_names).collect do |name| |
390 available_columns.find { |col| col.name == name } | 517 available_columns.find { |col| col.name == name } |
391 end.compact | 518 end.compact |
519 end | |
520 | |
521 def inline_columns | |
522 columns.select(&:inline?) | |
523 end | |
524 | |
525 def block_columns | |
526 columns.reject(&:inline?) | |
527 end | |
528 | |
529 def available_inline_columns | |
530 available_columns.select(&:inline?) | |
531 end | |
532 | |
533 def available_block_columns | |
534 available_columns.reject(&:inline?) | |
392 end | 535 end |
393 | 536 |
394 def default_columns_names | 537 def default_columns_names |
395 @default_columns_names ||= begin | 538 @default_columns_names ||= begin |
396 default_columns = Setting.issue_list_default_columns.map(&:to_sym) | 539 default_columns = Setting.issue_list_default_columns.map(&:to_sym) |
410 end | 553 end |
411 write_attribute(:column_names, names) | 554 write_attribute(:column_names, names) |
412 end | 555 end |
413 | 556 |
414 def has_column?(column) | 557 def has_column?(column) |
415 column_names && column_names.include?(column.name) | 558 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column) |
416 end | 559 end |
417 | 560 |
418 def has_default_columns? | 561 def has_default_columns? |
419 column_names.nil? || column_names.empty? | 562 column_names.nil? || column_names.empty? |
420 end | 563 end |
422 def sort_criteria=(arg) | 565 def sort_criteria=(arg) |
423 c = [] | 566 c = [] |
424 if arg.is_a?(Hash) | 567 if arg.is_a?(Hash) |
425 arg = arg.keys.sort.collect {|k| arg[k]} | 568 arg = arg.keys.sort.collect {|k| arg[k]} |
426 end | 569 end |
427 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} | 570 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']} |
428 write_attribute(:sort_criteria, c) | 571 write_attribute(:sort_criteria, c) |
429 end | 572 end |
430 | 573 |
431 def sort_criteria | 574 def sort_criteria |
432 read_attribute(:sort_criteria) || [] | 575 read_attribute(:sort_criteria) || [] |
436 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first | 579 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first |
437 end | 580 end |
438 | 581 |
439 def sort_criteria_order(arg) | 582 def sort_criteria_order(arg) |
440 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last | 583 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last |
584 end | |
585 | |
586 def sort_criteria_order_for(key) | |
587 sort_criteria.detect {|k, order| key.to_s == k}.try(:last) | |
441 end | 588 end |
442 | 589 |
443 # Returns the SQL sort order that should be prepended for grouping | 590 # Returns the SQL sort order that should be prepended for grouping |
444 def group_by_sort_order | 591 def group_by_sort_order |
445 if grouped? && (column = group_by_column) | 592 if grouped? && (column = group_by_column) |
593 order = sort_criteria_order_for(column.name) || column.default_order | |
446 column.sortable.is_a?(Array) ? | 594 column.sortable.is_a?(Array) ? |
447 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : | 595 column.sortable.collect {|s| "#{s} #{order}"}.join(',') : |
448 "#{column.sortable} #{column.default_order}" | 596 "#{column.sortable} #{order}" |
449 end | 597 end |
450 end | 598 end |
451 | 599 |
452 # Returns true if the query is a grouped query | 600 # Returns true if the query is a grouped query |
453 def grouped? | 601 def grouped? |
506 v.push("0") | 654 v.push("0") |
507 end | 655 end |
508 end | 656 end |
509 end | 657 end |
510 | 658 |
511 if field =~ /^cf_(\d+)$/ | 659 if field == 'project_id' |
660 if v.delete('mine') | |
661 v += User.current.memberships.map(&:project_id).map(&:to_s) | |
662 end | |
663 end | |
664 | |
665 if field =~ /cf_(\d+)$/ | |
512 # custom field | 666 # custom field |
513 filters_clauses << sql_for_custom_field(field, operator, v, $1) | 667 filters_clauses << sql_for_custom_field(field, operator, v, $1) |
514 elsif respond_to?("sql_for_#{field}_field") | 668 elsif respond_to?("sql_for_#{field}_field") |
515 # specific statement | 669 # specific statement |
516 filters_clauses << send("sql_for_#{field}_field", field, operator, v) | 670 filters_clauses << send("sql_for_#{field}_field", field, operator, v) |
536 # Returns the issue count by group or nil if query is not grouped | 690 # Returns the issue count by group or nil if query is not grouped |
537 def issue_count_by_group | 691 def issue_count_by_group |
538 r = nil | 692 r = nil |
539 if grouped? | 693 if grouped? |
540 begin | 694 begin |
541 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value | 695 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value |
542 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) | 696 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) |
543 rescue ActiveRecord::RecordNotFound | 697 rescue ActiveRecord::RecordNotFound |
544 r = {nil => issue_count} | 698 r = {nil => issue_count} |
545 end | 699 end |
546 c = group_by_column | 700 c = group_by_column |
556 # Returns the issues | 710 # Returns the issues |
557 # Valid options are :order, :offset, :limit, :include, :conditions | 711 # Valid options are :order, :offset, :limit, :include, :conditions |
558 def issues(options={}) | 712 def issues(options={}) |
559 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') | 713 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') |
560 order_option = nil if order_option.blank? | 714 order_option = nil if order_option.blank? |
561 | 715 |
562 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil | 716 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, |
563 | |
564 Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, | |
565 :conditions => statement, | 717 :conditions => statement, |
566 :order => order_option, | 718 :order => order_option, |
567 :joins => joins, | 719 :joins => joins_for_order_statement(order_option), |
568 :limit => options[:limit], | 720 :limit => options[:limit], |
569 :offset => options[:offset] | 721 :offset => options[:offset] |
722 | |
723 if has_column?(:spent_hours) | |
724 Issue.load_visible_spent_hours(issues) | |
725 end | |
726 if has_column?(:relations) | |
727 Issue.load_visible_relations(issues) | |
728 end | |
729 issues | |
730 rescue ::ActiveRecord::StatementInvalid => e | |
731 raise StatementInvalid.new(e.message) | |
732 end | |
733 | |
734 # Returns the issues ids | |
735 def issue_ids(options={}) | |
736 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') | |
737 order_option = nil if order_option.blank? | |
738 | |
739 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq, | |
740 :conditions => statement, | |
741 :order => order_option, | |
742 :joins => joins_for_order_statement(order_option), | |
743 :limit => options[:limit], | |
744 :offset => options[:offset]).find_ids | |
570 rescue ::ActiveRecord::StatementInvalid => e | 745 rescue ::ActiveRecord::StatementInvalid => e |
571 raise StatementInvalid.new(e.message) | 746 raise StatementInvalid.new(e.message) |
572 end | 747 end |
573 | 748 |
574 # Returns the journals | 749 # Returns the journals |
625 sw = operator == "!*" ? 'NOT' : '' | 800 sw = operator == "!*" ? 'NOT' : '' |
626 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' | 801 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' |
627 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" + | 802 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" + |
628 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))" | 803 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))" |
629 when "=", "!" | 804 when "=", "!" |
630 role_cond = value.any? ? | 805 role_cond = value.any? ? |
631 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" : | 806 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" : |
632 "1=0" | 807 "1=0" |
633 | 808 |
634 sw = operator == "!" ? 'NOT' : '' | 809 sw = operator == "!" ? 'NOT' : '' |
635 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' | 810 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' |
636 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" + | 811 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" + |
637 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))" | 812 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))" |
638 end | 813 end |
639 end | 814 end |
640 | 815 |
816 def sql_for_is_private_field(field, operator, value) | |
817 op = (operator == "=" ? 'IN' : 'NOT IN') | |
818 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',') | |
819 | |
820 "#{Issue.table_name}.is_private #{op} (#{va})" | |
821 end | |
822 | |
823 def sql_for_relations(field, operator, value, options={}) | |
824 relation_options = IssueRelation::TYPES[field] | |
825 return relation_options unless relation_options | |
826 | |
827 relation_type = field | |
828 join_column, target_join_column = "issue_from_id", "issue_to_id" | |
829 if relation_options[:reverse] || options[:reverse] | |
830 relation_type = relation_options[:reverse] || relation_type | |
831 join_column, target_join_column = target_join_column, join_column | |
832 end | |
833 | |
834 sql = case operator | |
835 when "*", "!*" | |
836 op = (operator == "*" ? 'IN' : 'NOT IN') | |
837 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')" | |
838 when "=", "!" | |
839 op = (operator == "=" ? 'IN' : 'NOT IN') | |
840 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})" | |
841 when "=p", "=!p", "!p" | |
842 op = (operator == "!p" ? 'NOT IN' : 'IN') | |
843 comp = (operator == "=!p" ? '<>' : '=') | |
844 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})" | |
845 end | |
846 | |
847 if relation_options[:sym] == field && !options[:reverse] | |
848 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)] | |
849 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ") | |
850 else | |
851 sql | |
852 end | |
853 end | |
854 | |
855 IssueRelation::TYPES.keys.each do |relation_type| | |
856 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations | |
857 end | |
858 | |
641 private | 859 private |
642 | 860 |
643 def sql_for_custom_field(field, operator, value, custom_field_id) | 861 def sql_for_custom_field(field, operator, value, custom_field_id) |
644 db_table = CustomValue.table_name | 862 db_table = CustomValue.table_name |
645 db_field = 'value' | 863 db_field = 'value' |
646 "#{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=#{custom_field_id} WHERE " + | 864 filter = @available_filters[field] |
865 return nil unless filter | |
866 if filter[:format] == 'user' | |
867 if value.delete('me') | |
868 value.push User.current.id.to_s | |
869 end | |
870 end | |
871 not_in = nil | |
872 if operator == '!' | |
873 # Makes ! operator work for custom fields with multiple values | |
874 operator = '=' | |
875 not_in = 'NOT' | |
876 end | |
877 customized_key = "id" | |
878 customized_class = Issue | |
879 if field =~ /^(.+)\.cf_/ | |
880 assoc = $1 | |
881 customized_key = "#{assoc}_id" | |
882 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil | |
883 raise "Unknown Issue association #{assoc}" unless customized_class | |
884 end | |
885 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " + | |
647 sql_for_field(field, operator, value, db_table, db_field, true) + ')' | 886 sql_for_field(field, operator, value, db_table, db_field, true) + ')' |
648 end | 887 end |
649 | 888 |
650 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ | 889 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ |
651 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) | 890 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) |
655 if value.any? | 894 if value.any? |
656 case type_for(field) | 895 case type_for(field) |
657 when :date, :date_past | 896 when :date, :date_past |
658 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil)) | 897 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil)) |
659 when :integer | 898 when :integer |
660 sql = "#{db_table}.#{db_field} = #{value.first.to_i}" | 899 if is_custom_filter |
900 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})" | |
901 else | |
902 sql = "#{db_table}.#{db_field} = #{value.first.to_i}" | |
903 end | |
661 when :float | 904 when :float |
662 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" | 905 if is_custom_filter |
906 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})" | |
907 else | |
908 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" | |
909 end | |
663 else | 910 else |
664 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" | 911 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" |
665 end | 912 end |
666 else | 913 else |
667 # IN an empty set | 914 # IN an empty set |
683 when ">=" | 930 when ">=" |
684 if [:date, :date_past].include?(type_for(field)) | 931 if [:date, :date_past].include?(type_for(field)) |
685 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) | 932 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) |
686 else | 933 else |
687 if is_custom_filter | 934 if is_custom_filter |
688 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f}" | 935 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})" |
689 else | 936 else |
690 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" | 937 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" |
691 end | 938 end |
692 end | 939 end |
693 when "<=" | 940 when "<=" |
694 if [:date, :date_past].include?(type_for(field)) | 941 if [:date, :date_past].include?(type_for(field)) |
695 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) | 942 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) |
696 else | 943 else |
697 if is_custom_filter | 944 if is_custom_filter |
698 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f}" | 945 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})" |
699 else | 946 else |
700 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" | 947 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" |
701 end | 948 end |
702 end | 949 end |
703 when "><" | 950 when "><" |
704 if [:date, :date_past].include?(type_for(field)) | 951 if [:date, :date_past].include?(type_for(field)) |
705 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) | 952 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) |
706 else | 953 else |
707 if is_custom_filter | 954 if is_custom_filter |
708 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f}" | 955 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})" |
709 else | 956 else |
710 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" | 957 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" |
711 end | 958 end |
712 end | 959 end |
713 when "o" | 960 when "o" |
714 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" | 961 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id" |
715 when "c" | 962 when "c" |
716 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" | 963 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id" |
964 when "><t-" | |
965 # between today - n days and today | |
966 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0) | |
717 when ">t-" | 967 when ">t-" |
718 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0) | 968 # >= today - n days |
969 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil) | |
719 when "<t-" | 970 when "<t-" |
971 # <= today - n days | |
720 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i) | 972 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i) |
721 when "t-" | 973 when "t-" |
974 # = n days in past | |
722 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i) | 975 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i) |
976 when "><t+" | |
977 # between today and today + n days | |
978 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i) | |
723 when ">t+" | 979 when ">t+" |
980 # >= today + n days | |
724 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) | 981 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) |
725 when "<t+" | 982 when "<t+" |
726 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i) | 983 # <= today + n days |
984 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i) | |
727 when "t+" | 985 when "t+" |
986 # = today + n days | |
728 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i) | 987 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i) |
729 when "t" | 988 when "t" |
989 # = today | |
730 sql = relative_date_clause(db_table, db_field, 0, 0) | 990 sql = relative_date_clause(db_table, db_field, 0, 0) |
731 when "w" | 991 when "w" |
992 # = this week | |
732 first_day_of_week = l(:general_first_day_of_week).to_i | 993 first_day_of_week = l(:general_first_day_of_week).to_i |
733 day_of_week = Date.today.cwday | 994 day_of_week = Date.today.cwday |
734 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) | 995 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) |
735 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6) | 996 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6) |
736 when "~" | 997 when "~" |
742 end | 1003 end |
743 | 1004 |
744 return sql | 1005 return sql |
745 end | 1006 end |
746 | 1007 |
747 def add_custom_fields_filters(custom_fields) | 1008 def add_custom_fields_filters(custom_fields, assoc=nil) |
1009 return unless custom_fields.present? | |
748 @available_filters ||= {} | 1010 @available_filters ||= {} |
749 | 1011 |
750 custom_fields.select(&:is_filter?).each do |field| | 1012 custom_fields.select(&:is_filter?).each do |field| |
751 case field.field_format | 1013 case field.field_format |
752 when "text" | 1014 when "text" |
761 options = { :type => :integer, :order => 20 } | 1023 options = { :type => :integer, :order => 20 } |
762 when "float" | 1024 when "float" |
763 options = { :type => :float, :order => 20 } | 1025 options = { :type => :float, :order => 20 } |
764 when "user", "version" | 1026 when "user", "version" |
765 next unless project | 1027 next unless project |
766 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20} | 1028 values = field.possible_values_options(project) |
1029 if User.current.logged? && field.field_format == 'user' | |
1030 values.unshift ["<< #{l(:label_me)} >>", "me"] | |
1031 end | |
1032 options = { :type => :list_optional, :values => values, :order => 20} | |
767 else | 1033 else |
768 options = { :type => :string, :order => 20 } | 1034 options = { :type => :string, :order => 20 } |
769 end | 1035 end |
770 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) | 1036 filter_id = "cf_#{field.id}" |
1037 filter_name = field.name | |
1038 if assoc.present? | |
1039 filter_id = "#{assoc}.#{filter_id}" | |
1040 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) | |
1041 end | |
1042 @available_filters[filter_id] = options.merge({ | |
1043 :name => filter_name, | |
1044 :format => field.field_format, | |
1045 :field => field | |
1046 }) | |
1047 end | |
1048 end | |
1049 | |
1050 def add_associations_custom_fields_filters(*associations) | |
1051 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class) | |
1052 associations.each do |assoc| | |
1053 association_klass = Issue.reflect_on_association(assoc).klass | |
1054 fields_by_class.each do |field_class, fields| | |
1055 if field_class.customized_class <= association_klass | |
1056 add_custom_fields_filters(fields, assoc) | |
1057 end | |
1058 end | |
771 end | 1059 end |
772 end | 1060 end |
773 | 1061 |
774 # Returns a SQL clause for a date or datetime field. | 1062 # Returns a SQL clause for a date or datetime field. |
775 def date_clause(table, field, from, to) | 1063 def date_clause(table, field, from, to) |
776 s = [] | 1064 s = [] |
777 if from | 1065 if from |
778 from_yesterday = from - 1 | 1066 from_yesterday = from - 1 |
779 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day) | 1067 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day) |
780 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)]) | 1068 if self.class.default_timezone == :utc |
1069 from_yesterday_time = from_yesterday_time.utc | |
1070 end | |
1071 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)]) | |
781 end | 1072 end |
782 if to | 1073 if to |
783 to_utc = Time.gm(to.year, to.month, to.day) | 1074 to_time = Time.local(to.year, to.month, to.day) |
784 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)]) | 1075 if self.class.default_timezone == :utc |
1076 to_time = to_time.utc | |
1077 end | |
1078 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)]) | |
785 end | 1079 end |
786 s.join(' AND ') | 1080 s.join(' AND ') |
787 end | 1081 end |
788 | 1082 |
789 # Returns a SQL clause for a date or datetime field using relative dates. | 1083 # Returns a SQL clause for a date or datetime field using relative dates. |
790 def relative_date_clause(table, field, days_from, days_to) | 1084 def relative_date_clause(table, field, days_from, days_to) |
791 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) | 1085 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) |
792 end | 1086 end |
1087 | |
1088 # Additional joins required for the given sort options | |
1089 def joins_for_order_statement(order_options) | |
1090 joins = [] | |
1091 | |
1092 if order_options | |
1093 if order_options.include?('authors') | |
1094 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id" | |
1095 end | |
1096 order_options.scan(/cf_\d+/).uniq.each do |name| | |
1097 column = available_columns.detect {|c| c.name.to_s == name} | |
1098 join = column && column.custom_field.join_for_order_statement | |
1099 if join | |
1100 joins << join | |
1101 end | |
1102 end | |
1103 end | |
1104 | |
1105 joins.any? ? joins.join(' ') : nil | |
1106 end | |
793 end | 1107 end |