2008-09-11 17:03:26 +00:00
# Redmine - project management software
2011-04-01 13:44:58 +00:00
# Copyright (C) 2006-2011 Jean-Philippe Lang
2006-12-16 13:37:32 +00:00
#
# 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.
2011-05-17 04:33:19 +00:00
#
2006-12-16 13:37:32 +00:00
# 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.
2011-05-17 04:33:19 +00:00
#
2006-12-16 13:37:32 +00:00
# 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.
2011-05-17 04:33:19 +00:00
class QueryColumn
2009-11-27 19:59:10 +00:00
attr_accessor :name , :sortable , :groupable , :default_order
2009-02-21 11:04:50 +00:00
include Redmine :: I18n
2011-05-17 04:33:19 +00:00
2007-10-01 08:44:17 +00:00
def initialize ( name , options = { } )
self . name = name
self . sortable = options [ :sortable ]
2009-04-26 13:09:14 +00:00
self . groupable = options [ :groupable ] || false
2009-11-16 18:07:30 +00:00
if groupable == true
self . groupable = name . to_s
end
2008-02-05 17:28:02 +00:00
self . default_order = options [ :default_order ]
2010-03-27 16:55:20 +00:00
@caption_key = options [ :caption ] || " field_ #{ name } "
2007-10-01 08:44:17 +00:00
end
2011-05-17 04:33:19 +00:00
2007-11-07 20:42:28 +00:00
def caption
2010-03-27 16:55:20 +00:00
l ( @caption_key )
2007-11-07 20:42:28 +00:00
end
2011-05-17 04:33:19 +00:00
2009-03-12 18:06:54 +00:00
# Returns true if the column is sortable, otherwise false
def sortable?
! sortable . nil?
end
2011-05-17 04:33:19 +00:00
2009-12-02 18:57:17 +00:00
def value ( issue )
issue . send name
end
2011-05-17 04:33:19 +00:00
2011-04-05 22:18:49 +00:00
def css_classes
name
end
2007-11-07 20:42:28 +00:00
end
class QueryCustomFieldColumn < QueryColumn
def initialize ( custom_field )
self . name = " cf_ #{ custom_field . id } " . to_sym
2009-01-11 16:33:51 +00:00
self . sortable = custom_field . order_statement || false
2009-11-16 18:07:30 +00:00
if %w( list date bool int ) . include? ( custom_field . field_format )
self . groupable = custom_field . order_statement
end
self . groupable || = false
2007-11-07 20:42:28 +00:00
@cf = custom_field
end
2011-05-17 04:33:19 +00:00
2007-11-07 20:42:28 +00:00
def caption
@cf . name
end
2011-05-17 04:33:19 +00:00
2007-11-07 20:42:28 +00:00
def custom_field
@cf
end
2011-05-17 04:33:19 +00:00
2009-12-02 18:57:17 +00:00
def value ( issue )
cv = issue . custom_values . detect { | v | v . custom_field_id == @cf . id }
cv && @cf . cast_value ( cv . value )
end
2011-05-17 04:33:19 +00:00
2011-04-05 22:18:49 +00:00
def css_classes
@css_classes || = " #{ name } #{ @cf . field_format } "
end
2007-10-01 08:44:17 +00:00
end
2006-12-16 13:37:32 +00:00
class Query < ActiveRecord :: Base
2009-11-28 10:29:48 +00:00
class StatementInvalid < :: ActiveRecord :: StatementInvalid
end
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
belongs_to :project
belongs_to :user
serialize :filters
2007-10-01 08:44:17 +00:00
serialize :column_names
2009-03-12 18:06:54 +00:00
serialize :sort_criteria , Array
2011-05-17 04:33:19 +00:00
2007-12-31 12:37:32 +00:00
attr_protected :project_id , :user_id
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
validates_presence_of :name , :on = > :save
2007-07-16 17:16:49 +00:00
validates_length_of :name , :maximum = > 255
2011-09-20 16:56:57 +00:00
validate :validate_query_filters
2011-05-17 04:33:19 +00:00
@@operators = { " = " = > :label_equals ,
2006-12-16 13:37:32 +00:00
" ! " = > :label_not_equals ,
" o " = > :label_open_issues ,
" c " = > :label_closed_issues ,
" !* " = > :label_none ,
" * " = > :label_all ,
2009-03-22 20:33:21 +00:00
" >= " = > :label_greater_or_equal ,
" <= " = > :label_less_or_equal ,
2011-07-10 08:00:25 +00:00
" >< " = > :label_between ,
2006-12-16 13:37:32 +00:00
" <t+ " = > :label_in_less_than ,
" >t+ " = > :label_in_more_than ,
" t+ " = > :label_in ,
" t " = > :label_today ,
2007-09-05 17:47:17 +00:00
" w " = > :label_this_week ,
2006-12-16 13:37:32 +00:00
" >t- " = > :label_less_than_ago ,
" <t- " = > :label_more_than_ago ,
" t- " = > :label_ago ,
" ~ " = > :label_contains ,
" !~ " = > :label_not_contains }
cattr_reader :operators
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
@@operators_by_filter_type = { :list = > [ " = " , " ! " ] ,
:list_status = > [ " o " , " = " , " ! " , " c " , " * " ] ,
:list_optional = > [ " = " , " ! " , " !* " , " * " ] ,
2008-02-28 21:57:03 +00:00
:list_subprojects = > [ " * " , " !* " , " = " ] ,
2011-07-10 17:29:29 +00:00
:date = > [ " = " , " >= " , " <= " , " >< " , " <t+ " , " >t+ " , " t+ " , " t " , " w " , " >t- " , " <t- " , " t- " ] ,
:date_past = > [ " = " , " >= " , " <= " , " >< " , " >t- " , " <t- " , " t- " , " t " , " w " ] ,
2007-04-17 10:53:20 +00:00
:string = > [ " = " , " ~ " , " ! " , " !~ " ] ,
2007-09-09 19:46:28 +00:00
:text = > [ " ~ " , " !~ " ] ,
2011-07-11 11:35:53 +00:00
:integer = > [ " = " , " >= " , " <= " , " >< " , " !* " , " * " ] ,
:float = > [ " = " , " >= " , " <= " , " >< " , " !* " , " * " ] }
2006-12-16 13:37:32 +00:00
cattr_reader :operators_by_filter_type
2007-10-01 08:44:17 +00:00
@@available_columns = [
2009-04-26 13:09:14 +00:00
QueryColumn . new ( :project , :sortable = > " #{ Project . table_name } .name " , :groupable = > true ) ,
2009-11-27 19:59:10 +00:00
QueryColumn . new ( :tracker , :sortable = > " #{ Tracker . table_name } .position " , :groupable = > true ) ,
2010-03-27 16:55:20 +00:00
QueryColumn . new ( :parent , :sortable = > [ " #{ Issue . table_name } .root_id " , " #{ Issue . table_name } .lft ASC " ] , :default_order = > 'desc' , :caption = > :field_parent_issue ) ,
2009-04-26 13:09:14 +00:00
QueryColumn . new ( :status , :sortable = > " #{ IssueStatus . table_name } .position " , :groupable = > true ) ,
2009-05-30 23:30:36 +00:00
QueryColumn . new ( :priority , :sortable = > " #{ IssuePriority . table_name } .position " , :default_order = > 'desc' , :groupable = > true ) ,
2008-02-05 17:28:02 +00:00
QueryColumn . new ( :subject , :sortable = > " #{ Issue . table_name } .subject " ) ,
2008-02-03 14:02:23 +00:00
QueryColumn . new ( :author ) ,
2009-11-27 19:59:10 +00:00
QueryColumn . new ( :assigned_to , :sortable = > [ " #{ User . table_name } .lastname " , " #{ User . table_name } .firstname " , " #{ User . table_name } .id " ] , :groupable = > true ) ,
2008-02-05 17:28:02 +00:00
QueryColumn . new ( :updated_on , :sortable = > " #{ Issue . table_name } .updated_on " , :default_order = > 'desc' ) ,
2009-11-27 19:59:10 +00:00
QueryColumn . new ( :category , :sortable = > " #{ IssueCategory . table_name } .name " , :groupable = > true ) ,
QueryColumn . new ( :fixed_version , :sortable = > [ " #{ Version . table_name } .effective_date " , " #{ Version . table_name } .name " ] , :default_order = > 'desc' , :groupable = > true ) ,
2007-10-01 08:44:17 +00:00
QueryColumn . new ( :start_date , :sortable = > " #{ Issue . table_name } .start_date " ) ,
QueryColumn . new ( :due_date , :sortable = > " #{ Issue . table_name } .due_date " ) ,
QueryColumn . new ( :estimated_hours , :sortable = > " #{ Issue . table_name } .estimated_hours " ) ,
2009-04-26 13:09:14 +00:00
QueryColumn . new ( :done_ratio , :sortable = > " #{ Issue . table_name } .done_ratio " , :groupable = > true ) ,
2008-02-05 17:28:02 +00:00
QueryColumn . new ( :created_on , :sortable = > " #{ Issue . table_name } .created_on " , :default_order = > 'desc' ) ,
2007-10-01 08:44:17 +00:00
]
cattr_reader :available_columns
2011-05-17 04:33:19 +00:00
2011-07-06 16:57:04 +00:00
named_scope :visible , lambda { | * args |
user = args . shift || User . current
base = Project . allowed_to_condition ( user , :view_issues , * args )
user_id = user . logged? ? user . id : 0
{
:conditions = > [ " ( #{ table_name } .project_id IS NULL OR ( #{ base } )) AND ( #{ table_name } .is_public = ? OR #{ table_name } .user_id = ?) " , true , user_id ] ,
:include = > :project
}
}
2011-08-20 03:38:29 +00:00
2006-12-16 13:37:32 +00:00
def initialize ( attributes = nil )
super attributes
self . filters || = { 'status_id' = > { :operator = > " o " , :values = > [ " " ] } }
2007-05-08 12:46:15 +00:00
end
2011-05-17 04:33:19 +00:00
2008-03-30 12:29:07 +00:00
def after_initialize
# Store the fact that project is nil (used in #editable_by?)
@is_for_all = project . nil?
end
2011-05-17 04:33:19 +00:00
2011-09-20 16:56:57 +00:00
def validate_query_filters
2006-12-16 13:37:32 +00:00
filters . each_key do | field |
2011-07-11 11:35:53 +00:00
if values_for ( field )
case type_for ( field )
2011-08-20 03:38:29 +00:00
when :integer
2011-07-11 11:35:53 +00:00
errors . add ( label_for ( field ) , :invalid ) if values_for ( field ) . detect { | v | v . present? && ! v . match ( / ^ \ d+$ / ) }
2011-08-20 03:38:29 +00:00
when :float
2011-07-11 11:35:53 +00:00
errors . add ( label_for ( field ) , :invalid ) if values_for ( field ) . detect { | v | v . present? && ! v . match ( / ^ \ d+( \ . \ d*)?$ / ) }
2011-07-11 14:13:59 +00:00
when :date , :date_past
case operator_for ( field )
when " = " , " >= " , " <= " , " >< "
2011-07-11 14:36:43 +00:00
errors . add ( label_for ( field ) , :invalid ) if values_for ( field ) . detect { | v | v . present? && ( ! v . match ( / ^ \ d{4}- \ d{2}- \ d{2}$ / ) || ( Date . parse ( v ) rescue nil ) . nil? ) }
2011-07-11 14:13:59 +00:00
when " >t- " , " <t- " , " t- "
errors . add ( label_for ( field ) , :invalid ) if values_for ( field ) . detect { | v | v . present? && ! v . match ( / ^ \ d+$ / ) }
end
2011-07-11 11:35:53 +00:00
end
2011-07-10 18:34:49 +00:00
end
2011-08-20 03:38:29 +00:00
2011-05-17 04:33:19 +00:00
errors . add label_for ( field ) , :blank unless
2006-12-16 13:37:32 +00:00
# filter requires one or more values
2011-05-17 04:33:19 +00:00
( values_for ( field ) and ! values_for ( field ) . first . blank? ) or
2006-12-16 13:37:32 +00:00
# filter doesn't require any value
2007-09-05 17:47:17 +00:00
[ " o " , " c " , " !* " , " * " , " t " , " w " ] . include? operator_for ( field )
2006-12-16 13:37:32 +00:00
end if filters
end
2011-08-20 03:38:29 +00:00
2011-07-03 11:01:08 +00:00
# Returns true if the query is visible to +user+ or the current user.
def visible? ( user = User . current )
2011-07-06 16:57:04 +00:00
( project . nil? || user . allowed_to? ( :view_issues , project ) ) && ( self . is_public? || self . user_id == user . id )
2011-07-03 11:01:08 +00:00
end
2011-05-17 04:33:19 +00:00
2007-06-23 13:49:29 +00:00
def editable_by? ( user )
return false unless user
2008-03-30 12:29:07 +00:00
# Admin can edit them all and regular users can edit their private queries
return true if user . admin? || ( ! is_public && self . user_id == user . id )
# 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 )
2007-06-23 13:49:29 +00:00
end
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
def available_filters
return @available_filters if @available_filters
2011-05-17 04:33:19 +00:00
2008-01-16 21:27:48 +00:00
trackers = project . nil? ? Tracker . find ( :all , :order = > 'position' ) : project . rolled_up_trackers
2011-05-17 04:33:19 +00:00
@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 ] } } ,
2009-05-30 23:30:36 +00:00
" priority_id " = > { :type = > :list , :order = > 3 , :values = > IssuePriority . all . collect { | s | [ s . name , s . id . to_s ] } } ,
2011-05-17 04:33:19 +00:00
" subject " = > { :type = > :text , :order = > 8 } ,
" created_on " = > { :type = > :date_past , :order = > 9 } ,
2007-01-15 20:22:23 +00:00
" updated_on " = > { :type = > :date_past , :order = > 10 } ,
" start_date " = > { :type = > :date , :order = > 11 } ,
2007-09-09 19:46:28 +00:00
" due_date " = > { :type = > :date , :order = > 12 } ,
2011-07-11 11:35:53 +00:00
" estimated_hours " = > { :type = > :float , :order = > 13 } ,
2008-07-26 09:05:26 +00:00
" done_ratio " = > { :type = > :integer , :order = > 14 } }
2011-05-17 04:33:19 +00:00
2011-07-23 18:18:13 +00:00
principals = [ ]
2007-08-31 17:02:44 +00:00
if project
2011-07-23 18:18:13 +00:00
principals += project . principals . sort
2007-12-12 20:45:32 +00:00
else
2010-12-20 20:03:22 +00:00
all_projects = Project . visible . all
if all_projects . any?
# members of visible projects
2011-07-23 18:18:13 +00:00
principals += Principal . active . find ( :all , :conditions = > [ " #{ User . table_name } .id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?)) " , all_projects . collect ( & :id ) ] ) . sort
2011-05-17 04:33:19 +00:00
2010-12-20 20:03:22 +00:00
# project filter
project_values = [ ]
Project . project_tree ( all_projects ) do | p , level |
prefix = ( level > 0 ? ( '--' * level + ' ' ) : '' )
project_values << [ " #{ prefix } #{ p . name } " , p . id . to_s ]
end
@available_filters [ " project_id " ] = { :type = > :list , :order = > 1 , :values = > project_values } unless project_values . empty?
2010-03-21 09:28:24 +00:00
end
2007-08-31 17:02:44 +00:00
end
2011-07-23 18:18:13 +00:00
users = principals . select { | p | p . is_a? ( User ) }
2011-08-20 03:38:29 +00:00
2011-07-23 18:18:13 +00:00
assigned_to_values = [ ]
assigned_to_values << [ " << #{ l ( :label_me ) } >> " , " me " ] if User . current . logged?
assigned_to_values += ( Setting . issue_group_assignment? ? principals : users ) . collect { | s | [ s . name , s . id . to_s ] }
@available_filters [ " assigned_to_id " ] = { :type = > :list_optional , :order = > 4 , :values = > assigned_to_values } unless assigned_to_values . empty?
2011-08-20 03:38:29 +00:00
2011-07-23 18:18:13 +00:00
author_values = [ ]
author_values << [ " << #{ l ( :label_me ) } >> " , " me " ] if User . current . logged?
author_values += users . collect { | s | [ s . name , s . id . to_s ] }
@available_filters [ " author_id " ] = { :type = > :list , :order = > 5 , :values = > author_values } unless author_values . empty?
2010-09-10 18:46:29 +00:00
2010-10-16 00:00:23 +00:00
group_values = Group . all . collect { | g | [ g . name , g . id . to_s ] }
2010-09-10 18:46:29 +00:00
@available_filters [ " member_of_group " ] = { :type = > :list_optional , :order = > 6 , :values = > group_values } unless group_values . empty?
2010-09-10 19:44:45 +00:00
2010-10-16 00:00:23 +00:00
role_values = Role . givable . collect { | r | [ r . name , r . id . to_s ] }
2010-09-10 19:44:45 +00:00
@available_filters [ " assigned_to_role " ] = { :type = > :list_optional , :order = > 7 , :values = > role_values } unless role_values . empty?
2011-05-17 04:33:19 +00:00
2009-02-12 17:35:57 +00:00
if User . current . logged?
@available_filters [ " watcher_id " ] = { :type = > :list , :order = > 15 , :values = > [ [ " << #{ l ( :label_me ) } >> " , " me " ] ] }
end
2011-05-17 04:33:19 +00:00
2007-08-31 17:02:44 +00:00
if project
2008-06-22 15:35:11 +00:00
# project specific filters
2011-04-02 10:18:05 +00:00
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 ] } }
2008-06-22 15:35:11 +00:00
end
2011-04-02 10:18:05 +00:00
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 ] } }
2008-06-22 15:20:59 +00:00
end
2011-04-02 10:18:05 +00:00
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
2007-03-19 20:31:02 +00:00
end
2008-06-27 20:13:56 +00:00
add_custom_fields_filters ( @project . all_issue_custom_fields )
2008-06-22 15:35:11 +00:00
else
# global filters for cross project issue list
2010-02-10 21:42:32 +00:00
system_shared_versions = Version . visible . find_all_by_sharing ( 'system' )
unless system_shared_versions . empty?
@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 ] } }
end
2008-06-22 15:35:11 +00:00
add_custom_fields_filters ( IssueCustomField . find ( :all , :conditions = > { :is_filter = > true , :is_for_all = > true } ) )
2006-12-16 13:37:32 +00:00
end
@available_filters
end
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
def add_filter ( field , operator , values )
# values must be an array
2011-07-10 17:29:29 +00:00
return unless values . nil? || values . is_a? ( Array )
2006-12-16 13:37:32 +00:00
# check if field is defined as an available filter
if available_filters . has_key? field
filter_options = available_filters [ field ]
# check if operator is allowed for that filter
#if @@operators_by_filter_type[filter_options[:type]].include? operator
# allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
# filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
#end
2011-07-10 18:34:49 +00:00
filters [ field ] = { :operator = > operator , :values = > ( values || [ '' ] ) }
2006-12-16 13:37:32 +00:00
end
end
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
def add_short_filter ( field , expression )
2011-10-12 23:45:47 +00:00
return unless expression && available_filters . has_key? ( field )
field_type = available_filters [ field ] [ :type ]
@@operators_by_filter_type [ field_type ] . sort . reverse . detect do | operator |
next unless expression =~ / ^ #{ Regexp . escape ( operator ) } (.*)$ /
add_filter field , operator , $1 . present? ? $1 . split ( '|' ) : [ '' ]
end || add_filter ( field , '=' , expression . split ( '|' ) )
2006-12-16 13:37:32 +00:00
end
2010-04-19 15:08:28 +00:00
# Add multiple filters using +add_filter+
def add_filters ( fields , operators , values )
2011-07-10 17:29:29 +00:00
if fields . is_a? ( Array ) && operators . is_a? ( Hash ) && ( values . nil? || values . is_a? ( Hash ) )
2010-11-07 15:38:51 +00:00
fields . each do | field |
2011-07-10 17:29:29 +00:00
add_filter ( field , operators [ field ] , values && values [ field ] )
2010-11-07 15:38:51 +00:00
end
2010-04-19 15:08:28 +00:00
end
end
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
def has_filter? ( field )
filters and filters [ field ]
end
2011-08-20 03:38:29 +00:00
2011-07-10 17:29:29 +00:00
def type_for ( field )
available_filters [ field ] [ :type ] if available_filters . has_key? ( field )
end
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
def operator_for ( field )
has_filter? ( field ) ? filters [ field ] [ :operator ] : nil
end
2011-05-17 04:33:19 +00:00
2006-12-16 13:37:32 +00:00
def values_for ( field )
has_filter? ( field ) ? filters [ field ] [ :values ] : nil
end
2011-08-20 03:38:29 +00:00
2011-07-10 08:00:25 +00:00
def value_for ( field , index = 0 )
( values_for ( field ) || [ ] ) [ index ]
end
2011-05-17 04:33:19 +00:00
2007-04-17 10:53:20 +00:00
def label_for ( field )
2008-05-25 12:50:33 +00:00
label = available_filters [ field ] [ :name ] if available_filters . has_key? ( field )
2007-04-17 10:53:20 +00:00
label || = field . gsub ( / \ _id$ / , " " )
end
2007-10-01 08:44:17 +00:00
def available_columns
2007-11-07 20:42:28 +00:00
return @available_columns if @available_columns
@available_columns = Query . available_columns
2011-08-20 03:38:29 +00:00
@available_columns += ( project ?
2008-06-27 20:13:56 +00:00
project . all_issue_custom_fields :
2009-05-13 17:32:15 +00:00
IssueCustomField . find ( :all )
2011-05-17 04:33:19 +00:00
) . collect { | cf | QueryCustomFieldColumn . new ( cf ) }
2007-10-01 08:44:17 +00:00
end
2010-04-20 15:42:52 +00:00
def self . available_columns = ( v )
self . available_columns = ( v )
end
2011-05-17 04:33:19 +00:00
2010-04-20 15:42:52 +00:00
def self . add_available_column ( column )
self . available_columns << ( column ) if column . is_a? ( QueryColumn )
end
2011-05-17 04:33:19 +00:00
2009-04-26 13:09:14 +00:00
# Returns an array of columns that can be used to group the results
def groupable_columns
available_columns . select { | c | c . groupable }
end
2010-03-19 15:42:03 +00:00
# Returns a Hash of columns and the key for sorting
def sortable_columns
{ 'id' = > " #{ Issue . table_name } .id " } . merge ( available_columns . inject ( { } ) { | h , column |
h [ column . name . to_s ] = column . sortable
h
} )
end
2011-05-17 04:33:19 +00:00
2007-10-01 08:44:17 +00:00
def columns
2011-09-26 17:44:20 +00:00
# preserve the column_names order
( has_default_columns? ? default_columns_names : column_names ) . collect do | name |
available_columns . find { | col | col . name == name }
end . compact
end
def default_columns_names
@default_columns_names || = begin
default_columns = Setting . issue_list_default_columns . map ( & :to_sym )
project . present? ? default_columns : [ :project ] | default_columns
2007-10-01 08:44:17 +00:00
end
end
2011-05-17 04:33:19 +00:00
2007-10-01 08:44:17 +00:00
def column_names = ( names )
2009-11-28 11:34:12 +00:00
if names
names = names . select { | n | n . is_a? ( Symbol ) || ! n . blank? }
names = names . collect { | n | n . is_a? ( Symbol ) ? n : n . to_sym }
# Set column_names to nil if default columns
2011-09-26 17:44:20 +00:00
if names == default_columns_names
2009-11-28 11:34:12 +00:00
names = nil
end
end
2007-10-01 08:44:17 +00:00
write_attribute ( :column_names , names )
end
2011-05-17 04:33:19 +00:00
2007-10-01 08:44:17 +00:00
def has_column? ( column )
column_names && column_names . include? ( column . name )
end
2011-05-17 04:33:19 +00:00
2007-10-01 10:44:45 +00:00
def has_default_columns?
column_names . nil? || column_names . empty?
end
2011-05-17 04:33:19 +00:00
2009-03-12 18:06:54 +00:00
def sort_criteria = ( arg )
c = [ ]
if arg . is_a? ( Hash )
arg = arg . keys . sort . collect { | k | arg [ k ] }
end
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
2011-05-17 04:33:19 +00:00
2009-03-12 18:06:54 +00:00
def sort_criteria
read_attribute ( :sort_criteria ) || [ ]
end
2011-05-17 04:33:19 +00:00
2009-03-12 18:06:54 +00:00
def sort_criteria_key ( arg )
sort_criteria && sort_criteria [ arg ] && sort_criteria [ arg ] . first
end
2011-05-17 04:33:19 +00:00
2009-03-12 18:06:54 +00:00
def sort_criteria_order ( arg )
sort_criteria && sort_criteria [ arg ] && sort_criteria [ arg ] . last
end
2011-05-17 04:33:19 +00:00
2009-04-26 13:09:14 +00:00
# Returns the SQL sort order that should be prepended for grouping
def group_by_sort_order
if grouped? && ( column = group_by_column )
column . sortable . is_a? ( Array ) ?
column . sortable . collect { | s | " #{ s } #{ column . default_order } " } . join ( ',' ) :
" #{ column . sortable } #{ column . default_order } "
end
end
2011-05-17 04:33:19 +00:00
2009-04-26 13:09:14 +00:00
# Returns true if the query is a grouped query
def grouped?
2010-12-21 21:46:54 +00:00
! group_by_column . nil?
2009-04-26 13:09:14 +00:00
end
2011-05-17 04:33:19 +00:00
2009-04-26 13:09:14 +00:00
def group_by_column
2010-12-21 21:46:54 +00:00
groupable_columns . detect { | c | c . groupable && c . name . to_s == group_by }
2009-04-26 13:09:14 +00:00
end
2011-05-17 04:33:19 +00:00
2009-11-16 18:07:30 +00:00
def group_by_statement
2010-12-21 21:46:54 +00:00
group_by_column . try ( :groupable )
2009-11-16 18:07:30 +00:00
end
2011-05-17 04:33:19 +00:00
2008-09-11 17:03:26 +00:00
def project_statement
2008-05-14 18:19:37 +00:00
project_clauses = [ ]
2009-01-24 11:31:15 +00:00
if project && ! @project . descendants . active . empty?
2008-02-28 21:57:03 +00:00
ids = [ project . id ]
2008-03-06 08:32:56 +00:00
if has_filter? ( " subproject_id " )
case operator_for ( " subproject_id " )
when '='
# include the selected subprojects
ids += values_for ( " subproject_id " ) . each ( & :to_i )
when '!*'
# main project only
else
# all subprojects
2009-01-24 11:31:15 +00:00
ids += project . descendants . collect ( & :id )
2008-03-06 08:32:56 +00:00
end
elsif Setting . display_subprojects_issues?
2009-01-24 11:31:15 +00:00
ids += project . descendants . collect ( & :id )
2007-03-19 20:31:02 +00:00
end
2008-09-11 17:03:26 +00:00
project_clauses << " #{ Project . table_name } .id IN (%s) " % ids . join ( ',' )
2007-08-31 17:02:44 +00:00
elsif project
2008-09-11 17:03:26 +00:00
project_clauses << " #{ Project . table_name } .id = %d " % project . id
2007-03-19 20:31:02 +00:00
end
2011-04-12 16:53:39 +00:00
project_clauses . any? ? project_clauses . join ( ' AND ' ) : nil
2008-09-11 17:03:26 +00:00
end
def statement
2007-08-26 12:29:53 +00:00
# filters clauses
filters_clauses = [ ]
2006-12-16 13:37:32 +00:00
filters . each_key do | field |
2007-03-19 20:31:02 +00:00
next if field == " subproject_id "
2007-05-08 12:46:15 +00:00
v = values_for ( field ) . clone
2007-04-17 10:53:20 +00:00
next unless v and ! v . empty?
2009-02-12 17:35:57 +00:00
operator = operator_for ( field )
2011-05-17 04:33:19 +00:00
2009-02-12 17:35:57 +00:00
# "me" value subsitution
if %w( assigned_to_id author_id watcher_id ) . include? ( field )
2011-07-31 10:22:36 +00:00
if v . delete ( " me " )
if User . current . logged?
v . push ( User . current . id . to_s )
v += User . current . group_ids . map ( & :to_s ) if field == 'assigned_to_id'
else
v . push ( " 0 " )
end
end
2009-02-12 17:35:57 +00:00
end
2011-05-17 04:33:19 +00:00
2007-04-17 10:53:20 +00:00
if field =~ / ^cf_( \ d+)$ /
# custom field
2011-07-20 17:44:10 +00:00
filters_clauses << sql_for_custom_field ( field , operator , v , $1 )
elsif respond_to? ( " sql_for_ #{ field } _field " )
# specific statement
filters_clauses << send ( " sql_for_ #{ field } _field " , field , operator , v )
2007-04-17 10:53:20 +00:00
else
# regular field
2011-07-20 17:44:10 +00:00
filters_clauses << '(' + sql_for_field ( field , operator , v , Issue . table_name , field ) + ')'
2007-04-17 10:53:20 +00:00
end
2006-12-16 13:37:32 +00:00
end if filters and valid?
2011-05-17 04:33:19 +00:00
2011-04-12 16:53:39 +00:00
filters_clauses << project_statement
filters_clauses . reject! ( & :blank? )
2011-05-17 04:33:19 +00:00
2011-04-12 16:53:39 +00:00
filters_clauses . any? ? filters_clauses . join ( ' AND ' ) : nil
2006-12-16 13:37:32 +00:00
end
2011-05-17 04:33:19 +00:00
2009-11-28 10:08:29 +00:00
# Returns the issue count
def issue_count
2011-07-24 16:48:26 +00:00
Issue . visible . count ( :include = > [ :status , :project ] , :conditions = > statement )
2009-11-28 10:29:48 +00:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
2009-11-28 10:08:29 +00:00
end
2011-05-17 04:33:19 +00:00
2009-11-28 10:08:29 +00:00
# Returns the issue count by group or nil if query is not grouped
def issue_count_by_group
2009-12-02 18:57:17 +00:00
r = nil
2009-11-28 10:08:29 +00:00
if grouped?
begin
# Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
2011-04-12 16:53:39 +00:00
r = Issue . visible . count ( :group = > group_by_statement , :include = > [ :status , :project ] , :conditions = > statement )
2009-11-28 10:08:29 +00:00
rescue ActiveRecord :: RecordNotFound
2009-12-02 18:57:17 +00:00
r = { nil = > issue_count }
end
c = group_by_column
if c . is_a? ( QueryCustomFieldColumn )
r = r . keys . inject ( { } ) { | h , k | h [ c . custom_field . cast_value ( k ) ] = r [ k ] ; h }
2009-11-28 10:08:29 +00:00
end
end
2009-12-02 18:57:17 +00:00
r
2009-11-28 10:29:48 +00:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
2009-11-28 10:08:29 +00:00
end
2011-05-17 04:33:19 +00:00
2009-11-28 10:08:29 +00:00
# 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?
2011-05-17 04:33:19 +00:00
2011-04-12 16:53:39 +00:00
Issue . visible . find :all , :include = > ( [ :status , :project ] + ( options [ :include ] || [ ] ) ) . uniq ,
2009-11-28 10:08:29 +00:00
:conditions = > Query . merge_conditions ( statement , options [ :conditions ] ) ,
:order = > order_option ,
:limit = > options [ :limit ] ,
:offset = > options [ :offset ]
2009-11-28 10:29:48 +00:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
2009-11-28 10:08:29 +00:00
end
# Returns the journals
# Valid options are :order, :offset, :limit
def journals ( options = { } )
2011-04-12 16:53:39 +00:00
Journal . visible . find :all , :include = > [ :details , :user , { :issue = > [ :project , :author , :tracker , :status ] } ] ,
2009-11-28 10:08:29 +00:00
:conditions = > statement ,
:order = > options [ :order ] ,
:limit = > options [ :limit ] ,
:offset = > options [ :offset ]
2009-11-28 10:29:48 +00:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
2009-11-28 10:08:29 +00:00
end
2011-05-17 04:33:19 +00:00
2009-11-28 10:08:29 +00:00
# Returns the versions
# Valid options are :conditions
def versions ( options = { } )
2011-04-12 16:53:39 +00:00
Version . visible . find :all , :include = > :project ,
2009-11-28 10:08:29 +00:00
:conditions = > Query . merge_conditions ( project_statement , options [ :conditions ] )
2009-11-28 10:29:48 +00:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
2009-11-28 10:08:29 +00:00
end
2011-08-20 03:38:29 +00:00
2011-07-20 17:44:10 +00:00
def sql_for_watcher_id_field ( field , operator , value )
db_table = Watcher . table_name
" #{ Issue . table_name } .id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{ db_table } .watchable_id FROM #{ db_table } WHERE #{ db_table } .watchable_type='Issue' AND " +
sql_for_field ( field , '=' , value , db_table , 'user_id' ) + ')'
end
2011-08-20 03:38:29 +00:00
2011-07-20 17:44:10 +00:00
def sql_for_member_of_group_field ( field , operator , value )
if operator == '*' # Any group
groups = Group . all
operator = '=' # Override the operator since we want to find by assigned_to
elsif operator == " !* "
groups = Group . all
operator = '!' # Override the operator since we want to find by assigned_to
else
groups = Group . find_all_by_id ( value )
end
groups || = [ ]
members_of_groups = groups . inject ( [ ] ) { | user_ids , group |
if group && group . user_ids . present?
user_ids << group . user_ids
end
user_ids . flatten . uniq . compact
} . sort . collect ( & :to_s )
'(' + sql_for_field ( " assigned_to_id " , operator , members_of_groups , Issue . table_name , " assigned_to_id " , false ) + ')'
end
2011-08-20 03:38:29 +00:00
2011-07-20 17:44:10 +00:00
def sql_for_assigned_to_role_field ( field , operator , value )
if operator == " * " # Any Role
roles = Role . givable
operator = '=' # Override the operator since we want to find by assigned_to
elsif operator == " !* " # No role
roles = Role . givable
operator = '!' # Override the operator since we want to find by assigned_to
else
roles = Role . givable . find_all_by_id ( value )
end
roles || = [ ]
2011-05-17 04:33:19 +00:00
2011-07-20 17:44:10 +00:00
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_for_field ( " assigned_to_id " , operator , members_of_roles , Issue . table_name , " assigned_to_id " , false ) + ')'
end
2011-05-17 04:33:19 +00:00
2011-07-20 17:44:10 +00:00
private
2011-08-20 03:38:29 +00:00
2011-07-20 17:44:10 +00:00
def sql_for_custom_field ( field , operator , value , custom_field_id )
db_table = CustomValue . table_name
db_field = 'value'
" #{ Issue . table_name } .id IN (SELECT #{ Issue . table_name } .id FROM #{ Issue . table_name } LEFT OUTER JOIN #{ db_table } ON #{ db_table } .customized_type='Issue' AND #{ db_table } .customized_id= #{ Issue . table_name } .id AND #{ db_table } .custom_field_id= #{ custom_field_id } WHERE " +
sql_for_field ( field , operator , value , db_table , db_field , true ) + ')'
end
2011-08-20 03:38:29 +00:00
2009-02-12 17:35:57 +00:00
# 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 )
2008-12-05 22:56:03 +00:00
sql = ''
2009-02-12 17:35:57 +00:00
case operator
2008-12-05 22:56:03 +00:00
when " = "
2011-07-11 11:35:53 +00:00
if value . any?
case type_for ( field )
when :date , :date_past
sql = date_clause ( db_table , db_field , ( Date . parse ( value . first ) rescue nil ) , ( Date . parse ( value . first ) rescue nil ) )
when :integer
sql = " #{ db_table } . #{ db_field } = #{ value . first . to_i } "
when :float
sql = " #{ db_table } . #{ db_field } BETWEEN #{ value . first . to_f - 1 e - 5 } AND #{ value . first . to_f + 1 e - 5 } "
2011-07-10 17:29:29 +00:00
else
2011-07-11 11:35:53 +00:00
sql = " #{ db_table } . #{ db_field } IN ( " + value . collect { | val | " ' #{ connection . quote_string ( val ) } ' " } . join ( " , " ) + " ) "
2011-07-10 17:29:29 +00:00
end
2011-07-11 11:35:53 +00:00
else
# IN an empty set
sql = " 1=0 "
2011-02-20 13:03:32 +00:00
end
2008-12-05 22:56:03 +00:00
when " ! "
2011-02-20 13:03:32 +00:00
if value . any?
sql = " ( #{ db_table } . #{ db_field } IS NULL OR #{ db_table } . #{ db_field } NOT IN ( " + value . collect { | val | " ' #{ connection . quote_string ( val ) } ' " } . join ( " , " ) + " )) "
else
# NOT IN an empty set
sql = " 1=1 "
end
2008-12-05 22:56:03 +00:00
when " !* "
sql = " #{ db_table } . #{ db_field } IS NULL "
sql << " OR #{ db_table } . #{ db_field } = '' " if is_custom_filter
when " * "
sql = " #{ db_table } . #{ db_field } IS NOT NULL "
sql << " AND #{ db_table } . #{ db_field } <> '' " if is_custom_filter
when " >= "
2011-07-10 17:29:29 +00:00
if [ :date , :date_past ] . include? ( type_for ( field ) )
sql = date_clause ( db_table , db_field , ( Date . parse ( value . first ) rescue nil ) , nil )
2011-07-09 21:34:35 +00:00
else
2011-07-10 17:29:29 +00:00
if is_custom_filter
2011-07-10 18:09:40 +00:00
sql = " CAST( #{ db_table } . #{ db_field } AS decimal(60,3)) >= #{ value . first . to_f } "
2011-07-10 17:29:29 +00:00
else
2011-07-10 18:09:40 +00:00
sql = " #{ db_table } . #{ db_field } >= #{ value . first . to_f } "
2011-07-10 17:29:29 +00:00
end
2011-07-09 21:34:35 +00:00
end
2008-12-05 22:56:03 +00:00
when " <= "
2011-07-10 17:29:29 +00:00
if [ :date , :date_past ] . include? ( type_for ( field ) )
sql = date_clause ( db_table , db_field , nil , ( Date . parse ( value . first ) rescue nil ) )
2011-07-09 21:34:35 +00:00
else
2011-07-10 17:29:29 +00:00
if is_custom_filter
2011-07-10 18:09:40 +00:00
sql = " CAST( #{ db_table } . #{ db_field } AS decimal(60,3)) <= #{ value . first . to_f } "
2011-07-10 17:29:29 +00:00
else
2011-07-10 18:09:40 +00:00
sql = " #{ db_table } . #{ db_field } <= #{ value . first . to_f } "
2011-07-10 17:29:29 +00:00
end
2011-07-09 21:34:35 +00:00
end
2011-07-10 08:00:25 +00:00
when " >< "
2011-07-10 17:29:29 +00:00
if [ :date , :date_past ] . include? ( type_for ( field ) )
sql = date_clause ( db_table , db_field , ( Date . parse ( value [ 0 ] ) rescue nil ) , ( Date . parse ( value [ 1 ] ) rescue nil ) )
2011-07-10 08:00:25 +00:00
else
2011-07-10 17:29:29 +00:00
if is_custom_filter
2011-07-10 18:09:40 +00:00
sql = " CAST( #{ db_table } . #{ db_field } AS decimal(60,3)) BETWEEN #{ value [ 0 ] . to_f } AND #{ value [ 1 ] . to_f } "
2011-07-10 17:29:29 +00:00
else
2011-07-10 18:09:40 +00:00
sql = " #{ db_table } . #{ db_field } BETWEEN #{ value [ 0 ] . to_f } AND #{ value [ 1 ] . to_f } "
2011-07-10 17:29:29 +00:00
end
2011-07-10 08:00:25 +00:00
end
2008-12-05 22:56:03 +00:00
when " o "
2008-12-05 22:56:08 +00:00
sql = " #{ IssueStatus . table_name } .is_closed= #{ connection . quoted_false } " if field == " status_id "
2008-12-05 22:56:03 +00:00
when " c "
2008-12-05 22:56:08 +00:00
sql = " #{ IssueStatus . table_name } .is_closed= #{ connection . quoted_true } " if field == " status_id "
2008-12-05 22:56:03 +00:00
when " >t- "
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , - value . first . to_i , 0 )
2008-12-05 22:56:03 +00:00
when " <t- "
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , nil , - value . first . to_i )
2008-12-05 22:56:03 +00:00
when " t- "
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , - value . first . to_i , - value . first . to_i )
2008-12-05 22:56:03 +00:00
when " >t+ "
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , value . first . to_i , nil )
2008-12-05 22:56:03 +00:00
when " <t+ "
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , 0 , value . first . to_i )
2008-12-05 22:56:03 +00:00
when " t+ "
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , value . first . to_i , value . first . to_i )
2008-12-05 22:56:03 +00:00
when " t "
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , 0 , 0 )
2008-12-05 22:56:03 +00:00
when " w "
2011-04-29 11:28:27 +00:00
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 )
2011-07-10 17:29:29 +00:00
sql = relative_date_clause ( db_table , db_field , - days_ago , - days_ago + 6 )
2008-12-05 22:56:03 +00:00
when " ~ "
2009-06-28 12:12:27 +00:00
sql = " LOWER( #{ db_table } . #{ db_field } ) LIKE '% #{ connection . quote_string ( value . first . to_s . downcase ) } %' "
2008-12-05 22:56:03 +00:00
when " !~ "
2009-06-28 12:12:27 +00:00
sql = " LOWER( #{ db_table } . #{ db_field } ) NOT LIKE '% #{ connection . quote_string ( value . first . to_s . downcase ) } %' "
2011-07-10 08:00:25 +00:00
else
raise " Unknown query operator #{ operator } "
2008-12-05 22:56:03 +00:00
end
2011-05-17 04:33:19 +00:00
2008-12-05 22:56:08 +00:00
return sql
2008-12-05 22:56:03 +00:00
end
2011-05-17 04:33:19 +00:00
2008-06-22 15:35:11 +00:00
def add_custom_fields_filters ( custom_fields )
@available_filters || = { }
2011-05-17 04:33:19 +00:00
2008-06-22 15:35:11 +00:00
custom_fields . select ( & :is_filter? ) . each do | field |
case field . field_format
when " text "
options = { :type = > :text , :order = > 20 }
when " list "
options = { :type = > :list_optional , :values = > field . possible_values , :order = > 20 }
when " date "
options = { :type = > :date , :order = > 20 }
when " bool "
options = { :type = > :list , :values = > [ [ l ( :general_text_yes ) , " 1 " ] , [ l ( :general_text_no ) , " 0 " ] ] , :order = > 20 }
2011-07-11 11:35:53 +00:00
when " int "
2011-07-09 20:30:51 +00:00
options = { :type = > :integer , :order = > 20 }
2011-07-11 11:38:46 +00:00
when " float "
2011-07-11 11:35:53 +00:00
options = { :type = > :float , :order = > 20 }
2011-04-01 13:44:58 +00:00
when " user " , " version "
next unless project
options = { :type = > :list_optional , :values = > field . possible_values_options ( project ) , :order = > 20 }
2008-06-22 15:35:11 +00:00
else
options = { :type = > :string , :order = > 20 }
end
@available_filters [ " cf_ #{ field . id } " ] = options . merge ( { :name = > field . name } )
end
end
2011-08-20 03:38:29 +00:00
2008-11-23 16:40:35 +00:00
# Returns a SQL clause for a date or datetime field.
2011-07-10 17:29:29 +00:00
def date_clause ( table , field , from , to )
2008-11-23 16:40:35 +00:00
s = [ ]
if from
2011-10-07 00:23:05 +00:00
from_yesterday = from - 1
from_yesterday_utc = Time . gm ( from_yesterday . year , from_yesterday . month , from_yesterday . day )
s << ( " #{ table } . #{ field } > '%s' " % [ connection . quoted_date ( from_yesterday_utc . end_of_day ) ] )
2008-11-23 16:40:35 +00:00
end
if to
2011-10-07 00:23:05 +00:00
to_utc = Time . gm ( to . year , to . month , to . day )
s << ( " #{ table } . #{ field } <= '%s' " % [ connection . quoted_date ( to_utc . end_of_day ) ] )
2008-11-23 16:40:35 +00:00
end
s . join ( ' AND ' )
end
2011-08-20 03:38:29 +00:00
2011-07-10 17:29:29 +00:00
# Returns a SQL clause for a date or datetime field using relative dates.
def relative_date_clause ( table , field , days_from , days_to )
date_clause ( table , field , ( days_from ? Date . today + days_from : nil ) , ( days_to ? Date . today + days_to : nil ) )
end
2006-12-16 13:37:32 +00:00
end