annotate app/models/.svn/text-base/query.rb.svn-base @ 45:65d9e2cabaa3 luisf

Added tipoftheday to the config/settings in order to correct previous issues. Tip of the day is now working correctly. Added the heading strings to the locales files.
author luisf
date Tue, 23 Nov 2010 11:50:01 +0000
parents 94944d00e43c
children af80e5618e9b
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@14 190 project_ids = Project.all(:conditions => Project.visible_by(User.current)).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@22 198
chris@37 199 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
chris@22 200 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
chris@22 201
chris@37 202 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
chris@22 203 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
Chris@0 204
Chris@0 205 if User.current.logged?
Chris@0 206 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
Chris@0 207 end
Chris@0 208
Chris@0 209 if project
Chris@0 210 # project specific filters
Chris@0 211 unless @project.issue_categories.empty?
Chris@0 212 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
Chris@0 213 end
Chris@0 214 unless @project.shared_versions.empty?
Chris@0 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] } }
Chris@0 216 end
Chris@0 217 unless @project.descendants.active.empty?
Chris@0 218 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
Chris@0 219 end
Chris@0 220 add_custom_fields_filters(@project.all_issue_custom_fields)
Chris@0 221 else
Chris@0 222 # global filters for cross project issue list
Chris@0 223 system_shared_versions = Version.visible.find_all_by_sharing('system')
Chris@0 224 unless system_shared_versions.empty?
Chris@0 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] } }
Chris@0 226 end
Chris@0 227 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
Chris@14 228 # project filter
Chris@14 229 project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p|
Chris@14 230 pre = (p.level > 0 ? ('--' * p.level + ' ') : '')
Chris@14 231 ["#{pre}#{p.name}",p.id.to_s]
Chris@14 232 end
Chris@14 233 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values}
Chris@0 234 end
Chris@0 235 @available_filters
Chris@0 236 end
Chris@0 237
Chris@0 238 def add_filter(field, operator, values)
Chris@0 239 # values must be an array
Chris@0 240 return unless values and values.is_a? Array # and !values.first.empty?
Chris@0 241 # check if field is defined as an available filter
Chris@0 242 if available_filters.has_key? field
Chris@0 243 filter_options = available_filters[field]
Chris@0 244 # check if operator is allowed for that filter
Chris@0 245 #if @@operators_by_filter_type[filter_options[:type]].include? operator
Chris@0 246 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
Chris@0 247 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
Chris@0 248 #end
Chris@0 249 filters[field] = {:operator => operator, :values => values }
Chris@0 250 end
Chris@0 251 end
Chris@0 252
Chris@0 253 def add_short_filter(field, expression)
Chris@0 254 return unless expression
Chris@0 255 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
Chris@0 256 add_filter field, (parms[0] || "="), [parms[1] || ""]
Chris@0 257 end
Chris@0 258
Chris@0 259 # Add multiple filters using +add_filter+
Chris@0 260 def add_filters(fields, operators, values)
chris@37 261 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
chris@37 262 fields.each do |field|
chris@37 263 add_filter(field, operators[field], values[field])
chris@37 264 end
Chris@0 265 end
Chris@0 266 end
Chris@0 267
Chris@0 268 def has_filter?(field)
Chris@0 269 filters and filters[field]
Chris@0 270 end
Chris@0 271
Chris@0 272 def operator_for(field)
Chris@0 273 has_filter?(field) ? filters[field][:operator] : nil
Chris@0 274 end
Chris@0 275
Chris@0 276 def values_for(field)
Chris@0 277 has_filter?(field) ? filters[field][:values] : nil
Chris@0 278 end
Chris@0 279
Chris@0 280 def label_for(field)
Chris@0 281 label = available_filters[field][:name] if available_filters.has_key?(field)
Chris@0 282 label ||= field.gsub(/\_id$/, "")
Chris@0 283 end
Chris@0 284
Chris@0 285 def available_columns
Chris@0 286 return @available_columns if @available_columns
Chris@0 287 @available_columns = Query.available_columns
Chris@0 288 @available_columns += (project ?
Chris@0 289 project.all_issue_custom_fields :
Chris@0 290 IssueCustomField.find(:all)
Chris@0 291 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
Chris@0 292 end
Chris@0 293
Chris@0 294 def self.available_columns=(v)
Chris@0 295 self.available_columns = (v)
Chris@0 296 end
Chris@0 297
Chris@0 298 def self.add_available_column(column)
Chris@0 299 self.available_columns << (column) if column.is_a?(QueryColumn)
Chris@0 300 end
Chris@0 301
Chris@0 302 # Returns an array of columns that can be used to group the results
Chris@0 303 def groupable_columns
Chris@0 304 available_columns.select {|c| c.groupable}
Chris@0 305 end
Chris@0 306
Chris@0 307 # Returns a Hash of columns and the key for sorting
Chris@0 308 def sortable_columns
Chris@0 309 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
Chris@0 310 h[column.name.to_s] = column.sortable
Chris@0 311 h
Chris@0 312 })
Chris@0 313 end
Chris@0 314
Chris@0 315 def columns
Chris@0 316 if has_default_columns?
Chris@0 317 available_columns.select do |c|
Chris@0 318 # Adds the project column by default for cross-project lists
Chris@0 319 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
Chris@0 320 end
Chris@0 321 else
Chris@0 322 # preserve the column_names order
Chris@0 323 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
Chris@0 324 end
Chris@0 325 end
Chris@0 326
Chris@0 327 def column_names=(names)
Chris@0 328 if names
Chris@0 329 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
Chris@0 330 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
Chris@0 331 # Set column_names to nil if default columns
Chris@0 332 if names.map(&:to_s) == Setting.issue_list_default_columns
Chris@0 333 names = nil
Chris@0 334 end
Chris@0 335 end
Chris@0 336 write_attribute(:column_names, names)
Chris@0 337 end
Chris@0 338
Chris@0 339 def has_column?(column)
Chris@0 340 column_names && column_names.include?(column.name)
Chris@0 341 end
Chris@0 342
Chris@0 343 def has_default_columns?
Chris@0 344 column_names.nil? || column_names.empty?
Chris@0 345 end
Chris@0 346
Chris@0 347 def sort_criteria=(arg)
Chris@0 348 c = []
Chris@0 349 if arg.is_a?(Hash)
Chris@0 350 arg = arg.keys.sort.collect {|k| arg[k]}
Chris@0 351 end
Chris@0 352 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
Chris@0 353 write_attribute(:sort_criteria, c)
Chris@0 354 end
Chris@0 355
Chris@0 356 def sort_criteria
Chris@0 357 read_attribute(:sort_criteria) || []
Chris@0 358 end
Chris@0 359
Chris@0 360 def sort_criteria_key(arg)
Chris@0 361 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
Chris@0 362 end
Chris@0 363
Chris@0 364 def sort_criteria_order(arg)
Chris@0 365 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
Chris@0 366 end
Chris@0 367
Chris@0 368 # Returns the SQL sort order that should be prepended for grouping
Chris@0 369 def group_by_sort_order
Chris@0 370 if grouped? && (column = group_by_column)
Chris@0 371 column.sortable.is_a?(Array) ?
Chris@0 372 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
Chris@0 373 "#{column.sortable} #{column.default_order}"
Chris@0 374 end
Chris@0 375 end
Chris@0 376
Chris@0 377 # Returns true if the query is a grouped query
Chris@0 378 def grouped?
Chris@0 379 !group_by.blank?
Chris@0 380 end
Chris@0 381
Chris@0 382 def group_by_column
Chris@0 383 groupable_columns.detect {|c| c.name.to_s == group_by}
Chris@0 384 end
Chris@0 385
Chris@0 386 def group_by_statement
Chris@0 387 group_by_column.groupable
Chris@0 388 end
Chris@0 389
Chris@0 390 def project_statement
Chris@0 391 project_clauses = []
Chris@0 392 if project && !@project.descendants.active.empty?
Chris@0 393 ids = [project.id]
Chris@0 394 if has_filter?("subproject_id")
Chris@0 395 case operator_for("subproject_id")
Chris@0 396 when '='
Chris@0 397 # include the selected subprojects
Chris@0 398 ids += values_for("subproject_id").each(&:to_i)
Chris@0 399 when '!*'
Chris@0 400 # main project only
Chris@0 401 else
Chris@0 402 # all subprojects
Chris@0 403 ids += project.descendants.collect(&:id)
Chris@0 404 end
Chris@0 405 elsif Setting.display_subprojects_issues?
Chris@0 406 ids += project.descendants.collect(&:id)
Chris@0 407 end
Chris@0 408 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
Chris@0 409 elsif project
Chris@0 410 project_clauses << "#{Project.table_name}.id = %d" % project.id
Chris@0 411 end
Chris@0 412 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
Chris@0 413 project_clauses.join(' AND ')
Chris@0 414 end
Chris@0 415
Chris@0 416 def statement
Chris@0 417 # filters clauses
Chris@0 418 filters_clauses = []
Chris@0 419 filters.each_key do |field|
Chris@0 420 next if field == "subproject_id"
Chris@0 421 v = values_for(field).clone
Chris@0 422 next unless v and !v.empty?
Chris@0 423 operator = operator_for(field)
Chris@0 424
Chris@0 425 # "me" value subsitution
Chris@0 426 if %w(assigned_to_id author_id watcher_id).include?(field)
Chris@0 427 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
Chris@0 428 end
Chris@0 429
Chris@0 430 sql = ''
Chris@0 431 if field =~ /^cf_(\d+)$/
Chris@0 432 # custom field
Chris@0 433 db_table = CustomValue.table_name
Chris@0 434 db_field = 'value'
Chris@0 435 is_custom_filter = true
Chris@0 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 "
Chris@0 437 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
Chris@0 438 elsif field == 'watcher_id'
Chris@0 439 db_table = Watcher.table_name
Chris@0 440 db_field = 'user_id'
Chris@0 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 "
Chris@0 442 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
chris@22 443 elsif field == "member_of_group" # named field
chris@22 444 if operator == '*' # Any group
chris@22 445 groups = Group.all
chris@22 446 operator = '=' # Override the operator since we want to find by assigned_to
chris@22 447 elsif operator == "!*"
chris@22 448 groups = Group.all
chris@22 449 operator = '!' # Override the operator since we want to find by assigned_to
chris@22 450 else
chris@22 451 groups = Group.find_all_by_id(v)
chris@22 452 end
chris@22 453 groups ||= []
chris@22 454
chris@22 455 members_of_groups = groups.inject([]) {|user_ids, group|
chris@22 456 if group && group.user_ids.present?
chris@22 457 user_ids << group.user_ids
chris@22 458 end
chris@22 459 user_ids.flatten.uniq.compact
chris@22 460 }.sort.collect(&:to_s)
chris@22 461
chris@22 462 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
chris@22 463
chris@22 464 elsif field == "assigned_to_role" # named field
chris@22 465 if operator == "*" # Any Role
chris@22 466 roles = Role.givable
chris@22 467 operator = '=' # Override the operator since we want to find by assigned_to
chris@22 468 elsif operator == "!*" # No role
chris@22 469 roles = Role.givable
chris@22 470 operator = '!' # Override the operator since we want to find by assigned_to
chris@22 471 else
chris@22 472 roles = Role.givable.find_all_by_id(v)
chris@22 473 end
chris@22 474 roles ||= []
chris@22 475
chris@22 476 members_of_roles = roles.inject([]) {|user_ids, role|
chris@22 477 if role && role.members
chris@22 478 user_ids << role.members.collect(&:user_id)
chris@22 479 end
chris@22 480 user_ids.flatten.uniq.compact
chris@22 481 }.sort.collect(&:to_s)
chris@22 482
chris@22 483 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
Chris@0 484 else
Chris@0 485 # regular field
Chris@0 486 db_table = Issue.table_name
Chris@0 487 db_field = field
Chris@0 488 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
Chris@0 489 end
Chris@0 490 filters_clauses << sql
Chris@0 491
Chris@0 492 end if filters and valid?
Chris@0 493
Chris@0 494 (filters_clauses << project_statement).join(' AND ')
Chris@0 495 end
Chris@0 496
Chris@0 497 # Returns the issue count
Chris@0 498 def issue_count
Chris@0 499 Issue.count(:include => [:status, :project], :conditions => statement)
Chris@0 500 rescue ::ActiveRecord::StatementInvalid => e
Chris@0 501 raise StatementInvalid.new(e.message)
Chris@0 502 end
Chris@0 503
Chris@0 504 # Returns the issue count by group or nil if query is not grouped
Chris@0 505 def issue_count_by_group
Chris@0 506 r = nil
Chris@0 507 if grouped?
Chris@0 508 begin
Chris@0 509 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
Chris@0 510 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
Chris@0 511 rescue ActiveRecord::RecordNotFound
Chris@0 512 r = {nil => issue_count}
Chris@0 513 end
Chris@0 514 c = group_by_column
Chris@0 515 if c.is_a?(QueryCustomFieldColumn)
Chris@0 516 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
Chris@0 517 end
Chris@0 518 end
Chris@0 519 r
Chris@0 520 rescue ::ActiveRecord::StatementInvalid => e
Chris@0 521 raise StatementInvalid.new(e.message)
Chris@0 522 end
Chris@0 523
Chris@0 524 # Returns the issues
Chris@0 525 # Valid options are :order, :offset, :limit, :include, :conditions
Chris@0 526 def issues(options={})
Chris@0 527 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
Chris@0 528 order_option = nil if order_option.blank?
Chris@0 529
Chris@0 530 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
Chris@0 531 :conditions => Query.merge_conditions(statement, options[:conditions]),
Chris@0 532 :order => order_option,
Chris@0 533 :limit => options[:limit],
Chris@0 534 :offset => options[:offset]
Chris@0 535 rescue ::ActiveRecord::StatementInvalid => e
Chris@0 536 raise StatementInvalid.new(e.message)
Chris@0 537 end
Chris@0 538
Chris@0 539 # Returns the journals
Chris@0 540 # Valid options are :order, :offset, :limit
Chris@0 541 def journals(options={})
Chris@0 542 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
Chris@0 543 :conditions => statement,
Chris@0 544 :order => options[:order],
Chris@0 545 :limit => options[:limit],
Chris@0 546 :offset => options[:offset]
Chris@0 547 rescue ::ActiveRecord::StatementInvalid => e
Chris@0 548 raise StatementInvalid.new(e.message)
Chris@0 549 end
Chris@0 550
Chris@0 551 # Returns the versions
Chris@0 552 # Valid options are :conditions
Chris@0 553 def versions(options={})
Chris@0 554 Version.find :all, :include => :project,
Chris@0 555 :conditions => Query.merge_conditions(project_statement, options[:conditions])
Chris@0 556 rescue ::ActiveRecord::StatementInvalid => e
Chris@0 557 raise StatementInvalid.new(e.message)
Chris@0 558 end
Chris@0 559
Chris@0 560 private
Chris@0 561
Chris@0 562 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
Chris@0 563 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
Chris@0 564 sql = ''
Chris@0 565 case operator
Chris@0 566 when "="
Chris@0 567 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
Chris@0 568 when "!"
Chris@0 569 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
Chris@0 570 when "!*"
Chris@0 571 sql = "#{db_table}.#{db_field} IS NULL"
Chris@0 572 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
Chris@0 573 when "*"
Chris@0 574 sql = "#{db_table}.#{db_field} IS NOT NULL"
Chris@0 575 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
Chris@0 576 when ">="
Chris@0 577 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
Chris@0 578 when "<="
Chris@0 579 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
Chris@0 580 when "o"
Chris@0 581 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
Chris@0 582 when "c"
Chris@0 583 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
Chris@0 584 when ">t-"
Chris@0 585 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
Chris@0 586 when "<t-"
Chris@0 587 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
Chris@0 588 when "t-"
Chris@0 589 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
Chris@0 590 when ">t+"
Chris@0 591 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
Chris@0 592 when "<t+"
Chris@0 593 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
Chris@0 594 when "t+"
Chris@0 595 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
Chris@0 596 when "t"
Chris@0 597 sql = date_range_clause(db_table, db_field, 0, 0)
Chris@0 598 when "w"
Chris@0 599 from = l(:general_first_day_of_week) == '7' ?
Chris@0 600 # week starts on sunday
Chris@0 601 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
Chris@0 602 # week starts on monday (Rails default)
Chris@0 603 Time.now.at_beginning_of_week
Chris@0 604 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
Chris@0 605 when "~"
Chris@0 606 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
Chris@0 607 when "!~"
Chris@0 608 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
Chris@0 609 end
Chris@0 610
Chris@0 611 return sql
Chris@0 612 end
Chris@0 613
Chris@0 614 def add_custom_fields_filters(custom_fields)
Chris@0 615 @available_filters ||= {}
Chris@0 616
Chris@0 617 custom_fields.select(&:is_filter?).each do |field|
Chris@0 618 case field.field_format
Chris@0 619 when "text"
Chris@0 620 options = { :type => :text, :order => 20 }
Chris@0 621 when "list"
Chris@0 622 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
Chris@0 623 when "date"
Chris@0 624 options = { :type => :date, :order => 20 }
Chris@0 625 when "bool"
Chris@0 626 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
Chris@0 627 else
Chris@0 628 options = { :type => :string, :order => 20 }
Chris@0 629 end
Chris@0 630 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
Chris@0 631 end
Chris@0 632 end
Chris@0 633
Chris@0 634 # Returns a SQL clause for a date or datetime field.
Chris@0 635 def date_range_clause(table, field, from, to)
Chris@0 636 s = []
Chris@0 637 if from
Chris@0 638 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
Chris@0 639 end
Chris@0 640 if to
Chris@0 641 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
Chris@0 642 end
Chris@0 643 s.join(' AND ')
Chris@0 644 end
Chris@0 645 end