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.6KB

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