You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

time_entry_query.rb 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2019 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. class TimeEntryQuery < Query
  19. self.queried_class = TimeEntry
  20. self.view_permission = :view_time_entries
  21. self.available_columns = [
  22. QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
  23. QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
  24. TimestampQueryColumn.new(:created_on, :sortable => "#{TimeEntry.table_name}.created_on", :default_order => 'desc', :groupable => true),
  25. QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => :label_week),
  26. QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement}),
  27. QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
  28. QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
  29. QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
  30. QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
  31. QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
  32. QueryAssociationColumn.new(:issue, :category, :caption => :field_category, :sortable => "#{IssueCategory.table_name}.name"),
  33. QueryColumn.new(:comments),
  34. QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
  35. ]
  36. def initialize(attributes=nil, *args)
  37. super attributes
  38. self.filters ||= { 'spent_on' => {:operator => "*", :values => []} }
  39. end
  40. def initialize_available_filters
  41. add_available_filter "spent_on", :type => :date_past
  42. add_available_filter("project_id",
  43. :type => :list, :values => lambda { project_values }
  44. ) if project.nil?
  45. if project && !project.leaf?
  46. add_available_filter "subproject_id",
  47. :type => :list_subprojects,
  48. :values => lambda { subproject_values }
  49. end
  50. add_available_filter("issue_id", :type => :tree, :label => :label_issue)
  51. add_available_filter("issue.tracker_id",
  52. :type => :list,
  53. :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
  54. :values => lambda { trackers.map {|t| [t.name, t.id.to_s]} })
  55. add_available_filter("issue.status_id",
  56. :type => :list,
  57. :name => l("label_attribute_of_issue", :name => l(:field_status)),
  58. :values => lambda { issue_statuses_values })
  59. add_available_filter("issue.fixed_version_id",
  60. :type => :list,
  61. :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
  62. :values => lambda { fixed_version_values })
  63. add_available_filter "issue.category_id",
  64. :type => :list_optional,
  65. :name => l("label_attribute_of_issue", :name => l(:field_category)),
  66. :values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } } if project
  67. add_available_filter("user_id",
  68. :type => :list_optional, :values => lambda { author_values }
  69. )
  70. add_available_filter("author_id",
  71. :type => :list_optional, :values => lambda { author_values }
  72. )
  73. activities = (project ? project.activities : TimeEntryActivity.shared)
  74. add_available_filter("activity_id",
  75. :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
  76. )
  77. add_available_filter("project.status",
  78. :type => :list,
  79. :name => l(:label_attribute_of_project, :name => l(:field_status)),
  80. :values => lambda { project_statuses_values }
  81. ) if project.nil? || !project.leaf?
  82. add_available_filter "comments", :type => :text
  83. add_available_filter "hours", :type => :float
  84. add_custom_fields_filters(TimeEntryCustomField)
  85. add_associations_custom_fields_filters :project
  86. add_custom_fields_filters(issue_custom_fields, :issue)
  87. add_associations_custom_fields_filters :user
  88. end
  89. def available_columns
  90. return @available_columns if @available_columns
  91. @available_columns = self.class.available_columns.dup
  92. @available_columns += TimeEntryCustomField.visible.
  93. map {|cf| QueryCustomFieldColumn.new(cf) }
  94. @available_columns += issue_custom_fields.visible.
  95. map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
  96. @available_columns += ProjectCustomField.visible.
  97. map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) }
  98. @available_columns
  99. end
  100. def default_columns_names
  101. @default_columns_names ||= begin
  102. default_columns = Setting.time_entry_list_defaults.symbolize_keys[:column_names].map(&:to_sym)
  103. project.present? ? default_columns : [:project] | default_columns
  104. end
  105. end
  106. def default_totalable_names
  107. Setting.time_entry_list_defaults.symbolize_keys[:totalable_names].map(&:to_sym)
  108. end
  109. def default_sort_criteria
  110. [['spent_on', 'desc']]
  111. end
  112. # If a filter against a single issue is set, returns its id, otherwise nil.
  113. def filtered_issue_id
  114. if value_for('issue_id').to_s =~ /\A(\d+)\z/
  115. $1
  116. end
  117. end
  118. def base_scope
  119. TimeEntry.visible.
  120. joins(:project, :user).
  121. includes(:activity).
  122. references(:activity).
  123. left_join_issue.
  124. where(statement)
  125. end
  126. def results_scope(options={})
  127. order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
  128. base_scope.
  129. order(order_option).
  130. joins(joins_for_order_statement(order_option.join(',')))
  131. end
  132. # Returns sum of all the spent hours
  133. def total_for_hours(scope)
  134. map_total(scope.sum(:hours)) {|t| t.to_f.round(2)}
  135. end
  136. def sql_for_issue_id_field(field, operator, value)
  137. case operator
  138. when "="
  139. "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
  140. when "~"
  141. issue = Issue.where(:id => value.first.to_i).first
  142. if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
  143. "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
  144. else
  145. "1=0"
  146. end
  147. when "!*"
  148. "#{TimeEntry.table_name}.issue_id IS NULL"
  149. when "*"
  150. "#{TimeEntry.table_name}.issue_id IS NOT NULL"
  151. end
  152. end
  153. def sql_for_issue_fixed_version_id_field(field, operator, value)
  154. issue_ids = Issue.where(:fixed_version_id => value.map(&:to_i)).pluck(:id)
  155. case operator
  156. when "="
  157. if issue_ids.any?
  158. "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
  159. else
  160. "1=0"
  161. end
  162. when "!"
  163. if issue_ids.any?
  164. "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
  165. else
  166. "1=1"
  167. end
  168. end
  169. end
  170. def sql_for_activity_id_field(field, operator, value)
  171. ids = value.map(&:to_i).join(',')
  172. table_name = Enumeration.table_name
  173. if operator == '='
  174. "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
  175. else
  176. "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
  177. end
  178. end
  179. def sql_for_issue_tracker_id_field(field, operator, value)
  180. sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
  181. end
  182. def sql_for_issue_status_id_field(field, operator, value)
  183. sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
  184. end
  185. def sql_for_issue_category_id_field(field, operator, value)
  186. sql_for_field("category_id", operator, value, Issue.table_name, "category_id")
  187. end
  188. def sql_for_project_status_field(field, operator, value, options={})
  189. sql_for_field(field, operator, value, Project.table_name, "status")
  190. end
  191. # Accepts :from/:to params as shortcut filters
  192. def build_from_params(params, defaults={})
  193. super
  194. if params[:from].present? && params[:to].present?
  195. add_filter('spent_on', '><', [params[:from], params[:to]])
  196. elsif params[:from].present?
  197. add_filter('spent_on', '>=', [params[:from]])
  198. elsif params[:to].present?
  199. add_filter('spent_on', '<=', [params[:to]])
  200. end
  201. self
  202. end
  203. def joins_for_order_statement(order_options)
  204. joins = [super]
  205. if order_options
  206. if order_options.include?('issue_statuses')
  207. joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
  208. end
  209. if order_options.include?('trackers')
  210. joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
  211. end
  212. if order_options.include?('issue_categories')
  213. joins << "LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{Issue.table_name}.category_id"
  214. end
  215. end
  216. joins.compact!
  217. joins.any? ? joins.join(' ') : nil
  218. end
  219. end