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

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