diff app/models/query.rb @ 441:cbce1fd3b1b7 redmine-1.2

Update to Redmine 1.2-stable branch (Redmine SVN rev 6000)
author Chris Cannam
date Mon, 06 Jun 2011 14:24:13 +0100
parents 051f544170fe
children 0c939c159af4
line wrap: on
line diff
--- a/app/models/query.rb	Thu Mar 03 11:42:28 2011 +0000
+++ b/app/models/query.rb	Mon Jun 06 14:24:13 2011 +0100
@@ -1,24 +1,24 @@
 # Redmine - project management software
-# Copyright (C) 2006-2008  Jean-Philippe Lang
+# Copyright (C) 2006-2011  Jean-Philippe Lang
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
 # as published by the Free Software Foundation; either version 2
 # of the License, or (at your option) any later version.
-# 
+#
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
-# 
+#
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
-class QueryColumn  
+class QueryColumn
   attr_accessor :name, :sortable, :groupable, :default_order
   include Redmine::I18n
-  
+
   def initialize(name, options={})
     self.name = name
     self.sortable = options[:sortable]
@@ -29,19 +29,23 @@
     self.default_order = options[:default_order]
     @caption_key = options[:caption] || "field_#{name}"
   end
-  
+
   def caption
     l(@caption_key)
   end
-  
+
   # Returns true if the column is sortable, otherwise false
   def sortable?
     !sortable.nil?
   end
-  
+
   def value(issue)
     issue.send name
   end
+
+  def css_classes
+    name
+  end
 end
 
 class QueryCustomFieldColumn < QueryColumn
@@ -55,37 +59,41 @@
     self.groupable ||= false
     @cf = custom_field
   end
-  
+
   def caption
     @cf.name
   end
-  
+
   def custom_field
     @cf
   end
-  
+
   def value(issue)
     cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
     cv && @cf.cast_value(cv.value)
   end
+
+  def css_classes
+    @css_classes ||= "#{name} #{@cf.field_format}"
+  end
 end
 
 class Query < ActiveRecord::Base
   class StatementInvalid < ::ActiveRecord::StatementInvalid
   end
-  
+
   belongs_to :project
   belongs_to :user
   serialize :filters
   serialize :column_names
   serialize :sort_criteria, Array
-  
+
   attr_protected :project_id, :user_id
-  
+
   validates_presence_of :name, :on => :save
   validates_length_of :name, :maximum => 255
-    
-  @@operators = { "="   => :label_equals, 
+
+  @@operators = { "="   => :label_equals,
                   "!"   => :label_not_equals,
                   "o"   => :label_open_issues,
                   "c"   => :label_closed_issues,
@@ -105,7 +113,7 @@
                   "!~"  => :label_not_contains }
 
   cattr_reader :operators
-    
+
   @@operators_by_filter_type = { :list => [ "=", "!" ],
                                  :list_status => [ "o", "=", "!", "c", "*" ],
                                  :list_optional => [ "=", "!", "!*", "*" ],
@@ -137,27 +145,27 @@
     QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
   ]
   cattr_reader :available_columns
-  
+
   def initialize(attributes = nil)
     super attributes
     self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
   end
-  
+
   def after_initialize
     # Store the fact that project is nil (used in #editable_by?)
     @is_for_all = project.nil?
   end
-  
+
   def validate
     filters.each_key do |field|
-      errors.add label_for(field), :blank unless 
+      errors.add label_for(field), :blank unless
           # filter requires one or more values
-          (values_for(field) and !values_for(field).first.blank?) or 
+          (values_for(field) and !values_for(field).first.blank?) or
           # filter doesn't require any value
           ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
     end if filters
   end
-  
+
   def editable_by?(user)
     return false unless user
     # Admin can edit them all and regular users can edit their private queries
@@ -165,23 +173,23 @@
     # Members can not edit public queries that are for all project (only admin is allowed to)
     is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
   end
-  
+
   def available_filters
     return @available_filters if @available_filters
-    
+
     trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
-    
-    @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },       
-                           "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },                                                                                                                
+
+    @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
+                           "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
                            "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
-                           "subject" => { :type => :text, :order => 8 },  
-                           "created_on" => { :type => :date_past, :order => 9 },                        
+                           "subject" => { :type => :text, :order => 8 },
+                           "created_on" => { :type => :date_past, :order => 9 },
                            "updated_on" => { :type => :date_past, :order => 10 },
                            "start_date" => { :type => :date, :order => 11 },
                            "due_date" => { :type => :date, :order => 12 },
                            "estimated_hours" => { :type => :integer, :order => 13 },
                            "done_ratio" =>  { :type => :integer, :order => 14 }}
