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.

issue_query.rb 36KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927
  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 IssueQuery < Query
  19. class EstimatedRemainingHoursColumn < QueryColumn
  20. COLUMN_SQL = Arel.sql("COALESCE(#{Issue.table_name}.estimated_hours, 0) * (100 - COALESCE(#{Issue.table_name}.done_ratio, 0)) / 100")
  21. def initialize
  22. super(:estimated_remaining_hours, totalable: true, sortable: COLUMN_SQL)
  23. end
  24. def value(object)
  25. (object.estimated_hours || 0) * (100 - (object.done_ratio || 0)) / 100
  26. end
  27. def value_object(object)
  28. value(object)
  29. end
  30. end
  31. self.queried_class = Issue
  32. self.view_permission = :view_issues
  33. self.available_columns = [
  34. QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id",
  35. :default_order => 'desc', :caption => '#', :frozen => true),
  36. QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
  37. QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
  38. QueryColumn.new(:parent,
  39. :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"],
  40. :default_order => 'desc', :caption => :field_parent_issue),
  41. QueryAssociationColumn.new(:parent, :subject, :caption => :field_parent_issue_subject),
  42. QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
  43. QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position",
  44. :default_order => 'desc', :groupable => true),
  45. QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
  46. QueryColumn.new(:author,
  47. :sortable => lambda {User.fields_for_order_statement("authors")},
  48. :groupable => true),
  49. QueryColumn.new(:assigned_to,
  50. :sortable => lambda {User.fields_for_order_statement},
  51. :groupable => true),
  52. WatcherQueryColumn.new(:watcher_users, :caption => :label_issue_watchers),
  53. TimestampQueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on",
  54. :default_order => 'desc', :groupable => true),
  55. QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
  56. QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement},
  57. :groupable => true),
  58. QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date", :groupable => true),
  59. QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date", :groupable => true),
  60. QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours",
  61. :totalable => true),
  62. EstimatedRemainingHoursColumn.new,
  63. QueryColumn.new(
  64. :total_estimated_hours,
  65. :sortable =>
  66. lambda do
  67. "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" \
  68. " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')}" \
  69. " AND subtasks.root_id = #{Issue.table_name}.root_id" \
  70. " AND subtasks.lft >= #{Issue.table_name}.lft" \
  71. " AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)"
  72. end,
  73. :default_order => 'desc'),
  74. QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
  75. TimestampQueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on",
  76. :default_order => 'desc', :groupable => true),
  77. TimestampQueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on",
  78. :default_order => 'desc', :groupable => true),
  79. QueryColumn.new(:last_updated_by,
  80. :sortable => lambda {User.fields_for_order_statement("last_journal_user")}),
  81. QueryColumn.new(:relations, :caption => :label_related_issues),
  82. QueryColumn.new(:attachments, :caption => :label_attachment_plural),
  83. QueryColumn.new(:description, :inline => false),
  84. QueryColumn.new(:last_notes, :caption => :label_last_notes, :inline => false)
  85. ]
  86. has_many :projects, foreign_key: 'default_issue_query_id', dependent: :nullify, inverse_of: 'default_issue_query'
  87. after_update { projects.clear unless visibility == VISIBILITY_PUBLIC }
  88. scope :for_all_projects, ->{ where(project_id: nil) }
  89. def self.default(project: nil, user: User.current)
  90. # user default
  91. if user&.logged? && (query_id = user.pref.default_issue_query).present?
  92. query = find_by(id: query_id)
  93. return query if query&.visible?(user)
  94. end
  95. # project default
  96. query = project&.default_issue_query
  97. return query if query&.visibility == VISIBILITY_PUBLIC
  98. # global default
  99. if (query_id = Setting.default_issue_query).present?
  100. query = find_by(id: query_id)
  101. return query if query&.visibility == VISIBILITY_PUBLIC
  102. end
  103. nil
  104. end
  105. def initialize(attributes=nil, *args)
  106. super(attributes)
  107. self.filters ||= {'status_id' => {:operator => "o", :values => [""]}}
  108. end
  109. def draw_relations
  110. r = options[:draw_relations]
  111. r.nil? || r == '1'
  112. end
  113. def draw_relations=(arg)
  114. options[:draw_relations] = (arg == '0' ? '0' : nil)
  115. end
  116. def draw_progress_line
  117. r = options[:draw_progress_line]
  118. r == '1'
  119. end
  120. def draw_progress_line=(arg)
  121. options[:draw_progress_line] = (arg == '1' ? '1' : nil)
  122. end
  123. def draw_selected_columns
  124. r = options[:draw_selected_columns]
  125. r == '1'
  126. end
  127. def draw_selected_columns=(arg)
  128. options[:draw_selected_columns] = (arg == '1' ? '1' : nil)
  129. end
  130. def build_from_params(params, defaults={})
  131. super
  132. self.draw_relations =
  133. params[:draw_relations] ||
  134. (params[:query] && params[:query][:draw_relations]) || options[:draw_relations]
  135. self.draw_progress_line =
  136. params[:draw_progress_line] ||
  137. (params[:query] && params[:query][:draw_progress_line]) ||
  138. options[:draw_progress_line]
  139. self.draw_selected_columns =
  140. params[:draw_selected_columns] ||
  141. (params[:query] && params[:query][:draw_selected_columns]) ||
  142. options[:draw_progress_line]
  143. self
  144. end
  145. def initialize_available_filters
  146. add_available_filter(
  147. "status_id",
  148. :type => :list_status, :values => lambda {issue_statuses_values}
  149. )
  150. add_available_filter(
  151. "project_id",
  152. :type => :list, :values => lambda {project_values}
  153. ) if project.nil?
  154. add_available_filter(
  155. "tracker_id",
  156. :type => :list_with_history, :values => trackers.collect{|s| [s.name, s.id.to_s]}
  157. )
  158. add_available_filter(
  159. "priority_id",
  160. :type => :list_with_history,
  161. :values => IssuePriority.pluck(:name, :id).map {|name, id| [name, id.to_s]}
  162. )
  163. add_available_filter(
  164. "author_id",
  165. :type => :list, :values => lambda {author_values}
  166. )
  167. add_available_filter(
  168. "assigned_to_id",
  169. :type => :list_optional_with_history, :values => lambda {assigned_to_values}
  170. )
  171. add_available_filter(
  172. "member_of_group",
  173. :type => :list_optional,
  174. :values => lambda {Group.givable.visible.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
  175. )
  176. add_available_filter(
  177. "assigned_to_role",
  178. :type => :list_optional,
  179. :values => lambda {Role.givable.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
  180. )
  181. add_available_filter(
  182. "fixed_version_id",
  183. :type => :list_optional_with_history, :values => lambda {fixed_version_values}
  184. )
  185. add_available_filter(
  186. "fixed_version.due_date",
  187. :type => :date,
  188. :name => l(:label_attribute_of_fixed_version, :name => l(:field_effective_date))
  189. )
  190. add_available_filter(
  191. "fixed_version.status",
  192. :type => :list,
  193. :name => l(:label_attribute_of_fixed_version, :name => l(:field_status)),
  194. :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s]}
  195. )
  196. add_available_filter(
  197. "category_id",
  198. :type => :list_optional_with_history,
  199. :values => lambda {project.issue_categories.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
  200. ) if project
  201. add_available_filter "subject", :type => :text
  202. add_available_filter "description", :type => :text
  203. add_available_filter "notes", :type => :text
  204. add_available_filter "created_on", :type => :date_past
  205. add_available_filter "updated_on", :type => :date_past
  206. add_available_filter "closed_on", :type => :date_past
  207. add_available_filter "start_date", :type => :date
  208. add_available_filter "due_date", :type => :date
  209. add_available_filter "estimated_hours", :type => :float
  210. if User.current.allowed_to?(:view_time_entries, project, :global => true)
  211. add_available_filter "spent_time", :type => :float, :label => :label_spent_time
  212. end
  213. add_available_filter "done_ratio", :type => :integer
  214. if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
  215. User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
  216. add_available_filter(
  217. "is_private",
  218. :type => :list,
  219. :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
  220. )
  221. end
  222. add_available_filter(
  223. "attachment",
  224. :type => :text, :name => l(:label_attachment)
  225. )
  226. add_available_filter(
  227. "attachment_description",
  228. :type => :text, :name => l(:label_attachment_description)
  229. )
  230. if User.current.logged?
  231. add_available_filter(
  232. "watcher_id",
  233. :type => :list, :values => lambda {watcher_values}
  234. )
  235. end
  236. add_available_filter(
  237. "updated_by",
  238. :type => :list, :values => lambda {author_values}
  239. )
  240. add_available_filter(
  241. "last_updated_by",
  242. :type => :list, :values => lambda {author_values}
  243. )
  244. if project && !project.leaf?
  245. add_available_filter(
  246. "subproject_id",
  247. :type => :list_subprojects,
  248. :values => lambda {subproject_values}
  249. )
  250. end
  251. add_available_filter(
  252. "project.status",
  253. :type => :list,
  254. :name => l(:label_attribute_of_project, :name => l(:field_status)),
  255. :values => lambda {project_statuses_values}
  256. ) if project.nil? || !project.leaf?
  257. add_custom_fields_filters(issue_custom_fields)
  258. add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
  259. IssueRelation::TYPES.each do |relation_type, options|
  260. add_available_filter(
  261. relation_type, :type => :relation, :label => options[:name],
  262. :values => lambda {all_projects_values}
  263. )
  264. end
  265. add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
  266. add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
  267. add_available_filter "issue_id", :type => :integer, :label => :label_issue
  268. add_available_filter "any_searchable", :type => :search
  269. Tracker.disabled_core_fields(trackers).each do |field|
  270. delete_available_filter field
  271. end
  272. end
  273. def available_columns
  274. return @available_columns if @available_columns
  275. @available_columns = self.class.available_columns.dup
  276. @available_columns += issue_custom_fields.visible.collect {|cf| QueryCustomFieldColumn.new(cf)}
  277. if User.current.allowed_to?(:view_time_entries, project, :global => true)
  278. # insert the columns after total_estimated_hours or at the end
  279. index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
  280. index = (index ? index + 1 : -1)
  281. subselect = "SELECT SUM(hours) FROM #{TimeEntry.table_name}" +
  282. " JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id" +
  283. " WHERE (#{TimeEntry.visible_condition(User.current)}) AND #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
  284. @available_columns.insert(
  285. index,
  286. QueryColumn.new(:spent_hours,
  287. :sortable => "COALESCE((#{subselect}), 0)",
  288. :default_order => 'desc',
  289. :caption => :label_spent_time,
  290. :totalable => true)
  291. )
  292. subselect = "SELECT SUM(hours) FROM #{TimeEntry.table_name}" +
  293. " JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id" +
  294. " JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
  295. " WHERE (#{TimeEntry.visible_condition(User.current)})" +
  296. " AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt"
  297. @available_columns.insert(
  298. index + 1,
  299. QueryColumn.new(:total_spent_hours,
  300. :sortable => "COALESCE((#{subselect}), 0)",
  301. :default_order => 'desc',
  302. :caption => :label_total_spent_time)
  303. )
  304. end
  305. if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
  306. User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
  307. @available_columns <<
  308. QueryColumn.new(:is_private,
  309. :sortable => "#{Issue.table_name}.is_private", :groupable => true)
  310. end
  311. disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.delete_suffix('_id')}
  312. if disabled_fields.include?("estimated_hours")
  313. disabled_fields += %w[total_estimated_hours estimated_remaining_hours]
  314. end
  315. @available_columns.reject! do |column|
  316. disabled_fields.include?(column.name.to_s)
  317. end
  318. @available_columns
  319. end
  320. def default_columns_names
  321. @default_columns_names ||= begin
  322. default_columns = Setting.issue_list_default_columns.map(&:to_sym)
  323. project.present? ? default_columns : [:project] | default_columns
  324. end
  325. end
  326. def default_totalable_names
  327. Setting.issue_list_default_totals.map(&:to_sym)
  328. end
  329. def default_sort_criteria
  330. [['id', 'desc']]
  331. end
  332. def base_scope
  333. Issue.visible.joins(:status, :project).where(statement)
  334. end
  335. # Returns the issue count
  336. def issue_count
  337. base_scope.count
  338. rescue ::ActiveRecord::StatementInvalid => e
  339. raise StatementInvalid.new(e.message)
  340. end
  341. # Returns sum of all the issue's estimated_hours
  342. def total_for_estimated_hours(scope)
  343. map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
  344. end
  345. def total_for_estimated_remaining_hours(scope)
  346. map_total(scope.sum(EstimatedRemainingHoursColumn::COLUMN_SQL)) {|t| t.to_f.round(2)}
  347. end
  348. # Returns sum of all the issue's time entries hours
  349. def total_for_spent_hours(scope)
  350. total = scope.joins(:time_entries).
  351. where(TimeEntry.visible_condition(User.current)).
  352. sum("#{TimeEntry.table_name}.hours")
  353. map_total(total) {|t| t.to_f.round(2)}
  354. end
  355. # Returns the issues
  356. # Valid options are :order, :offset, :limit, :include, :conditions
  357. def issues(options={})
  358. order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
  359. # The default order of IssueQuery is issues.id DESC(by IssueQuery#default_sort_criteria)
  360. unless ["#{Issue.table_name}.id ASC", "#{Issue.table_name}.id DESC"].any?{|i| order_option.include?(i)}
  361. order_option << "#{Issue.table_name}.id DESC"
  362. end
  363. scope = base_scope.
  364. preload(:priority).
  365. includes(([:status, :project] + (options[:include] || [])).uniq).
  366. where(options[:conditions]).
  367. order(order_option).
  368. joins(joins_for_order_statement(order_option.join(','))).
  369. limit(options[:limit]).
  370. offset(options[:offset])
  371. scope =
  372. scope.preload(
  373. [:tracker, :author, :assigned_to, :fixed_version,
  374. :category, :attachments] & columns.map(&:name)
  375. )
  376. if has_custom_field_column?
  377. scope = scope.preload(:custom_values)
  378. end
  379. if has_column?(:watcher_users)
  380. scope = scope.preload(:watcher_users)
  381. end
  382. issues = scope.to_a
  383. if has_column?(:spent_hours)
  384. Issue.load_visible_spent_hours(issues)
  385. end
  386. if has_column?(:total_spent_hours)
  387. Issue.load_visible_total_spent_hours(issues)
  388. end
  389. if has_column?(:last_updated_by)
  390. Issue.load_visible_last_updated_by(issues)
  391. end
  392. if has_column?(:relations)
  393. Issue.load_visible_relations(issues)
  394. end
  395. if has_column?(:last_notes)
  396. Issue.load_visible_last_notes(issues)
  397. end
  398. issues
  399. rescue ::ActiveRecord::StatementInvalid => e
  400. raise StatementInvalid.new(e.message)
  401. end
  402. # Returns the issues ids
  403. def issue_ids(options={})
  404. order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
  405. # The default order of IssueQuery is issues.id DESC(by IssueQuery#default_sort_criteria)
  406. unless ["#{Issue.table_name}.id ASC", "#{Issue.table_name}.id DESC"].any?{|i| order_option.include?(i)}
  407. order_option << "#{Issue.table_name}.id DESC"
  408. end
  409. base_scope.
  410. includes(([:status, :project] + (options[:include] || [])).uniq).
  411. references(([:status, :project] + (options[:include] || [])).uniq).
  412. where(options[:conditions]).
  413. order(order_option).
  414. joins(joins_for_order_statement(order_option.join(','))).
  415. limit(options[:limit]).
  416. offset(options[:offset]).
  417. pluck(:id)
  418. rescue ::ActiveRecord::StatementInvalid => e
  419. raise StatementInvalid.new(e.message)
  420. end
  421. # Returns the journals
  422. # Valid options are :order, :offset, :limit
  423. def journals(options={})
  424. Journal.visible.
  425. joins(:issue => [:project, :status]).
  426. where(statement).
  427. order(options[:order]).
  428. limit(options[:limit]).
  429. offset(options[:offset]).
  430. preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
  431. to_a
  432. rescue ::ActiveRecord::StatementInvalid => e
  433. raise StatementInvalid.new(e.message)
  434. end
  435. # Returns the versions
  436. # Valid options are :conditions
  437. def versions(options={})
  438. Version.visible.
  439. where(project_statement).
  440. where(options[:conditions]).
  441. includes(:project).
  442. references(:project).
  443. to_a
  444. rescue ::ActiveRecord::StatementInvalid => e
  445. raise StatementInvalid.new(e.message)
  446. end
  447. def sql_for_notes_field(field, operator, value)
  448. subquery = "SELECT 1 FROM #{Journal.table_name}" +
  449. " WHERE #{Journal.table_name}.journalized_type='Issue' AND #{Journal.table_name}.journalized_id=#{Issue.table_name}.id" +
  450. " AND (#{sql_for_field field, operator.delete_prefix('!'), value, Journal.table_name, 'notes'})" +
  451. " AND (#{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)})"
  452. "#{operator.start_with?('!') ? "NOT EXISTS" : "EXISTS"} (#{subquery})"
  453. end
  454. def sql_for_updated_by_field(field, operator, value)
  455. neg = (operator == '!' ? 'NOT' : '')
  456. subquery = "SELECT 1 FROM #{Journal.table_name}" +
  457. " WHERE #{Journal.table_name}.journalized_type='Issue' AND #{Journal.table_name}.journalized_id=#{Issue.table_name}.id" +
  458. " AND (#{sql_for_field field, '=', value, Journal.table_name, 'user_id'})" +
  459. " AND (#{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)})"
  460. "#{neg} EXISTS (#{subquery})"
  461. end
  462. def sql_for_last_updated_by_field(field, operator, value)
  463. neg = (operator == '!' ? 'NOT' : '')
  464. subquery = "SELECT 1 FROM #{Journal.table_name} sj" +
  465. " WHERE sj.journalized_type='Issue' AND sj.journalized_id=#{Issue.table_name}.id AND (#{sql_for_field field, '=', value, 'sj', 'user_id'})" +
  466. " AND sj.id IN (SELECT MAX(#{Journal.table_name}.id) FROM #{Journal.table_name}" +
  467. " WHERE #{Journal.table_name}.journalized_type='Issue' AND #{Journal.table_name}.journalized_id=#{Issue.table_name}.id" +
  468. " AND (#{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)}))"
  469. "#{neg} EXISTS (#{subquery})"
  470. end
  471. def sql_for_spent_time_field(field, operator, value)
  472. first, second = value.first.to_f, value.second.to_f
  473. sql_op =
  474. case operator
  475. when "=", ">=", "<=" then "#{operator} #{first}"
  476. when "><" then "BETWEEN #{first} AND #{second}"
  477. when "*" then "> 0"
  478. when "!*" then "= 0"
  479. else
  480. return nil
  481. end
  482. "COALESCE((" +
  483. "SELECT ROUND(CAST(SUM(hours) AS DECIMAL(30,3)), 2) " +
  484. "FROM #{TimeEntry.table_name} " +
  485. "WHERE issue_id = #{Issue.table_name}.id), 0) #{sql_op}"
  486. end
  487. def sql_for_watcher_id_field(field, operator, value)
  488. db_table = Watcher.table_name
  489. me, others = value.partition {|id| ['0', User.current.id.to_s].include?(id)}
  490. sql =
  491. if others.any?
  492. "SELECT #{Issue.table_name}.id FROM #{Issue.table_name} " +
  493. "INNER JOIN #{db_table} ON #{Issue.table_name}.id = #{db_table}.watchable_id AND #{db_table}.watchable_type = 'Issue' " +
  494. "LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id " +
  495. "WHERE (" +
  496. sql_for_field(field, '=', me, db_table, 'user_id') +
  497. ') OR (' +
  498. Project.allowed_to_condition(User.current, :view_issue_watchers) +
  499. ' AND ' +
  500. sql_for_field(field, '=', others, db_table, 'user_id') +
  501. ')'
  502. else
  503. "SELECT #{db_table}.watchable_id FROM #{db_table} " +
  504. "WHERE #{db_table}.watchable_type='Issue' AND " +
  505. sql_for_field(field, '=', me, db_table, 'user_id')
  506. end
  507. "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (#{sql})"
  508. end
  509. def sql_for_member_of_group_field(field, operator, value)
  510. if operator == '*' # Any group
  511. groups = Group.givable
  512. operator = '=' # Override the operator since we want to find by assigned_to
  513. elsif operator == "!*"
  514. groups = Group.givable
  515. operator = '!' # Override the operator since we want to find by assigned_to
  516. else
  517. groups = Group.where(:id => value).to_a
  518. end
  519. groups ||= []
  520. members_of_groups = groups.inject([]) do |user_ids, group|
  521. user_ids + group.user_ids + [group.id]
  522. end.uniq.compact.sort.collect(&:to_s)
  523. '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
  524. end
  525. def sql_for_assigned_to_role_field(field, operator, value)
  526. case operator
  527. when "*", "!*" # Member / Not member
  528. sw = operator == "!*" ? 'NOT' : ''
  529. nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
  530. subquery =
  531. "SELECT 1" +
  532. " FROM #{Member.table_name}" +
  533. " WHERE #{Issue.table_name}.project_id = #{Member.table_name}.project_id AND #{Member.table_name}.user_id = #{Issue.table_name}.assigned_to_id"
  534. "(#{nl} #{sw} EXISTS (#{subquery}))"
  535. when "=", "!"
  536. role_cond =
  537. if value.any?
  538. "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
  539. else
  540. "1=0"
  541. end
  542. sw = operator == "!" ? 'NOT' : ''
  543. nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
  544. subquery =
  545. "SELECT 1" +
  546. " FROM #{Member.table_name} inner join #{MemberRole.table_name} on members.id = member_roles.member_id" +
  547. " WHERE #{Issue.table_name}.project_id = #{Member.table_name}.project_id AND #{Member.table_name}.user_id = #{Issue.table_name}.assigned_to_id AND #{role_cond}"
  548. "(#{nl} #{sw} EXISTS (#{subquery}))"
  549. end
  550. end
  551. def sql_for_fixed_version_status_field(field, operator, value)
  552. where = sql_for_field(field, operator, value, Version.table_name, "status")
  553. version_id_scope = project ? project.shared_versions : Version.visible
  554. version_ids = version_id_scope.where(where).pluck(:id)
  555. nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
  556. "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
  557. end
  558. def sql_for_fixed_version_due_date_field(field, operator, value)
  559. where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
  560. version_id_scope = project ? project.shared_versions : Version.visible
  561. version_ids = version_id_scope.where(where).pluck(:id)
  562. nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
  563. "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
  564. end
  565. def sql_for_is_private_field(field, operator, value)
  566. op = (operator == "=" ? 'IN' : 'NOT IN')
  567. va =
  568. value.map do |v|
  569. v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true
  570. end.uniq.join(',')
  571. "#{Issue.table_name}.is_private #{op} (#{va})"
  572. end
  573. def sql_for_attachment_field(field, operator, value)
  574. case operator
  575. when "*", "!*"
  576. e = (operator == "*" ? "EXISTS" : "NOT EXISTS")
  577. "#{e} (SELECT 1 FROM #{Attachment.table_name} a WHERE a.container_type = 'Issue' AND a.container_id = #{Issue.table_name}.id)"
  578. when "~", "!~", "*~"
  579. c = sql_contains("a.filename", value.first, :all_words => (operator != "*~"))
  580. e = (operator == "!~" ? "NOT EXISTS" : "EXISTS")
  581. "#{e} (SELECT 1 FROM #{Attachment.table_name} a WHERE a.container_type = 'Issue' AND a.container_id = #{Issue.table_name}.id AND (#{c}))"
  582. when "^", "$"
  583. c = sql_contains("a.filename", value.first, (operator == "^" ? :starts_with : :ends_with) => true)
  584. "EXISTS (SELECT 1 FROM #{Attachment.table_name} a WHERE a.container_type = 'Issue' AND a.container_id = #{Issue.table_name}.id AND (#{c}))"
  585. end
  586. end
  587. def sql_for_attachment_description_field(field, operator, value)
  588. cond_description = "a.description IS NOT NULL AND a.description <> ''"
  589. c =
  590. case operator
  591. when '*', '!*'
  592. (operator == '*' ? cond_description : "NOT (#{cond_description})")
  593. when '~', '!~', '*~'
  594. (operator == '~' ? '' : "#{cond_description} AND ") +
  595. sql_contains('a.description', value.first, :match => (operator != '!~'), :all_words => (operator != '*~'))
  596. when '^', '$'
  597. sql_contains('a.description', value.first, (operator == '^' ? :starts_with : :ends_with) => true)
  598. else
  599. '1=0'
  600. end
  601. "EXISTS (SELECT 1 FROM #{Attachment.table_name} a WHERE a.container_type = 'Issue' AND a.container_id = #{Issue.table_name}.id AND (#{c}))"
  602. end
  603. def sql_for_parent_id_field(field, operator, value)
  604. case operator
  605. when "="
  606. # accepts a comma separated list of ids
  607. ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
  608. if ids.present?
  609. "#{Issue.table_name}.parent_id IN (#{ids.join(",")})"
  610. else
  611. "1=0"
  612. end
  613. when "~"
  614. ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
  615. conditions = ids.filter_map do |id|
  616. root_id, lft, rgt = Issue.where(id: id).pick(:root_id, :lft, :rgt)
  617. if root_id && lft && rgt
  618. "(#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt})"
  619. else
  620. nil
  621. end
  622. end
  623. if conditions.any?
  624. "(#{conditions.join(' OR ')})"
  625. else
  626. "1=0"
  627. end
  628. when "!*"
  629. "#{Issue.table_name}.parent_id IS NULL"
  630. when "*"
  631. "#{Issue.table_name}.parent_id IS NOT NULL"
  632. end
  633. end
  634. def sql_for_child_id_field(field, operator, value)
  635. case operator
  636. when "="
  637. # accepts a comma separated list of child ids
  638. child_ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
  639. ids = Issue.where(:id => child_ids).pluck(:parent_id).compact.uniq
  640. if ids.present?
  641. "#{Issue.table_name}.id IN (#{ids.join(",")})"
  642. else
  643. "1=0"
  644. end
  645. when "~"
  646. root_id, lft, rgt = Issue.where(:id => value.first.to_i).pick(:root_id, :lft, :rgt)
  647. if root_id && lft && rgt
  648. "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
  649. else
  650. "1=0"
  651. end
  652. when "!*"
  653. "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
  654. when "*"
  655. "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
  656. end
  657. end
  658. def sql_for_updated_on_field(field, operator, value)
  659. case operator
  660. when "!*"
  661. "#{Issue.table_name}.updated_on = #{Issue.table_name}.created_on"
  662. when "*"
  663. "#{Issue.table_name}.updated_on > #{Issue.table_name}.created_on"
  664. else
  665. sql_for_field("updated_on", operator, value, Issue.table_name, "updated_on")
  666. end
  667. end
  668. def sql_for_issue_id_field(field, operator, value)
  669. if operator == "="
  670. # accepts a comma separated list of ids
  671. ids = value.first.to_s.scan(/\d+/).map(&:to_i)
  672. if ids.present?
  673. "#{Issue.table_name}.id IN (#{ids.join(",")})"
  674. else
  675. "1=0"
  676. end
  677. else
  678. sql_for_field("id", operator, value, Issue.table_name, "id")
  679. end
  680. end
  681. def sql_for_relations(field, operator, value, options={})
  682. relation_options = IssueRelation::TYPES[field]
  683. return relation_options unless relation_options
  684. relation_type = field
  685. join_column, target_join_column = "issue_from_id", "issue_to_id"
  686. if relation_options[:reverse] || options[:reverse]
  687. relation_type = relation_options[:reverse] || relation_type
  688. join_column, target_join_column = target_join_column, join_column
  689. end
  690. sql =
  691. case operator
  692. when "*", "!*"
  693. op = (operator == "*" ? 'IN' : 'NOT IN')
  694. "#{Issue.table_name}.id #{op}" \
  695. " (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column}" \
  696. " FROM #{IssueRelation.table_name}" \
  697. " WHERE #{IssueRelation.table_name}.relation_type =" \
  698. " '#{self.class.connection.quote_string(relation_type)}')"
  699. when "=", "!"
  700. ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
  701. if ids.present?
  702. op = (operator == "=" ? 'IN' : 'NOT IN')
  703. "#{Issue.table_name}.id #{op}" \
  704. " (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column}" \
  705. " FROM #{IssueRelation.table_name}" \
  706. " WHERE #{IssueRelation.table_name}.relation_type =" \
  707. " '#{self.class.connection.quote_string(relation_type)}'" \
  708. " AND #{IssueRelation.table_name}.#{target_join_column} IN (#{ids.join(",")}))"
  709. else
  710. "1=0"
  711. end
  712. when "=p", "=!p", "!p"
  713. op = (operator == "!p" ? 'NOT IN' : 'IN')
  714. comp = (operator == "=!p" ? '<>' : '=')
  715. "#{Issue.table_name}.id #{op}" \
  716. " (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column}" \
  717. " FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues" \
  718. " WHERE #{IssueRelation.table_name}.relation_type =" \
  719. " '#{self.class.connection.quote_string(relation_type)}'" \
  720. " AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id" \
  721. " AND relissues.project_id #{comp} #{value.first.to_i})"
  722. when "*o", "!o"
  723. op = (operator == "!o" ? 'NOT IN' : 'IN')
  724. "#{Issue.table_name}.id #{op}" \
  725. " (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column}" \
  726. " FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues" \
  727. " WHERE #{IssueRelation.table_name}.relation_type =" \
  728. " '#{self.class.connection.quote_string(relation_type)}'" \
  729. " AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id" \
  730. " AND relissues.status_id IN" \
  731. " (SELECT id FROM #{IssueStatus.table_name}" \
  732. " WHERE is_closed = #{self.class.connection.quoted_false}))"
  733. end
  734. if relation_options[:sym] == field && !options[:reverse]
  735. sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
  736. sql = sqls.join(["!", "!*", "!p", '!o'].include?(operator) ? " AND " : " OR ")
  737. end
  738. "(#{sql})"
  739. end
  740. def sql_for_project_status_field(field, operator, value, options={})
  741. sql_for_field(field, operator, value, Project.table_name, "status")
  742. end
  743. def sql_for_any_searchable_field(field, operator, value)
  744. question = value.first
  745. # Fetch search results only from the selected and visible (sub-)projects
  746. project_scope = Project.allowed_to(:view_issues)
  747. if project
  748. projects = project_scope.where(project_statement)
  749. elsif has_filter?('project_id')
  750. case values_for('project_id').first
  751. when 'mine'
  752. project_ids = User.current.projects.ids
  753. when 'bookmarks'
  754. project_ids = User.current.bookmarked_project_ids
  755. else
  756. project_ids = values_for('project_id')
  757. end
  758. projects = project_scope.where(
  759. sql_for_field('project_id', operator_for('project_id'), project_ids, Project.table_name, 'id')
  760. )
  761. else
  762. projects = nil
  763. end
  764. is_all_words =
  765. case operator
  766. when '~' then true
  767. when '*~', '!~' then false
  768. end
  769. is_open_issues = has_filter?('status_id') && operator_for('status_id') == 'o'
  770. fetcher = Redmine::Search::Fetcher.new(
  771. question, User.current, ['issue'], projects,
  772. all_words: is_all_words, open_issues: is_open_issues, attachments: '0'
  773. )
  774. ids = fetcher.result_ids.map(&:last)
  775. if ids.present?
  776. sw = operator == '!~' ? 'NOT' : ''
  777. "#{Issue.table_name}.id #{sw} IN (#{ids.join(',')})"
  778. else
  779. operator == '!~' ? '1=1' : '1=0'
  780. end
  781. end
  782. def find_assigned_to_id_filter_values(values)
  783. Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
  784. end
  785. alias :find_author_id_filter_values :find_assigned_to_id_filter_values
  786. IssueRelation::TYPES.each_key do |relation_type|
  787. alias_method :"sql_for_#{relation_type}_field", :sql_for_relations
  788. end
  789. def joins_for_order_statement(order_options)
  790. joins = [super]
  791. if order_options
  792. if order_options.include?('authors')
  793. joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
  794. end
  795. if order_options.include?('users')
  796. joins << "LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{queried_table_name}.assigned_to_id"
  797. end
  798. if order_options.include?('last_journal_user')
  799. joins <<
  800. "LEFT OUTER JOIN #{Journal.table_name}" \
  801. " ON #{Journal.table_name}.id = (SELECT MAX(#{Journal.table_name}.id)" \
  802. " FROM #{Journal.table_name}" \
  803. " WHERE #{Journal.table_name}.journalized_type = 'Issue'" \
  804. " AND #{Journal.table_name}.journalized_id = #{Issue.table_name}.id " \
  805. " AND #{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)})" \
  806. " LEFT OUTER JOIN #{User.table_name} last_journal_user" \
  807. " ON last_journal_user.id = #{Journal.table_name}.user_id"
  808. end
  809. if order_options.include?('versions')
  810. joins <<
  811. "LEFT OUTER JOIN #{Version.table_name}" \
  812. " ON #{Version.table_name}.id = #{queried_table_name}.fixed_version_id"
  813. end
  814. if order_options.include?('issue_categories')
  815. joins <<
  816. "LEFT OUTER JOIN #{IssueCategory.table_name}" \
  817. " ON #{IssueCategory.table_name}.id = #{queried_table_name}.category_id"
  818. end
  819. if order_options.include?('trackers')
  820. joins <<
  821. "LEFT OUTER JOIN #{Tracker.table_name}" \
  822. " ON #{Tracker.table_name}.id = #{queried_table_name}.tracker_id"
  823. end
  824. if order_options.include?('enumerations')
  825. joins <<
  826. "LEFT OUTER JOIN #{IssuePriority.table_name}" \
  827. " ON #{IssuePriority.table_name}.id = #{queried_table_name}.priority_id"
  828. end
  829. end
  830. joins.any? ? joins.join(' ') : nil
  831. end
  832. end