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