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.

queries_helper.rb 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006- Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. require 'redmine/export/csv'
  19. module QueriesHelper
  20. include ApplicationHelper
  21. def filters_options_for_select(query)
  22. ungrouped = []
  23. grouped = {label_string: [], label_date: [], label_time_tracking: [], label_attachment: []}
  24. query.available_filters.map do |field, field_options|
  25. if /^cf_\d+\./.match?(field)
  26. group = (field_options[:through] || field_options[:field]).try(:name)
  27. elsif field =~ /^(.+)\./
  28. # association filters
  29. group = :"field_#{$1}"
  30. elsif field_options[:type] == :relation
  31. group = :label_relations
  32. elsif field_options[:type] == :tree
  33. group = query.is_a?(IssueQuery) ? :label_relations : nil
  34. elsif %w(member_of_group assigned_to_role).include?(field)
  35. group = :field_assigned_to
  36. elsif field_options[:type] == :date_past || field_options[:type] == :date
  37. group = :label_date
  38. elsif %w(estimated_hours spent_time).include?(field)
  39. group = :label_time_tracking
  40. elsif %w(attachment attachment_description).include?(field)
  41. group = :label_attachment
  42. elsif [:string, :text, :search].include?(field_options[:type])
  43. group = :label_string
  44. end
  45. if group
  46. (grouped[group] ||= []) << [field_options[:name], field]
  47. else
  48. ungrouped << [field_options[:name], field]
  49. end
  50. end
  51. # Remove empty groups
  52. grouped.delete_if {|k, v| v.empty?}
  53. # Don't group dates if there's only one (eg. time entries filters)
  54. if grouped[:label_date].try(:size) == 1
  55. ungrouped << grouped.delete(:label_date).first
  56. end
  57. s = options_for_select([[]] + ungrouped)
  58. if grouped.present?
  59. localized_grouped = grouped.map {|k, v| [k.is_a?(Symbol) ? l(k) : k.to_s, v]}
  60. s << grouped_options_for_select(localized_grouped)
  61. end
  62. s
  63. end
  64. def query_filters_hidden_tags(query)
  65. tags = ''.html_safe
  66. query.filters.each do |field, options|
  67. tags << hidden_field_tag("f[]", field, :id => nil)
  68. tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
  69. options[:values].each do |value|
  70. tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
  71. end
  72. end
  73. tags
  74. end
  75. def query_columns_hidden_tags(query)
  76. tags = ''.html_safe
  77. query.columns.each do |column|
  78. tags << hidden_field_tag("c[]", column.name, :id => nil)
  79. end
  80. tags
  81. end
  82. def query_hidden_tags(query)
  83. query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
  84. end
  85. def group_by_column_select_tag(query)
  86. options = [[]] + query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}
  87. select_tag('group_by', options_for_select(options, @query.group_by))
  88. end
  89. def available_block_columns_tags(query)
  90. tags = ''.html_safe
  91. query.available_block_columns.each do |column|
  92. tags <<
  93. content_tag(
  94. 'label',
  95. check_box_tag(
  96. 'c[]', column.name.to_s,
  97. query.has_column?(column), :id => nil
  98. ) + " #{column.caption}", :class => 'inline'
  99. )
  100. end
  101. tags
  102. end
  103. def available_totalable_columns_tags(query, options={})
  104. tag_name = (options[:name] || 't') + '[]'
  105. tags = ''.html_safe
  106. query.available_totalable_columns.each do |column|
  107. tags <<
  108. content_tag(
  109. 'label',
  110. check_box_tag(
  111. tag_name, column.name.to_s,
  112. query.totalable_columns.include?(column), :id => nil
  113. ) + " #{column.caption}", :class => 'inline'
  114. )
  115. end
  116. tags << hidden_field_tag(tag_name, '')
  117. tags
  118. end
  119. def query_available_inline_columns_options(query)
  120. (query.available_inline_columns - query.columns).
  121. reject(&:frozen?).collect {|column| [column.caption, column.name]}
  122. end
  123. def query_selected_inline_columns_options(query)
  124. (query.inline_columns & query.available_inline_columns).
  125. reject(&:frozen?).collect {|column| [column.caption, column.name]}
  126. end
  127. def render_query_columns_selection(query, options={})
  128. tag_name = (options[:name] || 'c') + '[]'
  129. render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
  130. end
  131. def available_display_types_tags(query)
  132. tags = ''.html_safe
  133. query.available_display_types.each do |t|
  134. tags << radio_button_tag('display_type', t, @query.display_type == t, :id => "display_type_#{t}") +
  135. content_tag('label', l(:"label_display_type_#{t}"), :for => "display_type_#{t}", :class => "inline")
  136. end
  137. tags
  138. end
  139. def grouped_query_results(items, query, &block)
  140. result_count_by_group = query.result_count_by_group
  141. previous_group, first = false, true
  142. totals_by_group = query.totalable_columns.inject({}) do |h, column|
  143. h[column] = query.total_by_group_for(column)
  144. h
  145. end
  146. items.each do |item|
  147. group_name = group_count = nil
  148. if query.grouped?
  149. group = query.group_by_column.group_value(item)
  150. if first || group != previous_group
  151. if group.blank? && group != false
  152. group_name = "(#{l(:label_blank_value)})"
  153. else
  154. group_name = format_object(group)
  155. end
  156. group_name ||= ""
  157. group_count = result_count_by_group ? result_count_by_group[group] : nil
  158. group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
  159. end
  160. end
  161. yield item, group_name, group_count, group_totals
  162. previous_group, first = group, false
  163. end
  164. end
  165. def render_query_totals(query)
  166. return unless query.totalable_columns.present?
  167. totals = query.totalable_columns.map do |column|
  168. total_tag(column, query.total_for(column))
  169. end
  170. content_tag('p', totals.join(" ").html_safe, :class => "query-totals")
  171. end
  172. def total_tag(column, value)
  173. label = content_tag('span', "#{column.caption}:")
  174. value =
  175. if [:hours, :spent_hours, :total_spent_hours, :estimated_hours, :total_estimated_hours].include? column.name
  176. format_hours(value)
  177. else
  178. format_object(value)
  179. end
  180. value = content_tag('span', value, :class => 'value')
  181. content_tag('span', label + " " + value, :class => "total-for-#{column.name.to_s.dasherize}")
  182. end
  183. def column_header(query, column, options={})
  184. if column.sortable?
  185. css, order = nil, column.default_order
  186. if column.name.to_s == query.sort_criteria.first_key
  187. if query.sort_criteria.first_asc?
  188. css = 'sort asc icon icon-sorted-desc'
  189. order = 'desc'
  190. else
  191. css = 'sort desc icon icon-sorted-asc'
  192. order = 'asc'
  193. end
  194. end
  195. param_key = options[:sort_param] || :sort
  196. sort_param = {param_key => query.sort_criteria.add(column.name, order).to_param}
  197. sort_param = {$1 => {$2 => sort_param.values.first}} while sort_param.keys.first.to_s =~ /^(.+)\[(.+)\]$/
  198. link_options = {
  199. :title => l(:label_sort_by, "\"#{column.caption}\""),
  200. :class => css
  201. }
  202. if options[:sort_link_options]
  203. link_options.merge! options[:sort_link_options]
  204. end
  205. content =
  206. link_to(
  207. column.caption,
  208. {:params => request.query_parameters.deep_merge(sort_param)},
  209. link_options
  210. )
  211. else
  212. content = column.caption
  213. end
  214. content_tag('th', content, :class => column.css_classes)
  215. end
  216. def column_content(column, item)
  217. value = column.value_object(item)
  218. content =
  219. if value.is_a?(Array)
  220. values = value.filter_map {|v| column_value(column, item, v)}
  221. safe_join(values, ', ')
  222. else
  223. column_value(column, item, value)
  224. end
  225. call_hook(:helper_queries_column_content,
  226. {:content => content, :column => column, :item => item})
  227. content
  228. end
  229. def column_value(column, item, value)
  230. content =
  231. case column.name
  232. when :id
  233. link_to value, issue_path(item)
  234. when :subject
  235. link_to value, issue_path(item)
  236. when :parent, :'issue.parent'
  237. value ? (value.visible? ? link_to_issue(value, :subject => false) : "##{value.id}") : ''
  238. when :description
  239. item.description? ? content_tag('div', textilizable(item, :description), :class => "wiki") : ''
  240. when :last_notes
  241. item.last_notes.present? ? content_tag('div', textilizable(item, :last_notes), :class => "wiki") : ''
  242. when :done_ratio
  243. progress_bar(value)
  244. when :relations
  245. content_tag(
  246. 'span',
  247. value.to_s(item) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
  248. :class => value.css_classes_for(item))
  249. when :hours, :estimated_hours, :total_estimated_hours
  250. format_hours(value)
  251. when :spent_hours
  252. link_to_if(value > 0, format_hours(value), project_time_entries_path(item.project, :issue_id => "#{item.id}"))
  253. when :total_spent_hours
  254. link_to_if(value > 0, format_hours(value), project_time_entries_path(item.project, :issue_id => "~#{item.id}"))
  255. when :attachments
  256. value.to_a.map {|a| format_object(a)}.join(" ").html_safe
  257. else
  258. format_object(value)
  259. end
  260. call_hook(:helper_queries_column_value,
  261. {:content => content, :column => column, :item => item, :value => value})
  262. content
  263. end
  264. def csv_content(column, item)
  265. value = column.value_object(item)
  266. if value.is_a?(Array)
  267. value.filter_map {|v| csv_value(column, item, v)}.join(', ')
  268. else
  269. csv_value(column, item, value)
  270. end
  271. end
  272. def csv_value(column, object, value)
  273. case column.name
  274. when :attachments
  275. value.to_a.map {|a| a.filename}.join("\n")
  276. else
  277. format_object(value, false) do |value|
  278. case value.class.name
  279. when 'Float'
  280. sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
  281. when 'IssueRelation'
  282. value.to_s(object)
  283. when 'Issue'
  284. if object.is_a?(TimeEntry)
  285. value.visible? ? "#{value.tracker} ##{value.id}: #{value.subject}" : "##{value.id}"
  286. else
  287. value.id
  288. end
  289. else
  290. value
  291. end
  292. end
  293. end
  294. end
  295. def query_to_csv(items, query, options={})
  296. columns = query.columns
  297. Redmine::Export::CSV.generate(encoding: params[:encoding], field_separator: params[:field_separator]) do |csv|
  298. # csv header fields
  299. csv << columns.map {|c| c.caption.to_s}
  300. # csv lines
  301. items.each do |item|
  302. csv << columns.map {|c| csv_content(c, item)}
  303. end
  304. end
  305. end
  306. def filename_for_export(query, default_name)
  307. query_name = params[:query_name].presence || query.name
  308. query_name = default_name if query_name == '_' || query_name.blank?
  309. # Convert file names using the same rules as Wiki titles
  310. filename_for_content_disposition(Wiki.titleize(query_name).downcase)
  311. end
  312. # Retrieve query from session or build a new query
  313. def retrieve_query(klass=IssueQuery, use_session=true, options={})
  314. session_key = klass.name.underscore.to_sym
  315. if params[:query_id].present?
  316. scope = klass.where(:project_id => nil)
  317. scope = scope.or(klass.where(:project_id => @project)) if @project
  318. @query = scope.find(params[:query_id])
  319. raise ::Unauthorized unless @query.visible?
  320. @query.project = @project
  321. session[session_key] = {:id => @query.id, :project_id => @query.project_id} if use_session
  322. elsif api_request? || params[:set_filter] || !use_session ||
  323. session[session_key].nil? ||
  324. session[session_key][:project_id] != (@project ? @project.id : nil)
  325. # Give it a name, required to be valid
  326. @query = klass.new(:name => "_", :project => @project)
  327. @query.build_from_params(params, options[:defaults])
  328. if use_session
  329. session[session_key] = {
  330. :project_id => @query.project_id,
  331. :filters => @query.filters,
  332. :group_by => @query.group_by,
  333. :column_names => @query.column_names,
  334. :totalable_names => @query.totalable_names,
  335. :sort => @query.sort_criteria.to_a
  336. }
  337. end
  338. else
  339. # retrieve from session
  340. @query = nil
  341. @query = klass.find_by_id(session[session_key][:id]) if session[session_key][:id]
  342. @query ||=
  343. klass.new(
  344. :name => "_",
  345. :filters => session[session_key][:filters],
  346. :group_by => session[session_key][:group_by],
  347. :column_names => session[session_key][:column_names],
  348. :totalable_names => session[session_key][:totalable_names],
  349. :sort_criteria => session[session_key][:sort]
  350. )
  351. @query.project = @project
  352. end
  353. if params[:sort].present?
  354. @query.sort_criteria = params[:sort]
  355. if use_session
  356. session[session_key] ||= {}
  357. session[session_key][:sort] = @query.sort_criteria.to_a
  358. end
  359. end
  360. @query
  361. end
  362. def retrieve_query_from_session(klass=IssueQuery)
  363. session_key = klass.name.underscore.to_sym
  364. session_data = session[session_key]
  365. if session_data
  366. if session_data[:id]
  367. @query = IssueQuery.find_by_id(session_data[:id])
  368. return unless @query
  369. else
  370. @query =
  371. IssueQuery.new(
  372. :name => "_",
  373. :filters => session_data[:filters],
  374. :group_by => session_data[:group_by],
  375. :column_names => session_data[:column_names],
  376. :totalable_names => session_data[:totalable_names],
  377. :sort_criteria => session[session_key][:sort]
  378. )
  379. end
  380. if session_data.has_key?(:project_id)
  381. @query.project_id = session_data[:project_id]
  382. else
  383. @query.project = @project
  384. end
  385. @query
  386. else
  387. @query = klass.default project: @project
  388. end
  389. end
  390. # Returns the query definition as hidden field tags
  391. def query_as_hidden_field_tags(query)
  392. tags = hidden_field_tag("set_filter", "1", :id => nil)
  393. if query.filters.present?
  394. query.filters.each do |field, filter|
  395. tags << hidden_field_tag("f[]", field, :id => nil)
  396. tags << hidden_field_tag("op[#{field}]", filter[:operator], :id => nil)
  397. filter[:values].each do |value|
  398. tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
  399. end
  400. end
  401. else
  402. tags << hidden_field_tag("f[]", "", :id => nil)
  403. end
  404. query.columns.each do |column|
  405. tags << hidden_field_tag("c[]", column.name, :id => nil)
  406. end
  407. if query.totalable_names.present?
  408. query.totalable_names.each do |name|
  409. tags << hidden_field_tag("t[]", name, :id => nil)
  410. end
  411. end
  412. if query.group_by.present?
  413. tags << hidden_field_tag("group_by", query.group_by, :id => nil)
  414. end
  415. if query.sort_criteria.present?
  416. tags << hidden_field_tag("sort", query.sort_criteria.to_param, :id => nil)
  417. end
  418. tags
  419. end
  420. def query_hidden_sort_tag(query)
  421. hidden_field_tag("sort", query.sort_criteria.to_param, :id => nil)
  422. end
  423. # Returns the queries that are rendered in the sidebar
  424. def sidebar_queries(klass, project)
  425. klass.visible.global_or_on_project(@project).sorted.to_a
  426. end
  427. # Renders a group of queries
  428. def query_links(title, queries)
  429. return '' if queries.empty?
  430. # links to #index on issues/show
  431. url_params =
  432. if controller_name == 'issues'
  433. {:controller => 'issues', :action => 'index', :project_id => @project}
  434. elsif controller_name == 'admin' && action_name == 'projects'
  435. {:admin_projects => '1'}
  436. else
  437. {}
  438. end
  439. default_query_by_class = {}
  440. content_tag('h3', title) + "\n" +
  441. content_tag(
  442. 'ul',
  443. queries.collect do |query|
  444. css = +'query'
  445. clear_link = +''
  446. clear_link_param = {:set_filter => 1, :sort => '', :project_id => @project}
  447. default_query =
  448. default_query_by_class[query.class] ||= query.class.default(project: @project)
  449. if query == default_query
  450. css << ' default'
  451. clear_link_param[:without_default] = 1
  452. end
  453. if query == @query
  454. css << ' selected'
  455. clear_link += link_to_clear_query(clear_link_param)
  456. end
  457. content_tag('li',
  458. link_to(query.name,
  459. url_params.merge(:query_id => query),
  460. :class => css) +
  461. clear_link.html_safe)
  462. end.join("\n").html_safe,
  463. :class => 'queries'
  464. ) + "\n"
  465. end
  466. def link_to_clear_query(params = {:set_filter => 1, :sort => '', :project_id => @project})
  467. link_to(
  468. l(:button_clear),
  469. params,
  470. :class => 'icon-only icon-clear-query',
  471. :title => l(:button_clear)
  472. )
  473. end
  474. # Renders the list of queries for the sidebar
  475. def render_sidebar_queries(klass, project)
  476. queries = sidebar_queries(klass, project)
  477. out = ''.html_safe
  478. out << query_links(l(:label_my_queries), queries.select(&:is_private?))
  479. out << query_links(l(:label_query_plural), queries.reject(&:is_private?))
  480. out
  481. end
  482. end