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

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