2012-12-09 18:10:49 +04:00
# Redmine - project management software
2013-01-12 13:29:31 +04:00
# Copyright (C) 2006-2013 Jean-Philippe Lang
2012-12-09 18:10:49 +04: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.
class IssueQuery < Query
2012-12-09 18:44:28 +04:00
self . queried_class = Issue
2012-12-09 18:10:49 +04:00
self . available_columns = [
2013-02-23 14:53:21 +04:00
QueryColumn . new ( :id , :sortable = > " #{ Issue . table_name } .id " , :default_order = > 'desc' , :caption = > '#' , :frozen = > true ) ,
2012-12-09 18:10:49 +04:00
QueryColumn . new ( :project , :sortable = > " #{ Project . table_name } .name " , :groupable = > true ) ,
QueryColumn . new ( :tracker , :sortable = > " #{ Tracker . table_name } .position " , :groupable = > true ) ,
QueryColumn . new ( :parent , :sortable = > [ " #{ Issue . table_name } .root_id " , " #{ Issue . table_name } .lft ASC " ] , :default_order = > 'desc' , :caption = > :field_parent_issue ) ,
QueryColumn . new ( :status , :sortable = > " #{ IssueStatus . table_name } .position " , :groupable = > true ) ,
QueryColumn . new ( :priority , :sortable = > " #{ IssuePriority . table_name } .position " , :default_order = > 'desc' , :groupable = > true ) ,
QueryColumn . new ( :subject , :sortable = > " #{ Issue . table_name } .subject " ) ,
QueryColumn . new ( :author , :sortable = > lambda { User . fields_for_order_statement ( " authors " ) } , :groupable = > true ) ,
QueryColumn . new ( :assigned_to , :sortable = > lambda { User . fields_for_order_statement } , :groupable = > true ) ,
QueryColumn . new ( :updated_on , :sortable = > " #{ Issue . table_name } .updated_on " , :default_order = > 'desc' ) ,
QueryColumn . new ( :category , :sortable = > " #{ IssueCategory . table_name } .name " , :groupable = > true ) ,
QueryColumn . new ( :fixed_version , :sortable = > lambda { Version . fields_for_order_statement } , :groupable = > true ) ,
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 " , :groupable = > true ) ,
QueryColumn . new ( :created_on , :sortable = > " #{ Issue . table_name } .created_on " , :default_order = > 'desc' ) ,
2013-02-16 13:39:52 +04:00
QueryColumn . new ( :closed_on , :sortable = > " #{ Issue . table_name } .closed_on " , :default_order = > 'desc' ) ,
2012-12-09 18:10:49 +04:00
QueryColumn . new ( :relations , :caption = > :label_related_issues ) ,
QueryColumn . new ( :description , :inline = > false )
]
scope :visible , lambda { | * args |
user = args . shift || User . current
base = Project . allowed_to_condition ( user , :view_issues , * args )
2013-07-11 21:45:10 +04:00
scope = includes ( :project ) . where ( " #{ table_name } .project_id IS NULL OR ( #{ base } ) " )
2012-12-09 18:10:49 +04:00
2013-07-11 21:45:10 +04:00
if user . admin?
scope . where ( " #{ table_name } .visibility <> ? OR #{ table_name } .user_id = ? " , VISIBILITY_PRIVATE , user . id )
elsif user . memberships . any?
scope . where ( " #{ table_name } .visibility = ? " +
" OR ( #{ table_name } .visibility = ? AND #{ table_name } .id IN ( " +
" SELECT DISTINCT q.id FROM #{ table_name } q " +
" INNER JOIN #{ table_name_prefix } queries_roles #{ table_name_suffix } qr on qr.query_id = q.id " +
" INNER JOIN #{ MemberRole . table_name } mr ON mr.role_id = qr.role_id " +
" INNER JOIN #{ Member . table_name } m ON m.id = mr.member_id AND m.user_id = ? " +
" WHERE q.project_id IS NULL OR q.project_id = m.project_id)) " +
" OR #{ table_name } .user_id = ? " ,
VISIBILITY_PUBLIC , VISIBILITY_ROLES , user . id , user . id )
elsif user . logged?
scope . where ( " #{ table_name } .visibility = ? OR #{ table_name } .user_id = ? " , VISIBILITY_PUBLIC , user . id )
else
scope . where ( " #{ table_name } .visibility = ? " , VISIBILITY_PUBLIC )
end
2012-12-09 18:10:49 +04:00
}
def initialize ( attributes = nil , * args )
super attributes
self . filters || = { 'status_id' = > { :operator = > " o " , :values = > [ " " ] } }
end
# Returns true if the query is visible to +user+ or the current user.
def visible? ( user = User . current )
2013-07-11 21:45:10 +04:00
return true if user . admin?
return false unless project . nil? || user . allowed_to? ( :view_issues , project )
case visibility
when VISIBILITY_PUBLIC
true
when VISIBILITY_ROLES
if project
( user . roles_for_project ( project ) & roles ) . any?
else
Member . where ( :user_id = > user . id ) . joins ( :roles ) . where ( :member_roles = > { :role_id = > roles . map ( & :id ) } ) . any?
end
else
user == self . user
end
end
def is_private?
visibility == VISIBILITY_PRIVATE
end
def is_public?
! is_private?
2012-12-09 18:10:49 +04:00
end
2013-07-14 17:41:30 +04:00
def draw_relations
r = options [ :draw_relations ]
r . nil? || r == '1'
end
def draw_relations = ( arg )
options [ :draw_relations ] = ( arg == '0' ? '0' : nil )
end
def draw_progress_line
r = options [ :draw_progress_line ]
r == '1'
end
def draw_progress_line = ( arg )
options [ :draw_progress_line ] = ( arg == '1' ? '1' : nil )
end
def build_from_params ( params )
super
self . draw_relations = params [ :draw_relations ] || ( params [ :query ] && params [ :query ] [ :draw_relations ] )
self . draw_progress_line = params [ :draw_progress_line ] || ( params [ :query ] && params [ :query ] [ :draw_progress_line ] )
self
end
2013-02-15 00:37:17 +04:00
def initialize_available_filters
2012-12-09 18:10:49 +04:00
principals = [ ]
2013-02-15 00:37:17 +04:00
subprojects = [ ]
versions = [ ]
categories = [ ]
issue_custom_fields = [ ]
2013-04-19 11:18:38 +04:00
2012-12-09 18:10:49 +04:00
if project
principals += project . principals . sort
unless project . leaf?
subprojects = project . descendants . visible . all
2013-02-15 00:37:17 +04:00
principals += Principal . member_of ( subprojects )
2012-12-09 18:10:49 +04:00
end
2013-02-15 00:37:17 +04:00
versions = project . shared_versions . all
categories = project . issue_categories . all
issue_custom_fields = project . all_issue_custom_fields
2012-12-09 18:10:49 +04:00
else
if all_projects . any?
principals += Principal . member_of ( all_projects )
end
2013-02-15 00:37:17 +04:00
versions = Version . visible . find_all_by_sharing ( 'system' )
2013-06-01 14:56:12 +04:00
issue_custom_fields = IssueCustomField . where ( :is_for_all = > true )
2012-12-09 18:10:49 +04:00
end
principals . uniq!
principals . sort!
users = principals . select { | p | p . is_a? ( User ) }
2013-02-15 00:37:17 +04:00
add_available_filter " status_id " ,
:type = > :list_status , :values = > IssueStatus . sorted . all . collect { | s | [ s . name , s . id . to_s ] }
if project . nil?
project_values = [ ]
if User . current . logged? && User . current . memberships . any?
project_values << [ " << #{ l ( :label_my_projects ) . downcase } >> " , " mine " ]
end
project_values += all_projects_values
add_available_filter ( " project_id " ,
:type = > :list , :values = > project_values
) unless project_values . empty?
end
add_available_filter " tracker_id " ,
:type = > :list , :values = > trackers . collect { | s | [ s . name , s . id . to_s ] }
add_available_filter " priority_id " ,
:type = > :list , :values = > IssuePriority . all . collect { | s | [ s . name , s . id . to_s ] }
2012-12-09 18:10:49 +04:00
author_values = [ ]
author_values << [ " << #{ l ( :label_me ) } >> " , " me " ] if User . current . logged?
author_values += users . collect { | s | [ s . name , s . id . to_s ] }
2013-02-15 00:37:17 +04:00
add_available_filter ( " author_id " ,
:type = > :list , :values = > author_values
) unless author_values . empty?
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 ] }
add_available_filter ( " assigned_to_id " ,
:type = > :list_optional , :values = > assigned_to_values
) unless assigned_to_values . empty?
2012-12-09 18:10:49 +04:00
group_values = Group . all . collect { | g | [ g . name , g . id . to_s ] }
2013-02-15 00:37:17 +04:00
add_available_filter ( " member_of_group " ,
:type = > :list_optional , :values = > group_values
) unless group_values . empty?
2012-12-09 18:10:49 +04:00
role_values = Role . givable . collect { | r | [ r . name , r . id . to_s ] }
2013-02-15 00:37:17 +04:00
add_available_filter ( " assigned_to_role " ,
:type = > :list_optional , :values = > role_values
) unless role_values . empty?
2012-12-09 18:10:49 +04:00
2013-02-15 00:37:17 +04:00
if versions . any?
add_available_filter " fixed_version_id " ,
:type = > :list_optional ,
:values = > versions . sort . collect { | s | [ " #{ s . project . name } - #{ s . name } " , s . id . to_s ] }
2012-12-09 18:10:49 +04:00
end
2013-02-15 00:37:17 +04:00
if categories . any?
add_available_filter " category_id " ,
:type = > :list_optional ,
:values = > categories . collect { | s | [ s . name , s . id . to_s ] }
2012-12-09 18:10:49 +04:00
end
2013-02-15 00:37:17 +04:00
add_available_filter " subject " , :type = > :text
add_available_filter " created_on " , :type = > :date_past
add_available_filter " updated_on " , :type = > :date_past
2013-02-16 13:39:52 +04:00
add_available_filter " closed_on " , :type = > :date_past
2013-02-15 00:37:17 +04:00
add_available_filter " start_date " , :type = > :date
add_available_filter " due_date " , :type = > :date
add_available_filter " estimated_hours " , :type = > :float
add_available_filter " done_ratio " , :type = > :integer
2012-12-09 18:10:49 +04:00
if User . current . allowed_to? ( :set_issues_private , nil , :global = > true ) ||
User . current . allowed_to? ( :set_own_issues_private , nil , :global = > true )
2013-02-15 00:37:17 +04:00
add_available_filter " is_private " ,
:type = > :list ,
2012-12-09 18:10:49 +04:00
:values = > [ [ l ( :general_text_yes ) , " 1 " ] , [ l ( :general_text_no ) , " 0 " ] ]
end
2013-02-15 00:37:17 +04:00
if User . current . logged?
add_available_filter " watcher_id " ,
:type = > :list , :values = > [ [ " << #{ l ( :label_me ) } >> " , " me " ] ]
end
if subprojects . any?
add_available_filter " subproject_id " ,
:type = > :list_subprojects ,
:values = > subprojects . collect { | s | [ s . name , s . id . to_s ] }
end
add_custom_fields_filters ( issue_custom_fields )
add_associations_custom_fields_filters :project , :author , :assigned_to , :fixed_version
IssueRelation :: TYPES . each do | relation_type , options |
add_available_filter relation_type , :type = > :relation , :label = > options [ :name ]
end
2012-12-09 18:10:49 +04:00
Tracker . disabled_core_fields ( trackers ) . each { | field |
2013-02-15 00:37:17 +04:00
delete_available_filter field
2012-12-09 18:10:49 +04:00
}
end
def available_columns
return @available_columns if @available_columns
@available_columns = self . class . available_columns . dup
@available_columns += ( project ?
project . all_issue_custom_fields :
2013-07-13 13:20:11 +04:00
IssueCustomField
) . visible . collect { | cf | QueryCustomFieldColumn . new ( cf ) }
2012-12-09 18:10:49 +04:00
if User . current . allowed_to? ( :view_time_entries , project , :global = > true )
index = nil
@available_columns . each_with_index { | column , i | index = i if column . name == :estimated_hours }
index = ( index ? index + 1 : - 1 )
# insert the column after estimated_hours or at the end
@available_columns . insert index , QueryColumn . new ( :spent_hours ,
2013-01-04 14:08:29 +04:00
:sortable = > " COALESCE((SELECT SUM(hours) FROM #{ TimeEntry . table_name } WHERE #{ TimeEntry . table_name } .issue_id = #{ Issue . table_name } .id), 0) " ,
2012-12-09 18:10:49 +04:00
:default_order = > 'desc' ,
:caption = > :label_spent_time
)
end
if User . current . allowed_to? ( :set_issues_private , nil , :global = > true ) ||
User . current . allowed_to? ( :set_own_issues_private , nil , :global = > true )
@available_columns << QueryColumn . new ( :is_private , :sortable = > " #{ Issue . table_name } .is_private " )
end
disabled_fields = Tracker . disabled_core_fields ( trackers ) . map { | field | field . sub ( / _id$ / , '' ) }
@available_columns . reject! { | column |
disabled_fields . include? ( column . name . to_s )
}
@available_columns
end
2012-12-09 18:44:28 +04:00
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
end
end
2012-12-09 18:10:49 +04:00
# Returns the issue count
def issue_count
2013-06-12 23:13:25 +04:00
Issue . visible . joins ( :status , :project ) . where ( statement ) . count
2012-12-09 18:10:49 +04:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
end
# Returns the issue count by group or nil if query is not grouped
def issue_count_by_group
r = nil
if grouped?
begin
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
2013-06-12 23:13:25 +04:00
r = Issue . visible .
joins ( :status , :project ) .
where ( statement ) .
joins ( joins_for_order_statement ( group_by_statement ) ) .
group ( group_by_statement ) .
count
2012-12-09 18:10:49 +04:00
rescue ActiveRecord :: RecordNotFound
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 }
end
end
r
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
end
# Returns the issues
# Valid options are :order, :offset, :limit, :include, :conditions
def issues ( options = { } )
2013-01-04 14:04:25 +04:00
order_option = [ group_by_sort_order , options [ :order ] ] . flatten . reject ( & :blank? )
2012-12-09 18:10:49 +04:00
2013-06-12 23:13:25 +04:00
issues = Issue . visible .
joins ( :status , :project ) .
where ( statement ) .
includes ( ( [ :status , :project ] + ( options [ :include ] || [ ] ) ) . uniq ) .
where ( options [ :conditions ] ) .
order ( order_option ) .
joins ( joins_for_order_statement ( order_option . join ( ',' ) ) ) .
limit ( options [ :limit ] ) .
offset ( options [ :offset ] ) .
all
2012-12-09 18:10:49 +04:00
if has_column? ( :spent_hours )
Issue . load_visible_spent_hours ( issues )
end
if has_column? ( :relations )
Issue . load_visible_relations ( issues )
end
issues
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
end
# Returns the issues ids
def issue_ids ( options = { } )
2013-01-04 14:04:25 +04:00
order_option = [ group_by_sort_order , options [ :order ] ] . flatten . reject ( & :blank? )
2012-12-09 18:10:49 +04:00
2013-06-12 23:13:25 +04:00
Issue . visible .
joins ( :status , :project ) .
where ( statement ) .
includes ( ( [ :status , :project ] + ( options [ :include ] || [ ] ) ) . uniq ) .
where ( options [ :conditions ] ) .
order ( order_option ) .
joins ( joins_for_order_statement ( order_option . join ( ',' ) ) ) .
limit ( options [ :limit ] ) .
offset ( options [ :offset ] ) .
find_ids
2012-12-09 18:10:49 +04:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
end
# Returns the journals
# Valid options are :order, :offset, :limit
def journals ( options = { } )
2013-06-12 23:13:25 +04:00
Journal . visible .
joins ( :issue = > [ :project , :status ] ) .
where ( statement ) .
order ( options [ :order ] ) .
limit ( options [ :limit ] ) .
offset ( options [ :offset ] ) .
preload ( :details , :user , { :issue = > [ :project , :author , :tracker , :status ] } ) .
all
2012-12-09 18:10:49 +04:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
end
# Returns the versions
# Valid options are :conditions
def versions ( options = { } )
2013-06-12 23:13:25 +04:00
Version . visible .
where ( project_statement ) .
where ( options [ :conditions ] ) .
includes ( :project ) .
all
2012-12-09 18:10:49 +04:00
rescue :: ActiveRecord :: StatementInvalid = > e
raise StatementInvalid . new ( e . message )
end
2012-12-09 18:44:28 +04: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
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 |
2013-01-31 01:31:13 +04:00
user_ids + group . user_ids + [ group . id ]
} . uniq . compact . sort . collect ( & :to_s )
2012-12-09 18:44:28 +04:00
'(' + sql_for_field ( " assigned_to_id " , operator , members_of_groups , Issue . table_name , " assigned_to_id " , false ) + ')'
end
def sql_for_assigned_to_role_field ( field , operator , value )
case operator
when " * " , " !* " # Member / Not member
sw = operator == " !* " ? 'NOT' : ''
nl = operator == " !* " ? " #{ Issue . table_name } .assigned_to_id IS NULL OR " : ''
" ( #{ nl } #{ Issue . table_name } .assigned_to_id #{ sw } IN (SELECT DISTINCT #{ Member . table_name } .user_id FROM #{ Member . table_name } " +
" WHERE #{ Member . table_name } .project_id = #{ Issue . table_name } .project_id)) "
when " = " , " ! "
role_cond = value . any? ?
" #{ MemberRole . table_name } .role_id IN ( " + value . collect { | val | " ' #{ connection . quote_string ( val ) } ' " } . join ( " , " ) + " ) " :
" 1=0 "
sw = operator == " ! " ? 'NOT' : ''
nl = operator == " ! " ? " #{ Issue . table_name } .assigned_to_id IS NULL OR " : ''
" ( #{ nl } #{ Issue . table_name } .assigned_to_id #{ sw } IN (SELECT DISTINCT #{ Member . table_name } .user_id FROM #{ Member . table_name } , #{ MemberRole . table_name } " +
" WHERE #{ Member . table_name } .project_id = #{ Issue . table_name } .project_id AND #{ Member . table_name } .id = #{ MemberRole . table_name } .member_id AND #{ role_cond } )) "
end
end
def sql_for_is_private_field ( field , operator , value )
op = ( operator == " = " ? 'IN' : 'NOT IN' )
va = value . map { | v | v == '0' ? connection . quoted_false : connection . quoted_true } . uniq . join ( ',' )
" #{ Issue . table_name } .is_private #{ op } ( #{ va } ) "
end
def sql_for_relations ( field , operator , value , options = { } )
relation_options = IssueRelation :: TYPES [ field ]
return relation_options unless relation_options
relation_type = field
join_column , target_join_column = " issue_from_id " , " issue_to_id "
if relation_options [ :reverse ] || options [ :reverse ]
relation_type = relation_options [ :reverse ] || relation_type
join_column , target_join_column = target_join_column , join_column
end
sql = case operator
when " * " , " !* "
op = ( operator == " * " ? 'IN' : 'NOT IN' )
" #{ Issue . table_name } .id #{ op } (SELECT DISTINCT #{ IssueRelation . table_name } . #{ join_column } FROM #{ IssueRelation . table_name } WHERE #{ IssueRelation . table_name } .relation_type = ' #{ connection . quote_string ( relation_type ) } ') "
when " = " , " ! "
op = ( operator == " = " ? 'IN' : 'NOT IN' )
" #{ Issue . table_name } .id #{ op } (SELECT DISTINCT #{ IssueRelation . table_name } . #{ join_column } FROM #{ IssueRelation . table_name } WHERE #{ IssueRelation . table_name } .relation_type = ' #{ connection . quote_string ( relation_type ) } ' AND #{ IssueRelation . table_name } . #{ target_join_column } = #{ value . first . to_i } ) "
when " =p " , " =!p " , " !p "
op = ( operator == " !p " ? 'NOT IN' : 'IN' )
comp = ( operator == " =!p " ? '<>' : '=' )
" #{ Issue . table_name } .id #{ op } (SELECT DISTINCT #{ IssueRelation . table_name } . #{ join_column } FROM #{ IssueRelation . table_name } , #{ Issue . table_name } relissues WHERE #{ IssueRelation . table_name } .relation_type = ' #{ connection . quote_string ( relation_type ) } ' AND #{ IssueRelation . table_name } . #{ target_join_column } = relissues.id AND relissues.project_id #{ comp } #{ value . first . to_i } ) "
end
if relation_options [ :sym ] == field && ! options [ :reverse ]
sqls = [ sql , sql_for_relations ( field , operator , value , :reverse = > true ) ]
2013-07-28 19:29:31 +04:00
sql = sqls . join ( [ " ! " , " !* " , " !p " ] . include? ( operator ) ? " AND " : " OR " )
2012-12-09 18:44:28 +04:00
end
2013-07-28 19:29:31 +04:00
" ( #{ sql } ) "
2012-12-09 18:44:28 +04:00
end
IssueRelation :: TYPES . keys . each do | relation_type |
alias_method " sql_for_ #{ relation_type } _field " . to_sym , :sql_for_relations
end
2012-12-09 18:10:49 +04:00
end