-    
+
     user_values = []
     user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
     if project
@@ -191,7 +199,7 @@
       if all_projects.any?
         # members of visible projects
         user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
-          
+
         # project filter
         project_values = []
         Project.project_tree(all_projects) do |p, level|
@@ -209,21 +217,26 @@
 
     role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
     @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
-    
+
     if User.current.logged?
       @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
     end
-  
+
     if project
       # project specific filters
-      unless @project.issue_categories.empty?
-        @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
+      categories = @project.issue_categories.all
+      unless categories.empty?
+        @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
       end
-      unless @project.shared_versions.empty?
-        @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] } }
+      versions = @project.shared_versions.all
+      unless versions.empty?
+        @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
       end
-      unless @project.descendants.active.empty?
-        @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
+      unless @project.leaf?
+        subprojects = @project.descendants.visible.all
+        unless subprojects.empty?
+          @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
+        end
       end
       add_custom_fields_filters(@project.all_issue_custom_fields)
     else
@@ -236,7 +249,7 @@
     end
     @available_filters
   end
-  
+
   def add_filter(field, operator, values)
     # values must be an array
     return unless values and values.is_a? Array # and !values.first.empty?
@@ -251,7 +264,7 @@
       filters[field] = {:operator => operator, :values => values }
     end
   end
-  
+
   def add_short_filter(field, expression)
     return unless expression
     parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
@@ -266,19 +279,19 @@
       end
     end
   end
-  
+
   def has_filter?(field)
     filters and filters[field]
   end
-  
+
   def operator_for(field)
     has_filter?(field) ? filters[field][:operator] : nil
   end
-  
+
   def values_for(field)
     has_filter?(field) ? filters[field][:values] : nil
   end
-  
+
   def label_for(field)
     label = available_filters[field][:name] if available_filters.has_key?(field)
     label ||= field.gsub(/\_id$/, "")
@@ -290,17 +303,17 @@
     @available_columns += (project ? 
                             project.all_issue_custom_fields :
                             IssueCustomField.find(:all)
-                           ).collect {|cf| QueryCustomFieldColumn.new(cf) }      
+                           ).collect {|cf| QueryCustomFieldColumn.new(cf) }
   end
 
   def self.available_columns=(v)
     self.available_columns = (v)
   end
-  
+
   def self.add_available_column(column)
     self.available_columns << (column) if column.is_a?(QueryColumn)
   end
-  
+
   # Returns an array of columns that can be used to group the results
   def groupable_columns
     available_columns.select {|c| c.groupable}
@@ -313,7 +326,7 @@
                                                h
                                              })
   end
-  
+
   def columns
     if has_default_columns?
       available_columns.select do |c|
@@ -325,7 +338,7 @@
       column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
     end
   end
-  
+
   def column_names=(names)
     if names
       names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
@@ -337,15 +350,15 @@
     end
     write_attribute(:column_names, names)
   end
-  
+
   def has_column?(column)
     column_names && column_names.include?(column.name)
   end
-  
+
   def has_default_columns?
     column_names.nil? || column_names.empty?
   end
-  
+
   def sort_criteria=(arg)
     c = []
     if arg.is_a?(Hash)
@@ -354,19 +367,19 @@
     c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
     write_attribute(:sort_criteria, c)
   end
-  
+
   def sort_criteria
     read_attribute(:sort_criteria) || []
   end
-  
+
   def sort_criteria_key(arg)
     sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
   end
-  
+
   def sort_criteria_order(arg)
     sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
   end
-  
+
   # Returns the SQL sort order that should be prepended for grouping
   def group_by_sort_order
     if grouped? && (column = group_by_column)
@@ -375,20 +388,20 @@
         "#{column.sortable} #{column.default_order}"
     end
   end
-  
+
   # Returns true if the query is a grouped query
   def grouped?
     !group_by_column.nil?
   end
-  
+
   def group_by_column
     groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
   end
-  
+
   def group_by_statement
     group_by_column.try(:groupable)
   end
-  
+
   def project_statement
     project_clauses = []
     if project && !@project.descendants.active.empty?
@@ -411,8 +424,7 @@
     elsif project
       project_clauses << "#{Project.table_name}.id = %d" % project.id
     end
