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

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