annotate app/models/query.rb @ 8:0c83d98252d9 yuya

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