-    project_clauses <<  Project.allowed_to_condition(User.current, :view_issues)
-    project_clauses.join(' AND ')
+    project_clauses.any? ? project_clauses.join(' AND ') : nil
   end
 
   def statement
@@ -423,12 +435,12 @@
       v = values_for(field).clone
       next unless v and !v.empty?
       operator = operator_for(field)
-      
+
       # "me" value subsitution
       if %w(assigned_to_id author_id watcher_id).include?(field)
         v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
       end
-      
+
       sql = ''
       if field =~ /^cf_(\d+)$/
         # custom field
@@ -460,7 +472,7 @@
           end
           user_ids.flatten.uniq.compact
         }.sort.collect(&:to_s)
-        
+
         sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
 
       elsif field == "assigned_to_role" # named field
@@ -474,14 +486,14 @@
           roles = Role.givable.find_all_by_id(v)
         end
         roles ||= []
-        
+
         members_of_roles = roles.inject([]) {|user_ids, role|
           if role && role.members
             user_ids << role.members.collect(&:user_id)
           end
           user_ids.flatten.uniq.compact
         }.sort.collect(&:to_s)
-        
+
         sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
       else
         # regular field
@@ -490,26 +502,29 @@
         sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
       end
       filters_clauses << sql
-      
+
     end if filters and valid?
-    
-    (filters_clauses << project_statement).join(' AND ')
+
+    filters_clauses << project_statement
+    filters_clauses.reject!(&:blank?)
+
+    filters_clauses.any? ? filters_clauses.join(' AND ') : nil
   end
-  
+
   # Returns the issue count
   def issue_count
     Issue.count(:include => [:status, :project], :conditions => statement)
   rescue ::ActiveRecord::StatementInvalid => e
     raise StatementInvalid.new(e.message)
   end
-  
+
   # Returns the issue count by group or nil if query is not grouped
   def issue_count_by_group
     r = nil
     if grouped?
       begin
         # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
-        r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
+        r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
       rescue ActiveRecord::RecordNotFound
         r = {nil => issue_count}
       end
@@ -522,14 +537,14 @@
   rescue ::ActiveRecord::StatementInvalid => e
     raise StatementInvalid.new(e.message)
   end
-  
+
   # Returns the issues
   # Valid options are :order, :offset, :limit, :include, :conditions
   def issues(options={})
     order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
     order_option = nil if order_option.blank?
-    
-    Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
+
+    Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
                      :conditions => Query.merge_conditions(statement, options[:conditions]),
                      :order => order_option,
                      :limit  => options[:limit],
@@ -541,7 +556,7 @@
   # Returns the journals
   # Valid options are :order, :offset, :limit
   def journals(options={})
-    Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
+    Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
                        :conditions => statement,
                        :order => options[:order],
                        :limit => options[:limit],
@@ -549,18 +564,18 @@
   rescue ::ActiveRecord::StatementInvalid => e
     raise StatementInvalid.new(e.message)
   end
-  
+
   # Returns the versions
   # Valid options are :conditions
   def versions(options={})
-    Version.find :all, :include => :project,
+    Version.visible.find :all, :include => :project,
                        :conditions => Query.merge_conditions(project_statement, options[:conditions])
   rescue ::ActiveRecord::StatementInvalid => e
     raise StatementInvalid.new(e.message)
   end
-  
+
   private
-  
+
   # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
   def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
     sql = ''
@@ -608,24 +623,22 @@
     when "t"
       sql = date_range_clause(db_table, db_field, 0, 0)
     when "w"
-      from = l(:general_first_day_of_week) == '7' ?
-      # week starts on sunday
-      ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
-        # week starts on monday (Rails default)
-        Time.now.at_beginning_of_week
-      sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
+      first_day_of_week = l(:general_first_day_of_week).to_i
+      day_of_week = Date.today.cwday
+      days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
+      sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
     when "~"
       sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
     when "!~"
       sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
     end
-    
+
     return sql
   end
-  
+
   def add_custom_fields_filters(custom_fields)
     @available_filters ||= {}
-    
+
     custom_fields.select(&:is_filter?).each do |field|
       case field.field_format
       when "text"
@@ -636,13 +649,16 @@
         options = { :type => :date, :order => 20 }
       when "bool"
         options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
+      when "user", "version"
+        next unless project
+        options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
       else
         options = { :type => :string, :order => 20 }
       end
       @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
     end
   end
-  
+
   # Returns a SQL clause for a date or datetime field.
   def date_range_clause(table, field, from, to)
     s = []