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

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