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 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006- 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}.tyear", "#{TimeEntry.table_name}.tweek"], :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", :groupable => true),
  30. QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
  31. QueryAssociationColumn.new(:issue, :parent, :caption => :field_parent_issue, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc'),
  32. QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
  33. QueryAssociationColumn.new(:issue, :category, :caption => :field_category, :sortable => "#{IssueCategory.table_name}.name"),
  34. QueryAssociationColumn.new(:issue, :fixed_version, :caption => :field_fixed_version, :sortable => Version.fields_for_order_statement),
  35. QueryColumn.new(:comments),
  36. QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
  37. ]
  38. def initialize(attributes=nil, *args)
  39. super(attributes)
  40. self.filters ||= {'spent_on' => {:operator => "*", :values => []}}
  41. end
  42. def initialize_available_filters
  43. add_available_filter "spent_on", :type => :date_past
  44. add_available_filter(
  45. "project_id",
  46. :type => :list, :values => lambda {project_values}
  47. ) if project.nil?
  48. if project && !project.leaf?
  49. add_available_filter(
  50. "subproject_id",
  51. :type => :list_subprojects,
  52. :values => lambda {subproject_values})
  53. end
  54. add_available_filter("issue_id", :type => :tree, :label => :label_issue)
  55. add_available_filter(
  56. "issue.tracker_id",
  57. :type => :list,
  58. :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
  59. :values => lambda {trackers.map {|t| [t.name, t.id.to_s]}})
  60. add_available_filter(
  61. "issue.parent_id",
  62. :type => :tree,
  63. :name => l("label_attribute_of_issue", :name => l(:field_parent_issue)))
  64. add_available_filter(
  65. "issue.status_id",
  66. :type => :list,
  67. :name => l("label_attribute_of_issue", :name => l(:field_status)),
  68. :values => lambda {issue_statuses_values})
  69. add_available_filter(
  70. "issue.fixed_version_id",
  71. :type => :list,
  72. :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
  73. :values => lambda {fixed_version_values})
  74. add_available_filter(
  75. "issue.category_id",
  76. :type => :list_optional,
  77. :name => l("label_attribute_of_issue", :name => l(:field_category)),
  78. :values => lambda {project.issue_categories.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
  79. ) if project
  80. add_available_filter(
  81. "issue.subject",
  82. :type => :text,
  83. :name => l("label_attribute_of_issue", :name => l(:field_subject))
  84. )
  85. add_available_filter(
  86. "user_id",
  87. :type => :list_optional, :values => lambda {author_values}
  88. )
  89. add_available_filter(
  90. "author_id",
  91. :type => :list_optional, :values => lambda {author_values}
  92. )
  93. activities = (project ? project.activities : TimeEntryActivity.shared)
  94. add_available_filter(
  95. "activity_id",
  96. :type => :list, :values => activities.map {|a| [a.name, (a.parent_id || a.id).to_s]}
  97. )
  98. add_available_filter(
  99. "project.status",
  100. :type => :list,
  101. :name => l(:label_attribute_of_project, :name => l(:field_status)),
  102. :values => lambda {project_statuses_values}
  103. ) if project.nil? || !project.leaf?
  104. add_available_filter "comments", :type => :text
  105. add_available_filter "hours", :type => :float
  106. add_custom_fields_filters(time_entry_custom_fields)
  107. add_associations_custom_fields_filters :project
  108. add_custom_fields_filters(issue_custom_fields, :issue)
  109. add_associations_custom_fields_filters :user
  110. end
  111. def available_columns
  112. return @available_columns if @available_columns
  113. @available_columns = self.class.available_columns.dup
  114. @available_columns += time_entry_custom_fields.visible.
  115. map {|cf| QueryCustomFieldColumn.new(cf)}
  116. @available_columns += issue_custom_fields.visible.
  117. map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false)}
  118. @available_columns += project_custom_fields.visible.
  119. map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf)}
  120. @available_columns
  121. end
  122. def default_columns_names
  123. @default_columns_names ||= begin
  124. default_columns = Setting.time_entry_list_defaults.symbolize_keys[:column_names].map(&:to_sym)
  125. project.present? ? default_columns : [:project] | default_columns
  126. end
  127. end
  128. def default_totalable_names
  129. Setting.time_entry_list_defaults.symbolize_keys[:totalable_names].map(&:to_sym)
  130. end
  131. def default_sort_criteria
  132. [['spent_on', 'desc']]
  133. end
  134. # If a filter against a single issue is set, returns its id, otherwise nil.
  135. def filtered_issue_id
  136. if value_for('issue_id').to_s =~ /\A(\d+)\z/
  137. $1
  138. end
  139. end
  140. def base_scope
  141. TimeEntry.visible.
  142. joins(:project, :user).
  143. includes(:activity).
  144. references(:activity).
  145. left_join_issue.
  146. where(statement)
  147. end
  148. def results_scope(options={})
  149. order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
  150. order_option << "#{TimeEntry.table_name}.id ASC"
  151. base_scope.
  152. order(order_option).
  153. joins(joins_for_order_statement(order_option.join(',')))
  154. end
  155. # Returns sum of all the spent hours
  156. def total_for_hours(scope)
  157. map_total(scope.sum(:hours)) {|t| t.to_f.round(2)}
  158. end
  159. def sql_for_issue_id_field(field, operator, value)
  160. case operator
  161. when "="
  162. "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
  163. when "~"
  164. issue = Issue.where(:id => value.first.to_i).first
  165. if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
  166. "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
  167. else
  168. "1=0"
  169. end
  170. when "!*"
  171. "#{TimeEntry.table_name}.issue_id IS NULL"
  172. when "*"
  173. "#{TimeEntry.table_name}.issue_id IS NOT NULL"
  174. end
  175. end
  176. def sql_for_issue_fixed_version_id_field(field, operator, value)
  177. issue_ids = Issue.where(:fixed_version_id => value.map(&:to_i)).pluck(:id)
  178. case operator
  179. when "="
  180. if issue_ids.any?
  181. "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
  182. else
  183. "1=0"
  184. end
  185. when "!"
  186. if issue_ids.any?
  187. "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
  188. else
  189. "1=1"
  190. end
  191. end
  192. end
  193. def sql_for_issue_parent_id_field(field, operator, value)
  194. case operator
  195. when "="
  196. # accepts a comma separated list of ids
  197. parent_ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
  198. issue_ids = Issue.where(:parent_id => parent_ids).pluck(:id)
  199. if issue_ids.present?
  200. "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
  201. else
  202. "1=0"
  203. end
  204. when "~"
  205. root_id, lft, rgt = Issue.where(:id => value.first.to_i).pick(:root_id, :lft, :rgt)
  206. 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
  207. if issue_ids.present?
  208. "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
  209. else
  210. "1=0"
  211. end
  212. else
  213. sql_for_field("parent_id", operator, value, Issue.table_name, "parent_id")
  214. end
  215. end
  216. def sql_for_activity_id_field(field, operator, value)
  217. ids = value.map(&:to_i).join(',')
  218. table_name = Enumeration.table_name
  219. if operator == '='
  220. "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
  221. else
  222. "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
  223. end
  224. end
  225. def sql_for_issue_tracker_id_field(field, operator, value)
  226. sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
  227. end
  228. def sql_for_issue_status_id_field(field, operator, value)
  229. sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
  230. end
  231. def sql_for_issue_category_id_field(field, operator, value)
  232. sql_for_field("category_id", operator, value, Issue.table_name, "category_id")
  233. end
  234. def sql_for_issue_subject_field(field, operator, value)
  235. sql_for_field("subject", operator, value, Issue.table_name, "subject")
  236. end
  237. def sql_for_project_status_field(field, operator, value, options={})
  238. sql_for_field(field, operator, value, Project.table_name, "status")
  239. end
  240. # Accepts :from/:to params as shortcut filters
  241. def build_from_params(params, defaults={})
  242. super
  243. if params[:from].present? && params[:to].present?
  244. add_filter('spent_on', '><', [params[:from], params[:to]])
  245. elsif params[:from].present?
  246. add_filter('spent_on', '>=', [params[:from]])
  247. elsif params[:to].present?
  248. add_filter('spent_on', '<=', [params[:to]])
  249. end
  250. self
  251. end
  252. def joins_for_order_statement(order_options)
  253. joins = [super]
  254. if order_options
  255. if order_options.include?('issue_statuses')
  256. joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
  257. end
  258. if order_options.include?('trackers')
  259. joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
  260. end
  261. if order_options.include?('issue_categories')
  262. joins << "LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{Issue.table_name}.category_id"
  263. end
  264. if order_options.include?('versions')
  265. joins << "LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Issue.table_name}.fixed_version_id"
  266. end
  267. end
  268. joins.compact!
  269. joins.any? ? joins.join(' ') : nil
  270. end
  271. end