2008-09-11 21:03:26 +04:00
# Redmine - project management software
2014-01-30 02:45:39 +04:00
# Copyright (C) 2006-2014 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.
2011-05-17 08:33:19 +04:00
#
2006-12-16 16:37:32 +03: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 08:33:19 +04:00
#
2006-12-16 16:37:32 +03: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 08:33:19 +04:00
class QueryColumn
2009-11-27 22:59:10 +03:00
attr_accessor :name , :sortable , :groupable , :default_order
2009-02-21 14:04:50 +03:00
include Redmine :: I18n
2011-05-17 08:33:19 +04:00
2007-10-01 12:44:17 +04:00
def initialize ( name , options = { } )
self . name = name
self . sortable = options [ :sortable ]
2009-04-26 17:09:14 +04:00
self . groupable = options [ :groupable ] || false
2009-11-16 21:07:30 +03:00
if groupable == true
self . groupable = name . to_s
end
2008-02-05 20:28:02 +03:00
self . default_order = options [ :default_order ]
2012-12-06 21:48:19 +04:00
@inline = options . key? ( :inline ) ? options [ :inline ] : true
2013-02-23 14:53:21 +04:00
@caption_key = options [ :caption ] || " field_ #{ name } " . to_sym
@frozen = options [ :frozen ]
2007-10-01 12:44:17 +04:00
end
2011-05-17 08:33:19 +04:00
2007-11-07 23:42:28 +03:00
def caption
2013-02-23 14:53:21 +04:00
@caption_key . is_a? ( Symbol ) ? l ( @caption_key ) : @caption_key
2007-11-07 23:42:28 +03:00
end
2011-05-17 08:33:19 +04:00
2009-03-12 21:06:54 +03:00
# Returns true if the column is sortable, otherwise false
def sortable?
2011-11-26 21:37:20 +04:00
! @sortable . nil?
end
2012-09-26 03:32:15 +04:00
2011-11-26 21:37:20 +04:00
def sortable
@sortable . is_a? ( Proc ) ? @sortable . call : @sortable
2009-03-12 21:06:54 +03:00
end
2011-05-17 08:33:19 +04:00
2012-12-06 21:48:19 +04:00
def inline?
@inline
end
2013-02-23 14:53:21 +04:00
def frozen?
@frozen
end
2012-12-09 18:44:28 +04:00
def value ( object )
object . send name
2009-12-02 21:57:17 +03:00
end
2011-05-17 08:33:19 +04:00
2014-04-06 15:11:52 +04:00
def value_object ( object )
object . send name
end
2011-04-06 02:18:49 +04:00
def css_classes
name
end
2007-11-07 23:42:28 +03:00
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
2012-07-24 20:28:34 +04:00
self . groupable = custom_field . group_statement || false
2012-12-06 21:48:19 +04:00
@inline = true
2007-11-07 23:42:28 +03:00
@cf = custom_field
end
2011-05-17 08:33:19 +04:00
2007-11-07 23:42:28 +03:00
def caption
@cf . name
end
2011-05-17 08:33:19 +04:00
2007-11-07 23:42:28 +03:00
def custom_field
@cf
end
2011-05-17 08:33:19 +04:00
2014-04-06 15:11:52 +04:00
def value_object ( object )
2013-07-13 13:20:11 +04:00
if custom_field . visible_by? ( object . project , User . current )
2014-04-06 15:11:52 +04:00
cv = object . custom_values . select { | v | v . custom_field_id == @cf . id }
cv . size > 1 ? cv . sort { | a , b | a . value . to_s < = > b . value . to_s } : cv . first
else
nil
end
end
def value ( object )
raw = value_object ( object )
if raw . is_a? ( Array )
raw . map { | r | @cf . cast_value ( r . value ) }
elsif raw
@cf . cast_value ( raw . value )
2013-07-13 13:20:11 +04:00
else
nil
end
2009-12-02 21:57:17 +03:00
end
2011-05-17 08:33:19 +04:00
2011-04-06 02:18:49 +04:00
def css_classes
@css_classes || = " #{ name } #{ @cf . field_format } "
end
2007-10-01 12:44:17 +04:00
end
2013-01-12 17:09:06 +04:00
class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
def initialize ( association , custom_field )
super ( custom_field )
self . name = " #{ association } .cf_ #{ custom_field . id } " . to_sym
# TODO: support sorting/grouping by association custom field
self . sortable = false
self . groupable = false
@association = association
end
2014-04-06 15:11:52 +04:00
def value_object ( object )
2013-01-12 17:09:06 +04:00
if assoc = object . send ( @association )
super ( assoc )
end
end
def css_classes
@css_classes || = " #{ @association } _cf_ #{ @cf . id } #{ @cf . field_format } "
end
end
2006-12-16 16:37:32 +03:00
class Query < ActiveRecord :: Base
2009-11-28 13:29:48 +03:00
class StatementInvalid < :: ActiveRecord :: StatementInvalid
end
2011-05-17 08:33:19 +04:00
2013-07-11 21:45:10 +04:00
VISIBILITY_PRIVATE = 0
VISIBILITY_ROLES = 1
VISIBILITY_PUBLIC = 2
2006-12-16 16:37:32 +03:00
belongs_to :project
belongs_to :user
2013-07-11 21:45:10 +04:00
has_and_belongs_to_many :roles , :join_table = > " #{ table_name_prefix } queries_roles #{ table_name_suffix } " , :foreign_key = > " query_id "
2006-12-16 16:37:32 +03:00
serialize :filters
2007-10-01 12:44:17 +04:00
serialize :column_names
2009-03-12 21:06:54 +03:00
serialize :sort_criteria , Array
2013-07-14 17:41:30 +04:00
serialize :options , Hash
2011-05-17 08:33:19 +04:00
2007-12-31 15:37:32 +03:00
attr_protected :project_id , :user_id
2011-05-17 08:33:19 +04:00
2012-02-24 22:27:38 +04:00
validates_presence_of :name
2007-07-16 21:16:49 +04:00
validates_length_of :name , :maximum = > 255
2013-07-11 21:45:10 +04:00
validates :visibility , :inclusion = > { :in = > [ VISIBILITY_PUBLIC , VISIBILITY_ROLES , VISIBILITY_PRIVATE ] }
2011-09-20 20:56:57 +04:00
validate :validate_query_filters
2013-07-11 21:45:10 +04:00
validate do | query |
errors . add ( :base , l ( :label_role_plural ) + ' ' + l ( 'activerecord.errors.messages.blank' ) ) if query . visibility == VISIBILITY_ROLES && roles . blank?
end
after_save do | query |
if query . visibility_changed? && query . visibility != VISIBILITY_ROLES
query . roles . clear
end
end
2011-05-17 08:33:19 +04:00
2012-12-09 17:19:32 +04:00
class_attribute :operators
self . operators = {
" = " = > :label_equals ,
" ! " = > :label_not_equals ,
" o " = > :label_open_issues ,
" c " = > :label_closed_issues ,
" !* " = > :label_none ,
" * " = > :label_any ,
" >= " = > :label_greater_or_equal ,
" <= " = > :label_less_or_equal ,
" >< " = > :label_between ,
" <t+ " = > :label_in_less_than ,
" >t+ " = > :label_in_more_than ,
" ><t+ " = > :label_in_the_next_days ,
" t+ " = > :label_in ,
" t " = > :label_today ,
2012-12-09 21:57:18 +04:00
" ld " = > :label_yesterday ,
2012-12-09 17:19:32 +04:00
" w " = > :label_this_week ,
2012-12-09 21:57:18 +04:00
" lw " = > :label_last_week ,
2012-12-09 22:46:02 +04:00
" l2w " = > [ :label_last_n_weeks , { :count = > 2 } ] ,
2012-12-09 21:57:18 +04:00
" m " = > :label_this_month ,
" lm " = > :label_last_month ,
" y " = > :label_this_year ,
2012-12-09 17:19:32 +04:00
" >t- " = > :label_less_than_ago ,
" <t- " = > :label_more_than_ago ,
" ><t- " = > :label_in_the_past_days ,
" t- " = > :label_ago ,
" ~ " = > :label_contains ,
" !~ " = > :label_not_contains ,
" =p " = > :label_any_issues_in_project ,
" =!p " = > :label_any_issues_not_in_project ,
" !p " = > :label_no_issues_in_project
}
class_attribute :operators_by_filter_type
self . operators_by_filter_type = {
:list = > [ " = " , " ! " ] ,
:list_status = > [ " o " , " = " , " ! " , " c " , " * " ] ,
:list_optional = > [ " = " , " ! " , " !* " , " * " ] ,
:list_subprojects = > [ " * " , " !* " , " = " ] ,
2012-12-09 21:57:18 +04:00
:date = > [ " = " , " >= " , " <= " , " >< " , " <t+ " , " >t+ " , " ><t+ " , " t+ " , " t " , " ld " , " w " , " lw " , " l2w " , " m " , " lm " , " y " , " >t- " , " <t- " , " ><t- " , " t- " , " !* " , " * " ] ,
:date_past = > [ " = " , " >= " , " <= " , " >< " , " >t- " , " <t- " , " ><t- " , " t- " , " t " , " ld " , " w " , " lw " , " l2w " , " m " , " lm " , " y " , " !* " , " * " ] ,
2012-12-09 17:19:32 +04:00
:string = > [ " = " , " ~ " , " ! " , " !~ " , " !* " , " * " ] ,
:text = > [ " ~ " , " !~ " , " !* " , " * " ] ,
:integer = > [ " = " , " >= " , " <= " , " >< " , " !* " , " * " ] ,
:float = > [ " = " , " >= " , " <= " , " >< " , " !* " , " * " ] ,
:relation = > [ " = " , " =p " , " =!p " , " !p " , " !* " , " * " ]
}
class_attribute :available_columns
2012-12-09 18:10:49 +04:00
self . available_columns = [ ]
2011-08-20 07:38:29 +04:00
2012-12-09 18:44:28 +04:00
class_attribute :queried_class
def queried_table_name
@queried_table_name || = self . class . queried_class . table_name
end
2011-12-18 17:26:20 +04:00
def initialize ( attributes = nil , * args )
2006-12-16 16:37:32 +03:00
super attributes
2008-03-30 16:29:07 +04:00
@is_for_all = project . nil?
end
2011-05-17 08:33:19 +04:00
2012-12-09 19:11:00 +04:00
# Builds the query from the given params
def build_from_params ( params )
if params [ :fields ] || params [ :f ]
self . filters = { }
add_filters ( params [ :fields ] || params [ :f ] , params [ :operators ] || params [ :op ] , params [ :values ] || params [ :v ] )
else
available_filters . keys . each do | field |
add_short_filter ( field , params [ field ] ) if params [ field ]
end
end
self . group_by = params [ :group_by ] || ( params [ :query ] && params [ :query ] [ :group_by ] )
self . column_names = params [ :c ] || ( params [ :query ] && params [ :query ] [ :column_names ] )
self
end
2012-12-09 21:57:18 +04:00
# Builds a new query from the given params and attributes
def self . build_from_params ( params , attributes = { } )
new ( attributes ) . build_from_params ( params )
end
2011-09-20 20:56:57 +04:00
def validate_query_filters
2006-12-16 16:37:32 +03:00
filters . each_key do | field |
2011-07-11 15:35:53 +04:00
if values_for ( field )
case type_for ( field )
2011-08-20 07:38:29 +04:00
when :integer
2012-07-04 22:50:09 +04:00
add_filter_error ( field , :invalid ) if values_for ( field ) . detect { | v | v . present? && ! v . match ( / ^[+-]? \ d+$ / ) }
2011-08-20 07:38:29 +04:00
when :float
2012-07-04 22:50:09 +04:00
add_filter_error ( field , :invalid ) if values_for ( field ) . detect { | v | v . present? && ! v . match ( / ^[+-]? \ d+( \ . \ d*)?$ / ) }
2011-07-11 18:13:59 +04:00
when :date , :date_past
case operator_for ( field )
when " = " , " >= " , " <= " , " >< "
2014-01-03 19:54:49 +04:00
add_filter_error ( field , :invalid ) if values_for ( field ) . detect { | v |
2014-01-03 23:42:43 +04:00
v . present? && ( ! v . match ( / \ A \ d{4}- \ d{2}- \ d{2}(T \ d{2}((:)? \ d{2}){0,2}(Z| \ d{2}:? \ d{2})?)? \ z / ) || parse_date ( v ) . nil? )
2014-01-03 19:54:49 +04:00
}
2012-10-30 12:21:15 +04:00
when " >t- " , " <t- " , " t- " , " >t+ " , " <t+ " , " t+ " , " ><t+ " , " ><t- "
2011-12-26 17:34:50 +04:00
add_filter_error ( field , :invalid ) if values_for ( field ) . detect { | v | v . present? && ! v . match ( / ^ \ d+$ / ) }
2011-07-11 18:13:59 +04:00
end
2011-07-11 15:35:53 +04:00
end
2011-07-10 22:34:49 +04:00
end
2011-08-20 07:38:29 +04:00
2011-12-26 17:34:50 +04:00
add_filter_error ( field , :blank ) unless
2006-12-16 16:37:32 +03:00
# filter requires one or more values
2011-05-17 08:33:19 +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
2012-12-09 21:57:18 +04:00
[ " o " , " c " , " !* " , " * " , " t " , " ld " , " w " , " lw " , " l2w " , " m " , " lm " , " y " ] . include? operator_for ( field )
2006-12-16 16:37:32 +03:00
end if filters
end
2011-08-20 07:38:29 +04:00
2011-12-26 17:34:50 +04:00
def add_filter_error ( field , message )
m = label_for ( field ) + " " + l ( message , :scope = > 'activerecord.errors.messages' )
errors . add ( :base , m )
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
2013-07-11 21:45:10 +04:00
return true if user . admin? || ( is_private? && self . user_id == user . id )
2008-03-30 16:29:07 +04:00
# Members can not edit public queries that are for all project (only admin is allowed to)
2013-07-11 21:45:10 +04:00
is_public? && ! @is_for_all && user . allowed_to? ( :manage_public_queries , project )
2007-06-23 17:49:29 +04:00
end
2011-05-17 08:33:19 +04:00
2012-07-05 16:20:07 +04:00
def trackers
2012-12-03 00:23:48 +04:00
@trackers || = project . nil? ? Tracker . sorted . all : project . rolled_up_trackers
2012-07-05 16:20:07 +04:00
end
2012-08-07 20:41:46 +04:00
# Returns a hash of localized labels for all filter operators
def self . operators_labels
2012-12-09 21:57:18 +04:00
operators . inject ( { } ) { | h , operator | h [ operator . first ] = l ( * operator . last ) ; h }
2012-08-07 20:41:46 +04:00
end
2012-09-26 03:31:49 +04:00
# Returns a representation of the available filters for JSON serialization
2012-08-07 20:41:46 +04:00
def available_filters_as_json
json = { }
available_filters . each do | field , options |
json [ field ] = options . slice ( :type , :name , :values ) . stringify_keys
end
json
end
2012-09-29 16:57:38 +04:00
def all_projects
@all_projects || = Project . visible . all
end
def all_projects_values
return @all_projects_values if @all_projects_values
values = [ ]
Project . project_tree ( all_projects ) do | p , level |
prefix = ( level > 0 ? ( '--' * level + ' ' ) : '' )
values << [ " #{ prefix } #{ p . name } " , p . id . to_s ]
end
@all_projects_values = values
end
2013-02-15 00:37:17 +04:00
# Adds available filters
def initialize_available_filters
# implemented by sub-classes
end
protected :initialize_available_filters
# Adds an available filter
def add_available_filter ( field , options )
@available_filters || = ActiveSupport :: OrderedHash . new
@available_filters [ field ] = options
@available_filters
end
# Removes an available filter
def delete_available_filter ( field )
if @available_filters
@available_filters . delete ( field )
end
end
# Return a hash of available filters
def available_filters
unless @available_filters
initialize_available_filters
@available_filters . each do | field , options |
options [ :name ] || = l ( options [ :label ] || " field_ #{ field } " . gsub ( / _id$ / , '' ) )
end
end
@available_filters
end
2012-12-09 21:57:18 +04:00
def add_filter ( field , operator , values = nil )
2006-12-16 16:37:32 +03:00
# values must be an array
2011-07-10 21:29:29 +04:00
return unless values . nil? || values . is_a? ( Array )
2006-12-16 16:37:32 +03:00
# check if field is defined as an available filter
if available_filters . has_key? field
filter_options = available_filters [ field ]
2011-07-10 22:34:49 +04:00
filters [ field ] = { :operator = > operator , :values = > ( values || [ '' ] ) }
2006-12-16 16:37:32 +03:00
end
end
2011-05-17 08:33:19 +04:00
2006-12-16 16:37:32 +03:00
def add_short_filter ( field , expression )
2011-10-13 03:45:47 +04:00
return unless expression && available_filters . has_key? ( field )
field_type = available_filters [ field ] [ :type ]
2012-12-09 17:19:32 +04:00
operators_by_filter_type [ field_type ] . sort . reverse . detect do | operator |
2011-10-13 03:45:47 +04:00
next unless expression =~ / ^ #{ Regexp . escape ( operator ) } (.*)$ /
2013-01-27 16:23:02 +04:00
values = $1
add_filter field , operator , values . present? ? values . split ( '|' ) : [ '' ]
2011-10-13 03:45:47 +04:00
end || add_filter ( field , '=' , expression . split ( '|' ) )
2006-12-16 16:37:32 +03:00
end
2010-04-19 19:08:28 +04:00
# Add multiple filters using +add_filter+
def add_filters ( fields , operators , values )
2011-07-10 21:29:29 +04:00
if fields . is_a? ( Array ) && operators . is_a? ( Hash ) && ( values . nil? || values . is_a? ( Hash ) )
2010-11-07 18:38:51 +03:00
fields . each do | field |
2011-07-10 21:29:29 +04:00
add_filter ( field , operators [ field ] , values && values [ field ] )
2010-11-07 18:38:51 +03:00
end
2010-04-19 19:08:28 +04:00
end
end
2011-05-17 08:33:19 +04:00
2006-12-16 16:37:32 +03:00
def has_filter? ( field )
filters and filters [ field ]
end
2011-08-20 07:38:29 +04:00
2011-07-10 21:29:29 +04:00
def type_for ( field )
available_filters [ field ] [ :type ] if available_filters . has_key? ( field )
end
2011-05-17 08:33:19 +04:00
2006-12-16 16:37:32 +03:00
def operator_for ( field )
has_filter? ( field ) ? filters [ field ] [ :operator ] : nil
end
2011-05-17 08:33:19 +04:00
2006-12-16 16:37:32 +03:00
def values_for ( field )
has_filter? ( field ) ? filters [ field ] [ :values ] : nil
end
2011-08-20 07:38:29 +04:00
2011-07-10 12:00:25 +04:00
def value_for ( field , index = 0 )
( values_for ( field ) || [ ] ) [ index ]
end
2011-05-17 08:33:19 +04:00
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 )
2011-12-26 17:34:50 +04:00
label || = l ( " field_ #{ field . to_s . gsub ( / _id$ / , '' ) } " , :default = > field )
2007-04-17 14:53:20 +04:00
end
2007-10-01 12:44:17 +04:00
2010-04-20 19:42:52 +04:00
def self . add_available_column ( column )
self . available_columns << ( column ) if column . is_a? ( QueryColumn )
end
2011-05-17 08:33:19 +04:00
2009-04-26 17:09:14 +04: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 18:42:03 +03:00
# Returns a Hash of columns and the key for sorting
def sortable_columns
2012-12-09 18:44:28 +04:00
available_columns . inject ( { } ) { | h , column |
h [ column . name . to_s ] = column . sortable
h
}
2010-03-19 18:42:03 +03:00
end
2011-05-17 08:33:19 +04:00
2007-10-01 12:44:17 +04:00
def columns
2011-09-26 21:44:20 +04:00
# preserve the column_names order
2013-02-23 14:53:21 +04:00
cols = ( has_default_columns? ? default_columns_names : column_names ) . collect do | name |
2011-09-26 21:44:20 +04:00
available_columns . find { | col | col . name == name }
end . compact
2013-02-23 14:53:21 +04:00
available_columns . select ( & :frozen? ) | cols
2011-09-26 21:44:20 +04:00
end
2012-12-06 21:48:19 +04:00
def inline_columns
columns . select ( & :inline? )
end
def block_columns
columns . reject ( & :inline? )
end
def available_inline_columns
available_columns . select ( & :inline? )
end
def available_block_columns
available_columns . reject ( & :inline? )
end
2011-09-26 21:44:20 +04:00
def default_columns_names
2012-12-09 18:44:28 +04:00
[ ]
2007-10-01 12:44:17 +04:00
end
2011-05-17 08:33:19 +04:00
2007-10-01 12:44:17 +04:00
def column_names = ( names )
2009-11-28 14:34:12 +03: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 21:44:20 +04:00
if names == default_columns_names
2009-11-28 14:34:12 +03:00
names = nil
end
end
2007-10-01 12:44:17 +04:00
write_attribute ( :column_names , names )
end
2011-05-17 08:33:19 +04:00
2007-10-01 12:44:17 +04:00
def has_column? ( column )
2011-12-04 20:43:32 +04:00
column_names && column_names . include? ( column . is_a? ( QueryColumn ) ? column . name : column )
2007-10-01 12:44:17 +04:00
end
2011-05-17 08:33:19 +04:00
2013-10-15 20:38:17 +04:00
def has_custom_field_column?
columns . any? { | column | column . is_a? QueryCustomFieldColumn }
end
2007-10-01 14:44:45 +04:00
def has_default_columns?
column_names . nil? || column_names . empty?
end
2011-05-17 08:33:19 +04:00
2009-03-12 21:06:54 +03:00
def sort_criteria = ( arg )
c = [ ]
if arg . is_a? ( Hash )
arg = arg . keys . sort . collect { | k | arg [ k ] }
end
2012-10-29 22:32:41 +04:00
c = arg . select { | k , o | ! k . to_s . blank? } . slice ( 0 , 3 ) . collect { | k , o | [ k . to_s , ( o == 'desc' || o == false ) ? 'desc' : 'asc' ] }
2009-03-12 21:06:54 +03:00
write_attribute ( :sort_criteria , c )
end
2011-05-17 08:33:19 +04:00
2009-03-12 21:06:54 +03:00
def sort_criteria
read_attribute ( :sort_criteria ) || [ ]
end
2011-05-17 08:33:19 +04:00
2009-03-12 21:06:54 +03:00
def sort_criteria_key ( arg )
sort_criteria && sort_criteria [ arg ] && sort_criteria [ arg ] . first
end
2011-05-17 08:33:19 +04:00
2009-03-12 21:06:54 +03:00
def sort_criteria_order ( arg )
sort_criteria && sort_criteria [ arg ] && sort_criteria [ arg ] . last
end
2011-05-17 08:33:19 +04:00
2012-10-29 22:32:41 +04:00
def sort_criteria_order_for ( key )
sort_criteria . detect { | k , order | key . to_s == k } . try ( :last )
end
2009-04-26 17:09:14 +04:00
# Returns the SQL sort order that should be prepended for grouping
def group_by_sort_order
if grouped? && ( column = group_by_column )
2012-10-29 22:32:41 +04:00
order = sort_criteria_order_for ( column . name ) || column . default_order
2009-04-26 17:09:14 +04:00
column . sortable . is_a? ( Array ) ?
2012-10-29 22:32:41 +04:00
column . sortable . collect { | s | " #{ s } #{ order } " } . join ( ',' ) :
" #{ column . sortable } #{ order } "
2009-04-26 17:09:14 +04:00
end
end
2011-05-17 08:33:19 +04:00
2009-04-26 17:09:14 +04:00
# Returns true if the query is a grouped query
def grouped?
2010-12-22 00:46:54 +03:00
! group_by_column . nil?
2009-04-26 17:09:14 +04:00
end
2011-05-17 08:33:19 +04:00
2009-04-26 17:09:14 +04:00
def group_by_column
2010-12-22 00:46:54 +03:00
groupable_columns . detect { | c | c . groupable && c . name . to_s == group_by }
2009-04-26 17:09:14 +04:00
end
2011-05-17 08:33:19 +04:00
2009-11-16 21:07:30 +03:00
def group_by_statement
2010-12-22 00:46:54 +03:00
group_by_column . try ( :groupable )
2009-11-16 21:07:30 +03:00
end
2011-05-17 08:33:19 +04:00
2008-09-11 21:03:26 +04:00
def project_statement
2008-05-14 22:19:37 +04:00
project_clauses = [ ]
2011-11-23 22:32:21 +04: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
2011-04-12 20:53:39 +04:00
project_clauses . any? ? project_clauses . join ( ' AND ' ) : nil
2008-09-11 21:03:26 +04:00
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?
2009-02-12 20:35:57 +03:00
operator = operator_for ( field )
2011-05-17 08:33:19 +04:00
2014-04-07 12:07:59 +04:00
# "me" value substitution
2012-12-09 23:18:57 +04:00
if %w( assigned_to_id author_id user_id watcher_id ) . include? ( field )
2011-07-31 14:22:36 +04: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 20:35:57 +03:00
end
2011-05-17 08:33:19 +04:00
2012-01-13 22:16:15 +04:00
if field == 'project_id'
if v . delete ( 'mine' )
v += User . current . memberships . map ( & :project_id ) . map ( & :to_s )
end
end
2012-08-07 23:17:59 +04:00
if field =~ / cf_( \ d+)$ /
2007-04-17 14:53:20 +04:00
# custom field
2011-07-20 21:44:10 +04: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 14:53:20 +04:00
else
# regular field
2012-12-09 18:44:28 +04:00
filters_clauses << '(' + sql_for_field ( field , operator , v , queried_table_name , field ) + ')'
2007-04-17 14:53:20 +04:00
end
2006-12-16 16:37:32 +03:00
end if filters and valid?
2011-05-17 08:33:19 +04:00
2013-07-13 13:20:11 +04:00
if ( c = group_by_column ) && c . is_a? ( QueryCustomFieldColumn )
# Excludes results for which the grouped custom field is not visible
filters_clauses << c . custom_field . visibility_by_project_condition
end
2011-04-12 20:53:39 +04:00
filters_clauses << project_statement
filters_clauses . reject! ( & :blank? )
2011-05-17 08:33:19 +04:00
2011-04-12 20:53:39 +04:00
filters_clauses . any? ? filters_clauses . join ( ' AND ' ) : nil
2006-12-16 16:37:32 +03:00
end
2011-05-17 08:33:19 +04:00
2011-07-20 21:44:10 +04:00
private
2011-08-20 07:38:29 +04:00
2011-07-20 21:44:10 +04:00
def sql_for_custom_field ( field , operator , value , custom_field_id )
db_table = CustomValue . table_name
db_field = 'value'
2012-01-13 23:38:06 +04:00
filter = @available_filters [ field ]
2012-08-07 23:17:59 +04:00
return nil unless filter
2013-12-14 12:22:43 +04:00
if filter [ :field ] . format . target_class && filter [ :field ] . format . target_class < = User
2012-01-13 23:38:06 +04:00
if value . delete ( 'me' )
value . push User . current . id . to_s
end
end
2012-01-30 00:51:48 +04:00
not_in = nil
if operator == '!'
# Makes ! operator work for custom fields with multiple values
operator = '='
not_in = 'NOT'
end
2012-08-07 23:17:59 +04:00
customized_key = " id "
2012-12-09 18:44:28 +04:00
customized_class = queried_class
2012-08-07 23:17:59 +04:00
if field =~ / ^(.+) \ .cf_ /
assoc = $1
customized_key = " #{ assoc } _id "
2012-12-09 18:44:28 +04:00
customized_class = queried_class . reflect_on_association ( assoc . to_sym ) . klass . base_class rescue nil
raise " Unknown #{ queried_class . name } association #{ assoc } " unless customized_class
2012-08-07 23:17:59 +04:00
end
2013-05-16 20:29:59 +04:00
where = sql_for_field ( field , operator , value , db_table , db_field , true )
if operator =~ / [<>] /
where = " ( #{ where } ) AND #{ db_table } . #{ db_field } <> '' "
end
2013-07-13 13:20:11 +04:00
" #{ queried_table_name } . #{ customized_key } #{ not_in } IN ( " +
" SELECT #{ customized_class . table_name } .id FROM #{ customized_class . table_name } " +
" LEFT OUTER JOIN #{ db_table } ON #{ db_table } .customized_type=' #{ customized_class } ' AND #{ db_table } .customized_id= #{ customized_class . table_name } .id AND #{ db_table } .custom_field_id= #{ custom_field_id } " +
" WHERE ( #{ where } ) AND ( #{ filter [ :field ] . visibility_by_project_condition } )) "
2011-07-20 21:44:10 +04:00
end
2011-08-20 07:38:29 +04:00
2009-02-12 20:35:57 +03: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-06 01:56:03 +03:00
sql = ''
2009-02-12 20:35:57 +03:00
case operator
2008-12-06 01:56:03 +03:00
when " = "
2011-07-11 15:35:53 +04:00
if value . any?
case type_for ( field )
when :date , :date_past
2014-01-03 19:54:49 +04:00
sql = date_clause ( db_table , db_field , parse_date ( value . first ) , parse_date ( value . first ) )
2011-07-11 15:35:53 +04:00
when :integer
2011-12-06 00:45:45 +04:00
if is_custom_filter
2013-01-03 17:33:16 +04:00
sql = " ( #{ db_table } . #{ db_field } <> '' AND CAST(CASE #{ db_table } . #{ db_field } WHEN '' THEN '0' ELSE #{ db_table } . #{ db_field } END AS decimal(30,3)) = #{ value . first . to_i } ) "
2011-12-06 00:45:45 +04:00
else
sql = " #{ db_table } . #{ db_field } = #{ value . first . to_i } "
end
2011-07-11 15:35:53 +04:00
when :float
2011-12-06 00:45:45 +04:00
if is_custom_filter
2013-01-03 17:33:16 +04:00
sql = " ( #{ db_table } . #{ db_field } <> '' AND CAST(CASE #{ db_table } . #{ db_field } WHEN '' THEN '0' ELSE #{ db_table } . #{ db_field } END AS decimal(30,3)) BETWEEN #{ value . first . to_f - 1 e - 5 } AND #{ value . first . to_f + 1 e - 5 } ) "
2011-12-06 00:45:45 +04:00
else
sql = " #{ db_table } . #{ db_field } BETWEEN #{ value . first . to_f - 1 e - 5 } AND #{ value . first . to_f + 1 e - 5 } "
end
2011-07-10 21:29:29 +04:00
else
2014-01-27 11:21:47 +04:00
sql = " #{ db_table } . #{ db_field } IN ( " + value . collect { | val | " ' #{ connection . quote_string ( val ) } ' " } . join ( " , " ) + " ) "
2011-07-10 21:29:29 +04:00
end
2011-07-11 15:35:53 +04:00
else
# IN an empty set
sql = " 1=0 "
2011-02-20 16:03:32 +03:00
end
2008-12-06 01:56:03 +03:00
when " ! "
2011-02-20 16:03:32 +03:00
if value . any?
2014-01-27 11:21:47 +04:00
sql = " ( #{ db_table } . #{ db_field } IS NULL OR #{ db_table } . #{ db_field } NOT IN ( " + value . collect { | val | " ' #{ connection . quote_string ( val ) } ' " } . join ( " , " ) + " )) "
2011-02-20 16:03:32 +03:00
else
# NOT IN an empty set
sql = " 1=1 "
end
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 " >= "
2011-07-10 21:29:29 +04:00
if [ :date , :date_past ] . include? ( type_for ( field ) )
2014-01-03 19:54:49 +04:00
sql = date_clause ( db_table , db_field , parse_date ( value . first ) , nil )
2011-07-10 01:34:35 +04:00
else
2011-07-10 21:29:29 +04:00
if is_custom_filter
2013-01-03 17:33:16 +04:00
sql = " ( #{ db_table } . #{ db_field } <> '' AND CAST(CASE #{ db_table } . #{ db_field } WHEN '' THEN '0' ELSE #{ db_table } . #{ db_field } END AS decimal(30,3)) >= #{ value . first . to_f } ) "
2011-07-10 21:29:29 +04:00
else
2011-07-10 22:09:40 +04:00
sql = " #{ db_table } . #{ db_field } >= #{ value . first . to_f } "
2011-07-10 21:29:29 +04:00
end
2011-07-10 01:34:35 +04:00
end
2008-12-06 01:56:03 +03:00
when " <= "
2011-07-10 21:29:29 +04:00
if [ :date , :date_past ] . include? ( type_for ( field ) )
2014-01-03 19:54:49 +04:00
sql = date_clause ( db_table , db_field , nil , parse_date ( value . first ) )
2011-07-10 01:34:35 +04:00
else
2011-07-10 21:29:29 +04:00
if is_custom_filter
2013-01-03 17:33:16 +04:00
sql = " ( #{ db_table } . #{ db_field } <> '' AND CAST(CASE #{ db_table } . #{ db_field } WHEN '' THEN '0' ELSE #{ db_table } . #{ db_field } END AS decimal(30,3)) <= #{ value . first . to_f } ) "
2011-07-10 21:29:29 +04:00
else
2011-07-10 22:09:40 +04:00
sql = " #{ db_table } . #{ db_field } <= #{ value . first . to_f } "
2011-07-10 21:29:29 +04:00
end
2011-07-10 01:34:35 +04:00
end
2011-07-10 12:00:25 +04:00
when " >< "
2011-07-10 21:29:29 +04:00
if [ :date , :date_past ] . include? ( type_for ( field ) )
2014-01-03 19:54:49 +04:00
sql = date_clause ( db_table , db_field , parse_date ( value [ 0 ] ) , parse_date ( value [ 1 ] ) )
2011-07-10 12:00:25 +04:00
else
2011-07-10 21:29:29 +04:00
if is_custom_filter
2013-01-03 17:33:16 +04:00
sql = " ( #{ db_table } . #{ db_field } <> '' AND CAST(CASE #{ db_table } . #{ db_field } WHEN '' THEN '0' ELSE #{ db_table } . #{ db_field } END AS decimal(30,3)) BETWEEN #{ value [ 0 ] . to_f } AND #{ value [ 1 ] . to_f } ) "
2011-07-10 21:29:29 +04:00
else
2011-07-10 22:09:40 +04:00
sql = " #{ db_table } . #{ db_field } BETWEEN #{ value [ 0 ] . to_f } AND #{ value [ 1 ] . to_f } "
2011-07-10 21:29:29 +04:00
end
2011-07-10 12:00:25 +04:00
end
2008-12-06 01:56:03 +03:00
when " o "
2014-01-27 11:21:47 +04:00
sql = " #{ queried_table_name } .status_id IN (SELECT id FROM #{ IssueStatus . table_name } WHERE is_closed= #{ connection . quoted_false } ) " if field == " status_id "
2008-12-06 01:56:03 +03:00
when " c "
2014-01-27 11:21:47 +04:00
sql = " #{ queried_table_name } .status_id IN (SELECT id FROM #{ IssueStatus . table_name } WHERE is_closed= #{ connection . quoted_true } ) " if field == " status_id "
2012-10-30 12:21:15 +04:00
when " ><t- "
# between today - n days and today
2011-07-10 21:29:29 +04:00
sql = relative_date_clause ( db_table , db_field , - value . first . to_i , 0 )
2012-10-30 12:21:15 +04:00
when " >t- "
# >= today - n days
sql = relative_date_clause ( db_table , db_field , - value . first . to_i , nil )
2008-12-06 01:56:03 +03:00
when " <t- "
2012-10-30 12:21:15 +04:00
# <= today - n days
2011-07-10 21:29:29 +04:00
sql = relative_date_clause ( db_table , db_field , nil , - value . first . to_i )
2008-12-06 01:56:03 +03:00
when " t- "
2012-10-30 12:21:15 +04:00
# = n days in past
2011-07-10 21:29:29 +04:00
sql = relative_date_clause ( db_table , db_field , - value . first . to_i , - value . first . to_i )
2012-10-30 12:21:15 +04:00
when " ><t+ "
# between today and today + n days
sql = relative_date_clause ( db_table , db_field , 0 , value . first . to_i )
2008-12-06 01:56:03 +03:00
when " >t+ "
2012-10-30 12:21:15 +04:00
# >= today + n days
2011-07-10 21:29:29 +04:00
sql = relative_date_clause ( db_table , db_field , value . first . to_i , nil )
2008-12-06 01:56:03 +03:00
when " <t+ "
2012-10-30 12:21:15 +04:00
# <= today + n days
sql = relative_date_clause ( db_table , db_field , nil , value . first . to_i )
2008-12-06 01:56:03 +03:00
when " t+ "
2012-10-30 12:21:15 +04:00
# = today + n days
2011-07-10 21:29:29 +04:00
sql = relative_date_clause ( db_table , db_field , value . first . to_i , value . first . to_i )
2008-12-06 01:56:03 +03:00
when " t "
2012-10-30 12:21:15 +04:00
# = today
2011-07-10 21:29:29 +04:00
sql = relative_date_clause ( db_table , db_field , 0 , 0 )
2012-12-09 21:57:18 +04:00
when " ld "
# = yesterday
sql = relative_date_clause ( db_table , db_field , - 1 , - 1 )
2008-12-06 01:56:03 +03:00
when " w "
2012-10-30 12:21:15 +04:00
# = this week
2011-04-29 15:28:27 +04: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 21:29:29 +04:00
sql = relative_date_clause ( db_table , db_field , - days_ago , - days_ago + 6 )
2012-12-09 21:57:18 +04:00
when " lw "
# = last week
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 = relative_date_clause ( db_table , db_field , - days_ago - 7 , - days_ago - 1 )
when " l2w "
# = last 2 weeks
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 = relative_date_clause ( db_table , db_field , - days_ago - 14 , - days_ago - 1 )
when " m "
# = this month
date = Date . today
sql = date_clause ( db_table , db_field , date . beginning_of_month , date . end_of_month )
when " lm "
# = last month
date = Date . today . prev_month
sql = date_clause ( db_table , db_field , date . beginning_of_month , date . end_of_month )
when " y "
# = this year
date = Date . today
sql = date_clause ( db_table , db_field , date . beginning_of_year , date . end_of_year )
2008-12-06 01:56:03 +03:00
when " ~ "
2014-01-27 11:21:47 +04:00
sql = " LOWER( #{ db_table } . #{ db_field } ) LIKE '% #{ connection . quote_string ( value . first . to_s . downcase ) } %' "
2008-12-06 01:56:03 +03:00
when " !~ "
2014-01-27 11:21:47 +04:00
sql = " LOWER( #{ db_table } . #{ db_field } ) NOT LIKE '% #{ connection . quote_string ( value . first . to_s . downcase ) } %' "
2011-07-10 12:00:25 +04:00
else
raise " Unknown query operator #{ operator } "
2008-12-06 01:56:03 +03:00
end
2011-05-17 08:33:19 +04:00
2008-12-06 01:56:08 +03:00
return sql
2008-12-06 01:56:03 +03:00
end
2011-05-17 08:33:19 +04:00
2013-06-01 14:56:12 +04:00
# Adds a filter for the given custom field
def add_custom_field_filter ( field , assoc = nil )
2013-12-14 12:22:43 +04:00
options = field . format . query_filter_options ( field , self )
if field . format . target_class && field . format . target_class < = User
if options [ :values ] . is_a? ( Array ) && User . current . logged?
options [ :values ] . unshift [ " << #{ l ( :label_me ) } >> " , " me " ]
2012-08-07 23:17:59 +04:00
end
2013-06-01 14:56:12 +04:00
end
2013-12-14 12:22:43 +04:00
2013-06-01 14:56:12 +04:00
filter_id = " cf_ #{ field . id } "
filter_name = field . name
if assoc . present?
filter_id = " #{ assoc } . #{ filter_id } "
filter_name = l ( " label_attribute_of_ #{ assoc } " , :name = > filter_name )
2012-08-07 23:17:59 +04:00
end
2013-06-01 14:56:12 +04:00
add_available_filter filter_id , options . merge ( {
:name = > filter_name ,
:field = > field
} )
2012-08-07 23:17:59 +04:00
end
2013-06-01 14:56:12 +04:00
# Adds filters for the given custom fields scope
def add_custom_fields_filters ( scope , assoc = nil )
2013-07-13 13:20:11 +04:00
scope . visible . where ( :is_filter = > true ) . sorted . each do | field |
2013-06-01 14:56:12 +04:00
add_custom_field_filter ( field , assoc )
end
end
# Adds filters for the given associations custom fields
2012-08-07 23:17:59 +04:00
def add_associations_custom_fields_filters ( * associations )
2013-07-13 13:20:11 +04:00
fields_by_class = CustomField . visible . where ( :is_filter = > true ) . group_by ( & :class )
2012-08-07 23:17:59 +04:00
associations . each do | assoc |
2012-12-09 18:44:28 +04:00
association_klass = queried_class . reflect_on_association ( assoc ) . klass
2012-08-07 23:17:59 +04:00
fields_by_class . each do | field_class , fields |
if field_class . customized_class < = association_klass
2013-06-01 14:56:12 +04:00
fields . sort . each do | field |
add_custom_field_filter ( field , assoc )
end
2012-08-07 23:17:59 +04:00
end
end
2008-06-22 19:35:11 +04:00
end
end
2011-08-20 07:38:29 +04:00
2008-11-23 19:40:35 +03:00
# Returns a SQL clause for a date or datetime field.
2011-07-10 21:29:29 +04:00
def date_clause ( table , field , from , to )
2008-11-23 19:40:35 +03:00
s = [ ]
if from
2014-01-03 19:54:49 +04:00
if from . is_a? ( Date )
2014-01-03 20:36:04 +04:00
from = Time . local ( from . year , from . month , from . day ) . yesterday . end_of_day
else
from = from - 1 # second
2014-01-03 19:54:49 +04:00
end
2012-05-26 16:07:56 +04:00
if self . class . default_timezone == :utc
2014-01-03 19:54:49 +04:00
from = from . utc
2012-05-26 16:07:56 +04:00
end
2014-01-27 11:21:47 +04:00
s << ( " #{ table } . #{ field } > '%s' " % [ connection . quoted_date ( from ) ] )
2008-11-23 19:40:35 +03:00
end
if to
2014-01-03 19:54:49 +04:00
if to . is_a? ( Date )
to = Time . local ( to . year , to . month , to . day ) . end_of_day
end
2012-05-26 16:07:56 +04:00
if self . class . default_timezone == :utc
2014-01-03 19:54:49 +04:00
to = to . utc
2012-05-26 16:07:56 +04:00
end
2014-01-27 11:21:47 +04:00
s << ( " #{ table } . #{ field } <= '%s' " % [ connection . quoted_date ( to ) ] )
2008-11-23 19:40:35 +03:00
end
s . join ( ' AND ' )
end
2011-08-20 07:38:29 +04:00
2011-07-10 21:29:29 +04: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
2012-07-24 21:39:30 +04:00
2014-01-03 19:54:49 +04:00
# Returns a Date or Time from the given filter value
def parse_date ( arg )
if arg . to_s =~ / \ A \ d{4}- \ d{2}- \ d{2}T /
Time . parse ( arg ) rescue nil
else
Date . parse ( arg ) rescue nil
end
end
2012-07-24 21:39:30 +04:00
# Additional joins required for the given sort options
def joins_for_order_statement ( order_options )
joins = [ ]
if order_options
if order_options . include? ( 'authors' )
2012-12-09 18:44:28 +04:00
joins << " LEFT OUTER JOIN #{ User . table_name } authors ON authors.id = #{ queried_table_name } .author_id "
2012-07-24 21:39:30 +04:00
end
order_options . scan ( / cf_ \ d+ / ) . uniq . each do | name |
column = available_columns . detect { | c | c . name . to_s == name }
2012-07-24 21:42:45 +04:00
join = column && column . custom_field . join_for_order_statement
2012-07-24 21:39:30 +04:00
if join
joins << join
end
end
end
joins . any? ? joins . join ( ' ' ) : nil
end
2006-12-16 16:37:32 +03:00
end