2008-09-11 21:03:26 +04:00
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
2006-12-16 16:37:32 +03: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.
#
# 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.
2007-10-01 12:44:17 +04:00
class QueryColumn
2008-02-05 20:28:02 +03:00
attr_accessor :name , :sortable , :default_order
2007-11-07 23:42:28 +03:00
include GLoc
2007-10-01 12:44:17 +04:00
def initialize ( name , options = { } )
self . name = name
self . sortable = options [ :sortable ]
2008-02-05 20:28:02 +03:00
self . default_order = options [ :default_order ]
2007-10-01 12:44:17 +04:00
end
2007-11-07 23:42:28 +03:00
def caption
2007-11-16 23:46:33 +03:00
set_language_if_valid ( User . current . language )
2007-11-07 23:42:28 +03:00
l ( " field_ #{ name } " )
end
end
class QueryCustomFieldColumn < QueryColumn
def initialize ( custom_field )
self . name = " cf_ #{ custom_field . id } " . to_sym
2009-01-11 19:33:51 +03:00
self . sortable = custom_field . order_statement || false
2007-11-07 23:42:28 +03:00
@cf = custom_field
end
def caption
@cf . name
end
def custom_field
@cf
end
2007-10-01 12:44:17 +04:00
end
2006-12-16 16:37:32 +03:00
class Query < ActiveRecord :: Base
belongs_to :project
belongs_to :user
serialize :filters
2007-10-01 12:44:17 +04:00
serialize :column_names
2006-12-16 16:37:32 +03:00
2007-12-31 15:37:32 +03:00
attr_protected :project_id , :user_id
2006-12-16 16:37:32 +03:00
validates_presence_of :name , :on = > :save
2007-07-16 21:16:49 +04:00
validates_length_of :name , :maximum = > 255
2006-12-16 16:37:32 +03:00
@@operators = { " = " = > :label_equals ,
" ! " = > :label_not_equals ,
" o " = > :label_open_issues ,
" c " = > :label_closed_issues ,
" !* " = > :label_none ,
" * " = > :label_all ,
2007-09-09 23:46:28 +04:00
" >= " = > '>=' ,
" <= " = > '<=' ,
2006-12-16 16:37:32 +03:00
" <t+ " = > :label_in_less_than ,
" >t+ " = > :label_in_more_than ,
" t+ " = > :label_in ,
" t " = > :label_today ,
2007-09-05 21:47:17 +04:00
" w " = > :label_this_week ,
2006-12-16 16:37:32 +03:00
" >t- " = > :label_less_than_ago ,
" <t- " = > :label_more_than_ago ,
" t- " = > :label_ago ,
" ~ " = > :label_contains ,
" !~ " = > :label_not_contains }
cattr_reader :operators
@@operators_by_filter_type = { :list = > [ " = " , " ! " ] ,
:list_status = > [ " o " , " = " , " ! " , " c " , " * " ] ,
:list_optional = > [ " = " , " ! " , " !* " , " * " ] ,
2008-02-29 00:57:03 +03:00
:list_subprojects = > [ " * " , " !* " , " = " ] ,
2007-09-05 21:47:17 +04:00
:date = > [ " <t+ " , " >t+ " , " t+ " , " t " , " w " , " >t- " , " <t- " , " t- " ] ,
:date_past = > [ " >t- " , " <t- " , " t- " , " t " , " w " ] ,
2007-04-17 14:53:20 +04:00
:string = > [ " = " , " ~ " , " ! " , " !~ " ] ,
2007-09-09 23:46:28 +04:00
:text = > [ " ~ " , " !~ " ] ,
2008-07-26 13:05:26 +04:00
:integer = > [ " = " , " >= " , " <= " , " !* " , " * " ] }
2006-12-16 16:37:32 +03:00
cattr_reader :operators_by_filter_type
2007-10-01 12:44:17 +04:00
@@available_columns = [
2007-10-01 16:04:24 +04:00
QueryColumn . new ( :tracker , :sortable = > " #{ Tracker . table_name } .position " ) ,
QueryColumn . new ( :status , :sortable = > " #{ IssueStatus . table_name } .position " ) ,
2008-02-05 20:28:02 +03:00
QueryColumn . new ( :priority , :sortable = > " #{ Enumeration . table_name } .position " , :default_order = > 'desc' ) ,
QueryColumn . new ( :subject , :sortable = > " #{ Issue . table_name } .subject " ) ,
2008-02-03 17:02:23 +03:00
QueryColumn . new ( :author ) ,
2009-02-01 23:56:10 +03:00
QueryColumn . new ( :assigned_to , :sortable = > [ " #{ User . table_name } .lastname " , " #{ User . table_name } .firstname " ] ) ,
2008-02-05 20:28:02 +03:00
QueryColumn . new ( :updated_on , :sortable = > " #{ Issue . table_name } .updated_on " , :default_order = > 'desc' ) ,
2007-10-01 12:44:17 +04:00
QueryColumn . new ( :category , :sortable = > " #{ IssueCategory . table_name } .name " ) ,
2009-02-01 23:57:44 +03:00
QueryColumn . new ( :fixed_version , :sortable = > [ " #{ Version . table_name } .effective_date " , " #{ Version . table_name } .name " ] , :default_order = > 'desc' ) ,
2007-10-01 12:44:17 +04: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 " ) ,
QueryColumn . new ( :done_ratio , :sortable = > " #{ Issue . table_name } .done_ratio " ) ,
2008-02-05 20:28:02 +03:00
QueryColumn . new ( :created_on , :sortable = > " #{ Issue . table_name } .created_on " , :default_order = > 'desc' ) ,
2007-10-01 12:44:17 +04:00
]
cattr_reader :available_columns
2006-12-16 16:37:32 +03:00
def initialize ( attributes = nil )
super attributes
self . filters || = { 'status_id' = > { :operator = > " o " , :values = > [ " " ] } }
2007-12-12 23:45:32 +03:00
set_language_if_valid ( User . current . language )
2007-05-08 16:46:15 +04:00
end
2008-03-30 16:29:07 +04:00
def after_initialize
# Store the fact that project is nil (used in #editable_by?)
@is_for_all = project . nil?
end
2006-12-16 16:37:32 +03:00
def validate
filters . each_key do | field |
2007-04-17 14:53:20 +04:00
errors . add label_for ( field ) , :activerecord_error_blank unless
2006-12-16 16:37:32 +03:00
# filter requires one or more values
2008-04-29 13:57:47 +04:00
( values_for ( field ) and ! values_for ( field ) . first . blank? ) or
2006-12-16 16:37:32 +03:00
# filter doesn't require any value
2007-09-05 21:47:17 +04:00
[ " o " , " c " , " !* " , " * " , " t " , " w " ] . include? operator_for ( field )
2006-12-16 16:37:32 +03:00
end if filters
end
2007-06-23 17:49:29 +04:00
def editable_by? ( user )
return false unless user
2008-03-30 16:29:07 +04: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 17:49:29 +04:00
end
2006-12-16 16:37:32 +03:00
def available_filters
return @available_filters if @available_filters
2008-01-17 00:27:48 +03:00
trackers = project . nil? ? Tracker . find ( :all , :order = > 'position' ) : project . rolled_up_trackers
2007-01-31 22:53:24 +03:00
@available_filters = { " status_id " = > { :type = > :list_status , :order = > 1 , :values = > IssueStatus . find ( :all , :order = > 'position' ) . collect { | s | [ s . name , s . id . to_s ] } } ,
2008-01-17 00:27:48 +03:00
" tracker_id " = > { :type = > :list , :order = > 2 , :values = > trackers . collect { | s | [ s . name , s . id . to_s ] } } ,
2008-03-30 12:19:56 +04:00
" priority_id " = > { :type = > :list , :order = > 3 , :values = > Enumeration . find ( :all , :conditions = > [ 'opt=?' , 'IPRI' ] , :order = > 'position' ) . collect { | s | [ s . name , s . id . to_s ] } } ,
2007-01-15 23:22:23 +03:00
" 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 } ,
2007-09-09 23:46:28 +04:00
" due_date " = > { :type = > :date , :order = > 12 } ,
2008-07-26 13:05:26 +04:00
" estimated_hours " = > { :type = > :integer , :order = > 13 } ,
" done_ratio " = > { :type = > :integer , :order = > 14 } }
2007-08-31 21:02:44 +04:00
user_values = [ ]
2007-12-12 23:45:32 +03:00
user_values << [ " << #{ l ( :label_me ) } >> " , " me " ] if User . current . logged?
2007-08-31 21:02:44 +04:00
if project
2007-12-01 20:42:26 +03:00
user_values += project . users . sort . collect { | s | [ s . name , s . id . to_s ] }
2007-12-12 23:45:32 +03:00
else
2007-08-31 21:02:44 +04:00
# members of the user's projects
2007-12-12 23:45:32 +03:00
user_values += User . current . projects . collect ( & :users ) . flatten . uniq . sort . collect { | s | [ s . name , s . id . to_s ] }
2007-08-31 21:02:44 +04:00
end
@available_filters [ " assigned_to_id " ] = { :type = > :list_optional , :order = > 4 , :values = > user_values } unless user_values . empty?
@available_filters [ " author_id " ] = { :type = > :list , :order = > 5 , :values = > user_values } unless user_values . empty?
if project
2008-06-22 19:35:11 +04:00
# 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 ] } }
end
2008-06-22 19:20:59 +04:00
unless @project . versions . empty?
@available_filters [ " fixed_version_id " ] = { :type = > :list_optional , :order = > 7 , :values = > @project . versions . sort . collect { | s | [ s . name , s . id . to_s ] } }
end
2009-01-24 14:31:15 +03:00
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 ] } }
2007-03-19 23:31:02 +03:00
end
2008-06-28 00:13:56 +04:00
add_custom_fields_filters ( @project . all_issue_custom_fields )
2008-06-22 19:35:11 +04:00
else
# global filters for cross project issue list
add_custom_fields_filters ( IssueCustomField . find ( :all , :conditions = > { :is_filter = > true , :is_for_all = > true } ) )
2006-12-16 16:37:32 +03:00
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?
# 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
filters [ field ] = { :operator = > operator , :values = > values }
end
end
def add_short_filter ( field , expression )
return unless expression
parms = expression . scan ( / ^(o|c| \ !| \ *)?(.*)$ / ) . first
add_filter field , ( parms [ 0 ] || " = " ) , [ parms [ 1 ] || " " ]
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
2007-04-17 14:53:20 +04:00
def label_for ( field )
2008-05-25 16:50:33 +04:00
label = available_filters [ field ] [ :name ] if available_filters . has_key? ( field )
2007-04-17 14:53:20 +04:00
label || = field . gsub ( / \ _id$ / , " " )
end
2007-10-01 12:44:17 +04:00
def available_columns
2007-11-07 23:42:28 +03:00
return @available_columns if @available_columns
@available_columns = Query . available_columns
@available_columns += ( project ?
2008-06-28 00:13:56 +04:00
project . all_issue_custom_fields :
2007-11-07 23:42:28 +03:00
IssueCustomField . find ( :all , :conditions = > { :is_for_all = > true } )
) . collect { | cf | QueryCustomFieldColumn . new ( cf ) }
2007-10-01 12:44:17 +04:00
end
2007-04-17 14:53:20 +04:00
2007-10-01 12:44:17 +04:00
def columns
2007-10-01 14:44:45 +04:00
if has_default_columns?
2007-10-01 16:04:24 +04:00
available_columns . select { | c | Setting . issue_list_default_columns . include? ( c . name . to_s ) }
2007-10-01 14:44:45 +04:00
else
2007-10-01 15:39:34 +04:00
# preserve the column_names order
column_names . collect { | name | available_columns . find { | col | col . name == name } } . compact
2007-10-01 12:44:17 +04:00
end
end
def column_names = ( names )
names = names . select { | n | n . is_a? ( Symbol ) || ! n . blank? } if names
names = names . collect { | n | n . is_a? ( Symbol ) ? n : n . to_sym } if names
write_attribute ( :column_names , names )
end
def has_column? ( column )
column_names && column_names . include? ( column . name )
end
2007-10-01 14:44:45 +04:00
def has_default_columns?
column_names . nil? || column_names . empty?
end
2008-09-11 21:03:26 +04:00
def project_statement
2008-05-14 22:19:37 +04:00
project_clauses = [ ]
2009-01-24 14:31:15 +03:00
if project && ! @project . descendants . active . empty?
2008-02-29 00:57:03 +03:00
ids = [ project . id ]
2008-03-06 11:32:56 +03: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 14:31:15 +03:00
ids += project . descendants . collect ( & :id )
2008-03-06 11:32:56 +03:00
end
elsif Setting . display_subprojects_issues?
2009-01-24 14:31:15 +03:00
ids += project . descendants . collect ( & :id )
2007-03-19 23:31:02 +03:00
end
2008-09-11 21:03:26 +04:00
project_clauses << " #{ Project . table_name } .id IN (%s) " % ids . join ( ',' )
2007-08-31 21:02:44 +04:00
elsif project
2008-09-11 21:03:26 +04:00
project_clauses << " #{ Project . table_name } .id = %d " % project . id
2007-03-19 23:31:02 +03:00
end
2008-09-25 22:51:03 +04:00
project_clauses << Project . allowed_to_condition ( User . current , :view_issues )
2008-09-11 21:03:26 +04:00
project_clauses . join ( ' AND ' )
end
def statement
2007-08-26 16:29:53 +04:00
# filters clauses
filters_clauses = [ ]
2006-12-16 16:37:32 +03:00
filters . each_key do | field |
2007-03-19 23:31:02 +03:00
next if field == " subproject_id "
2007-05-08 16:46:15 +04:00
v = values_for ( field ) . clone
2007-04-17 14:53:20 +04:00
next unless v and ! v . empty?
2007-08-26 16:29:53 +04:00
2008-04-29 13:57:47 +04:00
sql = ''
is_custom_filter = false
2007-04-17 14:53:20 +04:00
if field =~ / ^cf_( \ d+)$ /
# custom field
db_table = CustomValue . table_name
2007-08-26 16:29:53 +04:00
db_field = 'value'
2008-04-29 13:57:47 +04:00
is_custom_filter = true
2008-04-26 20:55:24 +04:00
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 "
2007-04-17 14:53:20 +04:00
else
# regular field
db_table = Issue . table_name
db_field = field
2007-08-26 16:29:53 +04:00
sql << '('
2007-04-17 14:53:20 +04:00
end
2007-05-08 16:46:15 +04:00
# "me" value subsitution
if %w( assigned_to_id author_id ) . include? ( field )
2007-12-12 23:45:32 +03:00
v . push ( User . current . logged? ? User . current . id . to_s : " 0 " ) if v . delete ( " me " )
2007-05-08 16:46:15 +04:00
end
2008-12-06 01:56:03 +03:00
sql = sql + sql_for_field ( field , v , db_table , db_field , is_custom_filter )
2007-08-26 16:29:53 +04:00
sql << ')'
filters_clauses << sql
2006-12-16 16:37:32 +03:00
end if filters and valid?
2007-08-26 16:29:53 +04:00
2008-09-11 21:03:26 +04:00
( filters_clauses << project_statement ) . join ( ' AND ' )
2006-12-16 16:37:32 +03:00
end
2008-06-22 19:35:11 +04:00
private
2008-12-06 01:56:13 +03:00
# Helper method to generate the WHERE sql for a +field+ with a +value+
def sql_for_field ( field , value , db_table , db_field , is_custom_filter )
2008-12-06 01:56:03 +03:00
sql = ''
case operator_for field
when " = "
2008-12-06 01:56:13 +03:00
sql = " #{ db_table } . #{ db_field } IN ( " + value . collect { | val | " ' #{ connection . quote_string ( val ) } ' " } . join ( " , " ) + " ) "
2008-12-06 01:56:03 +03:00
when " ! "
2008-12-06 01:56:13 +03:00
sql = " ( #{ db_table } . #{ db_field } IS NULL OR #{ db_table } . #{ db_field } NOT IN ( " + value . collect { | val | " ' #{ connection . quote_string ( val ) } ' " } . join ( " , " ) + " )) "
2008-12-06 01:56:03 +03: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 " >= "
2008-12-06 01:56:13 +03:00
sql = " #{ db_table } . #{ db_field } >= #{ value . first . to_i } "
2008-12-06 01:56:03 +03:00
when " <= "
2008-12-06 01:56:13 +03:00
sql = " #{ db_table } . #{ db_field } <= #{ value . first . to_i } "
2008-12-06 01:56:03 +03:00
when " o "
2008-12-06 01:56:08 +03:00
sql = " #{ IssueStatus . table_name } .is_closed= #{ connection . quoted_false } " if field == " status_id "
2008-12-06 01:56:03 +03:00
when " c "
2008-12-06 01:56:08 +03:00
sql = " #{ IssueStatus . table_name } .is_closed= #{ connection . quoted_true } " if field == " status_id "
2008-12-06 01:56:03 +03:00
when " >t- "
2008-12-06 01:56:13 +03:00
sql = date_range_clause ( db_table , db_field , - value . first . to_i , 0 )
2008-12-06 01:56:03 +03:00
when " <t- "
2008-12-06 01:56:13 +03:00
sql = date_range_clause ( db_table , db_field , nil , - value . first . to_i )
2008-12-06 01:56:03 +03:00
when " t- "
2008-12-06 01:56:13 +03:00
sql = date_range_clause ( db_table , db_field , - value . first . to_i , - value . first . to_i )
2008-12-06 01:56:03 +03:00
when " >t+ "
2008-12-06 01:56:13 +03:00
sql = date_range_clause ( db_table , db_field , value . first . to_i , nil )
2008-12-06 01:56:03 +03:00
when " <t+ "
2008-12-06 01:56:13 +03:00
sql = date_range_clause ( db_table , db_field , 0 , value . first . to_i )
2008-12-06 01:56:03 +03:00
when " t+ "
2008-12-06 01:56:13 +03:00
sql = date_range_clause ( db_table , db_field , value . first . to_i , value . first . to_i )
2008-12-06 01:56:03 +03:00
when " t "
2008-12-06 01:56:08 +03:00
sql = date_range_clause ( db_table , db_field , 0 , 0 )
2008-12-06 01:56:03 +03:00
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
2008-12-06 01:56:08 +03:00
sql = " #{ db_table } . #{ db_field } BETWEEN '%s' AND '%s' " % [ connection . quoted_date ( from ) , connection . quoted_date ( from + 7 . days ) ]
2008-12-06 01:56:03 +03:00
when " ~ "
2008-12-06 01:56:13 +03:00
sql = " #{ db_table } . #{ db_field } LIKE '% #{ connection . quote_string ( value . first ) } %' "
2008-12-06 01:56:03 +03:00
when " !~ "
2008-12-06 01:56:13 +03:00
sql = " #{ db_table } . #{ db_field } NOT LIKE '% #{ connection . quote_string ( value . first ) } %' "
2008-12-06 01:56:03 +03:00
end
2008-12-06 01:56:08 +03:00
return sql
2008-12-06 01:56:03 +03:00
end
2008-06-22 19:35:11 +04:00
def add_custom_fields_filters ( custom_fields )
@available_filters || = { }
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 }
else
options = { :type = > :string , :order = > 20 }
end
@available_filters [ " cf_ #{ field . id } " ] = options . merge ( { :name = > field . name } )
end
end
2008-11-23 19:40:35 +03:00
# Returns a SQL clause for a date or datetime field.
def date_range_clause ( table , field , from , to )
s = [ ]
if from
s << ( " #{ table } . #{ field } > '%s' " % [ connection . quoted_date ( ( Date . yesterday + from ) . to_time . end_of_day ) ] )
end
if to
s << ( " #{ table } . #{ field } <= '%s' " % [ connection . quoted_date ( ( Date . today + to ) . to_time . end_of_day ) ] )
end
s . join ( ' AND ' )
end
2006-12-16 16:37:32 +03:00
end