2019-03-16 09:37:35 +00:00
# frozen_string_literal: true
2019-03-15 01:32:57 +00:00
2012-12-09 17:57:18 +00:00
# Redmine - project management software
2024-02-26 22:55:54 +00:00
# Copyright (C) 2006- Jean-Philippe Lang
2012-12-09 17:57:18 +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.
#
# 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 TimeEntryQuery < Query
self . queried_class = TimeEntry
2016-07-11 18:08:55 +00:00
self . view_permission = :view_time_entries
2012-12-09 17:57:18 +00:00
self . available_columns = [
QueryColumn . new ( :project , :sortable = > " #{ Project . table_name } .name " , :groupable = > true ) ,
2012-12-09 21:03:30 +00:00
QueryColumn . new ( :spent_on , :sortable = > [ " #{ TimeEntry . table_name } .spent_on " , " #{ TimeEntry . table_name } .created_on " ] , :default_order = > 'desc' , :groupable = > true ) ,
2019-03-24 06:14:33 +00:00
TimestampQueryColumn . new ( :created_on , :sortable = > " #{ TimeEntry . table_name } .created_on " , :default_order = > 'desc' , :groupable = > true ) ,
2020-12-12 03:32:20 +00:00
QueryColumn . new ( :tweek , :sortable = > [ " #{ TimeEntry . table_name } .tyear " , " #{ TimeEntry . table_name } .tweek " ] , :caption = > :label_week ) ,
2018-12-16 16:28:22 +00:00
QueryColumn . new ( :author , :sortable = > lambda { User . fields_for_order_statement } ) ,
2012-12-09 17:57:18 +00:00
QueryColumn . new ( :user , :sortable = > lambda { User . fields_for_order_statement } , :groupable = > true ) ,
QueryColumn . new ( :activity , :sortable = > " #{ TimeEntryActivity . table_name } .position " , :groupable = > true ) ,
2019-11-10 07:56:47 +00:00
QueryColumn . new ( :issue , :sortable = > " #{ Issue . table_name } .id " , :groupable = > true ) ,
2016-08-20 10:27:39 +00:00
QueryAssociationColumn . new ( :issue , :tracker , :caption = > :field_tracker , :sortable = > " #{ Tracker . table_name } .position " ) ,
2023-04-11 09:11:40 +00:00
QueryAssociationColumn . new ( :issue , :parent , :caption = > :field_parent_issue , :sortable = > [ " #{ Issue . table_name } .root_id " , " #{ Issue . table_name } .lft ASC " ] , :default_order = > 'desc' ) ,
2016-08-20 10:27:39 +00:00
QueryAssociationColumn . new ( :issue , :status , :caption = > :field_status , :sortable = > " #{ IssueStatus . table_name } .position " ) ,
2018-04-01 00:31:07 +00:00
QueryAssociationColumn . new ( :issue , :category , :caption = > :field_category , :sortable = > " #{ IssueCategory . table_name } .name " ) ,
2019-11-10 00:42:30 +00:00
QueryAssociationColumn . new ( :issue , :fixed_version , :caption = > :field_fixed_version , :sortable = > Version . fields_for_order_statement ) ,
2012-12-09 17:57:18 +00:00
QueryColumn . new ( :comments ) ,
2016-07-13 19:02:48 +00:00
QueryColumn . new ( :hours , :sortable = > " #{ TimeEntry . table_name } .hours " , :totalable = > true ) ,
2012-12-09 17:57:18 +00:00
]
def initialize ( attributes = nil , * args )
2023-12-20 07:15:11 +00:00
super ( attributes )
2020-11-16 12:21:47 +00:00
self . filters || = { 'spent_on' = > { :operator = > " * " , :values = > [ ] } }
2012-12-09 17:57:18 +00:00
end
2013-02-14 20:37:17 +00:00
def initialize_available_filters
add_available_filter " spent_on " , :type = > :date_past
2019-11-09 09:18:40 +00:00
add_available_filter (
" project_id " ,
2020-11-16 12:21:47 +00:00
:type = > :list , :values = > lambda { project_values }
2017-01-09 19:59:54 +00:00
) if project . nil?
if project && ! project . leaf?
2019-11-09 09:18:40 +00:00
add_available_filter (
" subproject_id " ,
2017-01-09 19:59:54 +00:00
:type = > :list_subprojects ,
2020-11-16 12:21:47 +00:00
:values = > lambda { subproject_values } )
2012-12-09 19:18:57 +00:00
end
2016-07-12 19:28:49 +00:00
add_available_filter ( " issue_id " , :type = > :tree , :label = > :label_issue )
2019-11-09 09:18:40 +00:00
add_available_filter (
" issue.tracker_id " ,
2016-08-20 10:27:39 +00:00
:type = > :list ,
:name = > l ( " label_attribute_of_issue " , :name = > l ( :field_tracker ) ) ,
2020-11-16 12:21:47 +00:00
:values = > lambda { trackers . map { | t | [ t . name , t . id . to_s ] } } )
2023-04-11 09:11:40 +00:00
add_available_filter (
" issue.parent_id " ,
:type = > :tree ,
:name = > l ( " label_attribute_of_issue " , :name = > l ( :field_parent_issue ) ) )
2019-11-09 09:18:40 +00:00
add_available_filter (
" issue.status_id " ,
2016-08-20 10:27:39 +00:00
:type = > :list ,
:name = > l ( " label_attribute_of_issue " , :name = > l ( :field_status ) ) ,
2020-11-16 12:21:47 +00:00
:values = > lambda { issue_statuses_values } )
2019-11-09 09:18:40 +00:00
add_available_filter (
" issue.fixed_version_id " ,
2016-07-13 18:26:44 +00:00
:type = > :list ,
:name = > l ( " label_attribute_of_issue " , :name = > l ( :field_fixed_version ) ) ,
2020-11-16 12:21:47 +00:00
:values = > lambda { fixed_version_values } )
2019-11-09 09:18:40 +00:00
add_available_filter (
" issue.category_id " ,
2018-04-01 00:29:22 +00:00
:type = > :list_optional ,
:name = > l ( " label_attribute_of_issue " , :name = > l ( :field_category ) ) ,
2023-12-29 08:00:23 +00:00
:values = > lambda { project . issue_categories . pluck ( :name , :id ) . map { | name , id | [ name , id . to_s ] } }
2019-11-09 09:18:40 +00:00
) if project
2023-04-09 01:38:36 +00:00
add_available_filter (
" issue.subject " ,
:type = > :text ,
:name = > l ( " label_attribute_of_issue " , :name = > l ( :field_subject ) )
)
2019-11-09 09:18:40 +00:00
add_available_filter (
" user_id " ,
2020-11-16 12:21:47 +00:00
:type = > :list_optional , :values = > lambda { author_values }
2017-01-09 19:59:54 +00:00
)
2024-08-16 02:05:25 +00:00
add_available_filter (
" user.group " ,
:type = > :list_optional ,
:name = > l ( " label_attribute_of_user " , :name = > l ( :label_group ) ) ,
:values = > lambda { Group . givable . visible . pluck ( :name , :id ) . map { | name , id | [ name , id . to_s ] } }
)
add_available_filter (
" user.role " ,
:type = > :list_optional ,
:name = > l ( " label_attribute_of_user " , :name = > l ( :field_role ) ) ,
:values = > lambda { Role . givable . pluck ( :name , :id ) . map { | name , id | [ name , id . to_s ] } }
)
2019-11-09 09:18:40 +00:00
add_available_filter (
" author_id " ,
2020-11-16 12:21:47 +00:00
:type = > :list_optional , :values = > lambda { author_values }
2018-12-16 16:28:22 +00:00
)
2015-09-30 18:03:13 +00:00
activities = ( project ? project . activities : TimeEntryActivity . shared )
2019-11-09 09:18:40 +00:00
add_available_filter (
" activity_id " ,
2022-02-20 18:50:22 +00:00
:type = > :list , :values = > activities . map { | a | [ a . name , ( a . parent_id || a . id ) . to_s ] }
2017-01-09 19:59:54 +00:00
)
2019-11-09 09:18:40 +00:00
add_available_filter (
" project.status " ,
2018-10-29 04:05:18 +00:00
:type = > :list ,
:name = > l ( :label_attribute_of_project , :name = > l ( :field_status ) ) ,
2020-11-16 12:21:47 +00:00
:values = > lambda { project_statuses_values }
2018-10-29 04:05:18 +00:00
) if project . nil? || ! project . leaf?
2013-02-14 20:37:17 +00:00
add_available_filter " comments " , :type = > :text
add_available_filter " hours " , :type = > :float
2012-12-09 19:18:57 +00:00
2020-05-08 04:30:24 +00:00
add_custom_fields_filters ( time_entry_custom_fields )
2016-10-08 07:28:45 +00:00
add_associations_custom_fields_filters :project
add_custom_fields_filters ( issue_custom_fields , :issue )
add_associations_custom_fields_filters :user
2012-12-09 17:57:18 +00:00
end
2012-12-09 21:03:30 +00:00
def available_columns
return @available_columns if @available_columns
2020-11-05 13:42:16 +00:00
2012-12-09 21:03:30 +00:00
@available_columns = self . class . available_columns . dup
2020-05-08 04:30:24 +00:00
@available_columns += time_entry_custom_fields . visible .
2020-11-16 12:21:47 +00:00
map { | cf | QueryCustomFieldColumn . new ( cf ) }
2016-10-08 07:28:45 +00:00
@available_columns += issue_custom_fields . visible .
2020-11-16 12:21:47 +00:00
map { | cf | QueryAssociationCustomFieldColumn . new ( :issue , cf , :totalable = > false ) }
2020-05-08 04:30:24 +00:00
@available_columns += project_custom_fields . visible .
2020-11-16 12:21:47 +00:00
map { | cf | QueryAssociationCustomFieldColumn . new ( :project , cf ) }
2012-12-09 21:03:30 +00:00
@available_columns
end
2012-12-09 17:57:18 +00:00
def default_columns_names
2016-12-03 08:29:44 +00:00
@default_columns_names || = begin
2018-09-23 11:59:52 +00:00
default_columns = Setting . time_entry_list_defaults . symbolize_keys [ :column_names ] . map ( & :to_sym )
2016-12-03 08:29:44 +00:00
project . present? ? default_columns : [ :project ] | default_columns
end
2012-12-09 17:57:18 +00:00
end
2016-07-13 19:02:48 +00:00
def default_totalable_names
2018-09-23 11:59:52 +00:00
Setting . time_entry_list_defaults . symbolize_keys [ :totalable_names ] . map ( & :to_sym )
2016-07-13 19:02:48 +00:00
end
2017-04-25 17:44:08 +00:00
2017-03-13 19:17:59 +00:00
def default_sort_criteria
[ [ 'spent_on' , 'desc' ] ]
end
2016-07-13 19:02:48 +00:00
2017-07-30 16:42:55 +00:00
# If a filter against a single issue is set, returns its id, otherwise nil.
def filtered_issue_id
if value_for ( 'issue_id' ) . to_s =~ / \ A( \ d+) \ z /
$1
end
end
2016-07-13 19:02:48 +00:00
def base_scope
2025-04-08 01:30:37 +00:00
scope = TimeEntry . visible
. joins ( :project , :user )
. includes ( :activity )
. references ( :activity )
. left_join_issue
. where ( statement )
if Redmine :: Database . mysql? && ActiveRecord :: Base . connection . supports_optimizer_hints?
# Provides MySQL with a hint to use a better join order and avoid slow response times
scope . optimizer_hints ( 'JOIN_ORDER(time_entries, projects, users)' )
else
scope
end
2016-07-13 19:02:48 +00:00
end
2013-07-28 09:59:34 +00:00
def results_scope ( options = { } )
2017-03-13 19:17:59 +00:00
order_option = [ group_by_sort_order , ( options [ :order ] || sort_clause ) ] . flatten . reject ( & :blank? )
2013-07-28 09:59:34 +00:00
2019-06-20 06:13:11 +00:00
order_option << " #{ TimeEntry . table_name } .id ASC "
2016-08-20 10:27:39 +00:00
base_scope .
2013-07-28 09:59:34 +00:00
order ( order_option ) .
2017-07-09 17:16:01 +00:00
joins ( joins_for_order_statement ( order_option . join ( ',' ) ) )
2013-12-15 09:49:12 +00:00
end
2016-07-13 19:02:48 +00:00
# Returns sum of all the spent hours
def total_for_hours ( scope )
map_total ( scope . sum ( :hours ) ) { | t | t . to_f . round ( 2 ) }
end
2016-07-19 11:28:24 +00:00
2016-07-12 19:28:49 +00:00
def sql_for_issue_id_field ( field , operator , value )
case operator
when " = "
" #{ TimeEntry . table_name } .issue_id = #{ value . first . to_i } "
when " ~ "
issue = Issue . where ( :id = > value . first . to_i ) . first
if issue && ( issue_ids = issue . self_and_descendants . pluck ( :id ) ) . any?
" #{ TimeEntry . table_name } .issue_id IN ( #{ issue_ids . join ( ',' ) } ) "
else
" 1=0 "
end
when " !* "
" #{ TimeEntry . table_name } .issue_id IS NULL "
when " * "
" #{ TimeEntry . table_name } .issue_id IS NOT NULL "
end
end
2013-12-15 09:49:12 +00:00
2016-07-13 18:26:44 +00:00
def sql_for_issue_fixed_version_id_field ( field , operator , value )
2017-10-15 11:54:39 +00:00
issue_ids = Issue . where ( :fixed_version_id = > value . map ( & :to_i ) ) . pluck ( :id )
2016-07-13 18:26:44 +00:00
case operator
when " = "
if issue_ids . any?
" #{ TimeEntry . table_name } .issue_id IN ( #{ issue_ids . join ( ',' ) } ) "
else
" 1=0 "
end
when " ! "
if issue_ids . any?
" #{ TimeEntry . table_name } .issue_id NOT IN ( #{ issue_ids . join ( ',' ) } ) "
else
" 1=1 "
end
end
end
2023-04-11 09:11:40 +00:00
def sql_for_issue_parent_id_field ( field , operator , value )
case operator
when " = "
# accepts a comma separated list of ids
parent_ids = value . first . to_s . scan ( / \ d+ / ) . map ( & :to_i ) . uniq
issue_ids = Issue . where ( :parent_id = > parent_ids ) . pluck ( :id )
if issue_ids . present?
" #{ TimeEntry . table_name } .issue_id IN ( #{ issue_ids . join ( ',' ) } ) "
else
" 1=0 "
end
when " ~ "
root_id , lft , rgt = Issue . where ( :id = > value . first . to_i ) . pick ( :root_id , :lft , :rgt )
issue_ids = Issue . where ( " #{ Issue . table_name } .root_id = ? AND #{ Issue . table_name } .lft > ? AND #{ Issue . table_name } .rgt < ? " , root_id , lft , rgt ) . pluck ( :id ) if root_id && lft && rgt
if issue_ids . present?
" #{ TimeEntry . table_name } .issue_id IN ( #{ issue_ids . join ( ',' ) } ) "
else
" 1=0 "
end
else
sql_for_field ( " parent_id " , operator , value , Issue . table_name , " parent_id " )
end
end
2013-12-15 09:49:12 +00:00
def sql_for_activity_id_field ( field , operator , value )
ids = value . map ( & :to_i ) . join ( ',' )
table_name = Enumeration . table_name
if operator == '='
" ( #{ table_name } .id IN ( #{ ids } ) OR #{ table_name } .parent_id IN ( #{ ids } )) "
else
" ( #{ table_name } .id NOT IN ( #{ ids } ) AND ( #{ table_name } .parent_id IS NULL OR #{ table_name } .parent_id NOT IN ( #{ ids } ))) "
end
2013-07-28 09:59:34 +00:00
end
2016-08-20 10:27:39 +00:00
def sql_for_issue_tracker_id_field ( field , operator , value )
sql_for_field ( " tracker_id " , operator , value , Issue . table_name , " tracker_id " )
end
def sql_for_issue_status_id_field ( field , operator , value )
sql_for_field ( " status_id " , operator , value , Issue . table_name , " status_id " )
end
2018-04-01 00:29:22 +00:00
def sql_for_issue_category_id_field ( field , operator , value )
sql_for_field ( " category_id " , operator , value , Issue . table_name , " category_id " )
end
2023-04-09 01:38:36 +00:00
def sql_for_issue_subject_field ( field , operator , value )
sql_for_field ( " subject " , operator , value , Issue . table_name , " subject " )
end
2018-10-29 04:05:18 +00:00
def sql_for_project_status_field ( field , operator , value , options = { } )
sql_for_field ( field , operator , value , Project . table_name , " status " )
end
2024-08-16 02:05:25 +00:00
def sql_for_user_group_field ( field , operator , value )
if operator == '*' # Any group
groups = Group . givable
operator = '='
elsif operator == '!*'
groups = Group . givable
operator = '!'
else
groups = Group . where ( :id = > value ) . to_a
end
groups || = [ ]
members_of_groups = groups . inject ( [ ] ) do | user_ids , group |
user_ids + group . user_ids
end . uniq . compact . sort . collect ( & :to_s )
'(' + sql_for_field ( 'user_id' , operator , members_of_groups , TimeEntry . table_name , " user_id " , false ) + ')'
end
def sql_for_user_role_field ( field , operator , value )
case operator
when " * " , " !* "
sw = operator == " !* " ? " NOT " : " "
nl = operator == " !* " ? " #{ TimeEntry . table_name } .user_id IS NULL OR " : " "
subquery =
" SELECT 1 " +
" FROM #{ Member . table_name } " +
" WHERE #{ TimeEntry . table_name } .project_id = #{ Member . table_name } .project_id AND #{ Member . table_name } .user_id = #{ TimeEntry . table_name } .user_id "
" ( #{ nl } #{ sw } EXISTS ( #{ subquery } )) "
when " = " , " ! "
role_cond =
if value . any?
" #{ MemberRole . table_name } .role_id IN ( " + value . collect { | val | " ' #{ self . class . connection . quote_string ( val ) } ' " } . join ( " , " ) + " ) "
else
" 1=0 "
end
sw = operator == " ! " ? 'NOT' : ''
nl = operator == " ! " ? " #{ TimeEntry . table_name } .user_id IS NULL OR " : ''
subquery =
" SELECT 1 " +
" FROM #{ Member . table_name } inner join #{ MemberRole . table_name } on members.id = member_roles.member_id " +
" WHERE #{ TimeEntry . table_name } .project_id = #{ Member . table_name } .project_id AND #{ Member . table_name } .user_id = #{ TimeEntry . table_name } .user_id AND #{ role_cond } "
" ( #{ nl } #{ sw } EXISTS ( #{ subquery } )) "
end
end
2012-12-09 17:57:18 +00:00
# Accepts :from/:to params as shortcut filters
2017-07-12 18:13:20 +00:00
def build_from_params ( params , defaults = { } )
2012-12-09 17:57:18 +00:00
super
if params [ :from ] . present? && params [ :to ] . present?
add_filter ( 'spent_on' , '><' , [ params [ :from ] , params [ :to ] ] )
elsif params [ :from ] . present?
add_filter ( 'spent_on' , '>=' , [ params [ :from ] ] )
elsif params [ :to ] . present?
add_filter ( 'spent_on' , '<=' , [ params [ :to ] ] )
end
self
end
2016-08-20 10:27:39 +00:00
def joins_for_order_statement ( order_options )
joins = [ super ]
if order_options
if order_options . include? ( 'issue_statuses' )
joins << " LEFT OUTER JOIN #{ IssueStatus . table_name } ON #{ IssueStatus . table_name } .id = #{ Issue . table_name } .status_id "
end
if order_options . include? ( 'trackers' )
joins << " LEFT OUTER JOIN #{ Tracker . table_name } ON #{ Tracker . table_name } .id = #{ Issue . table_name } .tracker_id "
end
2018-04-01 00:31:07 +00:00
if order_options . include? ( 'issue_categories' )
joins << " LEFT OUTER JOIN #{ IssueCategory . table_name } ON #{ IssueCategory . table_name } .id = #{ Issue . table_name } .category_id "
end
2019-11-10 00:42:30 +00:00
if order_options . include? ( 'versions' )
joins << " LEFT OUTER JOIN #{ Version . table_name } ON #{ Version . table_name } .id = #{ Issue . table_name } .fixed_version_id "
end
2016-08-20 10:27:39 +00:00
end
joins . compact!
joins . any? ? joins . join ( ' ' ) : nil
end
2012-12-09 17:57:18 +00:00
end