##// END OF EJS Templates
Fixed that Link custom fields are not displayed as links on the issue list (#16496)....
Jean-Philippe Lang -
r12779:22b2a1f69927
parent child
Show More
@@ -1,1349 +1,1352
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28
29 29 extend Forwardable
30 30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31 31
32 32 # Return true if user is authorized for controller/action, otherwise false
33 33 def authorize_for(controller, action)
34 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 35 end
36 36
37 37 # Display a link if user is authorized
38 38 #
39 39 # @param [String] name Anchor text (passed to link_to)
40 40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 41 # @param [optional, Hash] html_options Options passed to link_to
42 42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active? || (User.current.admin? && user.logged?)
52 52 link_to name, user_path(user), :class => user.css_classes
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 # Displays a link to +issue+ with its subject.
62 62 # Examples:
63 63 #
64 64 # link_to_issue(issue) # => Defect #6: This is the subject
65 65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 66 # link_to_issue(issue, :subject => false) # => Defect #6
67 67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 69 #
70 70 def link_to_issue(issue, options={})
71 71 title = nil
72 72 subject = nil
73 73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 74 if options[:subject] == false
75 75 title = issue.subject.truncate(60)
76 76 else
77 77 subject = issue.subject
78 78 if truncate_length = options[:truncate]
79 79 subject = subject.truncate(truncate_length)
80 80 end
81 81 end
82 82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 83 s = link_to(text, issue_path(issue, :only_path => only_path),
84 84 :class => issue.css_classes, :title => title)
85 85 s << h(": #{subject}") if subject
86 86 s = h("#{issue.project} - ") + s if options[:project]
87 87 s
88 88 end
89 89
90 90 # Generates a link to an attachment.
91 91 # Options:
92 92 # * :text - Link text (default to attachment filename)
93 93 # * :download - Force download (default: false)
94 94 def link_to_attachment(attachment, options={})
95 95 text = options.delete(:text) || attachment.filename
96 96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 97 html_options = options.slice!(:only_path)
98 98 url = send(route_method, attachment, attachment.filename, options)
99 99 link_to text, url, html_options
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, repository, options={})
106 106 if repository.is_a?(Project)
107 107 repository = repository.repository
108 108 end
109 109 text = options.delete(:text) || format_revision(revision)
110 110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 111 link_to(
112 112 h(text),
113 113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 114 :title => l(:label_revision_id, format_revision(revision))
115 115 )
116 116 end
117 117
118 118 # Generates a link to a message
119 119 def link_to_message(message, options={}, html_options = nil)
120 120 link_to(
121 121 message.subject.truncate(60),
122 122 board_message_path(message.board_id, message.parent_id || message.id, {
123 123 :r => (message.parent_id && message.id),
124 124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 125 }.merge(options)),
126 126 html_options
127 127 )
128 128 end
129 129
130 130 # Generates a link to a project if active
131 131 # Examples:
132 132 #
133 133 # link_to_project(project) # => link to the specified project overview
134 134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 136 #
137 137 def link_to_project(project, options={}, html_options = nil)
138 138 if project.archived?
139 139 h(project.name)
140 140 elsif options.key?(:action)
141 141 ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
142 142 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
143 143 link_to project.name, url, html_options
144 144 else
145 145 link_to project.name, project_path(project, options), html_options
146 146 end
147 147 end
148 148
149 149 # Generates a link to a project settings if active
150 150 def link_to_project_settings(project, options={}, html_options=nil)
151 151 if project.active?
152 152 link_to project.name, settings_project_path(project, options), html_options
153 153 elsif project.archived?
154 154 h(project.name)
155 155 else
156 156 link_to project.name, project_path(project, options), html_options
157 157 end
158 158 end
159 159
160 160 # Helper that formats object for html or text rendering
161 def format_object(object, html=true)
161 def format_object(object, html=true, &block)
162 if block_given?
163 object = yield object
164 end
162 165 case object.class.name
163 166 when 'Array'
164 167 object.map {|o| format_object(o, html)}.join(', ').html_safe
165 168 when 'Time'
166 169 format_time(object)
167 170 when 'Date'
168 171 format_date(object)
169 172 when 'Fixnum'
170 173 object.to_s
171 174 when 'Float'
172 175 sprintf "%.2f", object
173 176 when 'User'
174 177 html ? link_to_user(object) : object.to_s
175 178 when 'Project'
176 179 html ? link_to_project(object) : object.to_s
177 180 when 'Version'
178 181 html ? link_to(object.name, version_path(object)) : object.to_s
179 182 when 'TrueClass'
180 183 l(:general_text_Yes)
181 184 when 'FalseClass'
182 185 l(:general_text_No)
183 186 when 'Issue'
184 187 object.visible? && html ? link_to_issue(object) : "##{object.id}"
185 188 when 'CustomValue', 'CustomFieldValue'
186 189 if object.custom_field
187 190 f = object.custom_field.format.formatted_custom_value(self, object, html)
188 191 if f.nil? || f.is_a?(String)
189 192 f
190 193 else
191 format_object(f, html)
194 format_object(f, html, &block)
192 195 end
193 196 else
194 197 object.value.to_s
195 198 end
196 199 else
197 200 html ? h(object) : object.to_s
198 201 end
199 202 end
200 203
201 204 def wiki_page_path(page, options={})
202 205 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
203 206 end
204 207
205 208 def thumbnail_tag(attachment)
206 209 link_to image_tag(thumbnail_path(attachment)),
207 210 named_attachment_path(attachment, attachment.filename),
208 211 :title => attachment.filename
209 212 end
210 213
211 214 def toggle_link(name, id, options={})
212 215 onclick = "$('##{id}').toggle(); "
213 216 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
214 217 onclick << "return false;"
215 218 link_to(name, "#", :onclick => onclick)
216 219 end
217 220
218 221 def image_to_function(name, function, html_options = {})
219 222 html_options.symbolize_keys!
220 223 tag(:input, html_options.merge({
221 224 :type => "image", :src => image_path(name),
222 225 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
223 226 }))
224 227 end
225 228
226 229 def format_activity_title(text)
227 230 h(truncate_single_line_raw(text, 100))
228 231 end
229 232
230 233 def format_activity_day(date)
231 234 date == User.current.today ? l(:label_today).titleize : format_date(date)
232 235 end
233 236
234 237 def format_activity_description(text)
235 238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
236 239 ).gsub(/[\r\n]+/, "<br />").html_safe
237 240 end
238 241
239 242 def format_version_name(version)
240 243 if version.project == @project
241 244 h(version)
242 245 else
243 246 h("#{version.project} - #{version}")
244 247 end
245 248 end
246 249
247 250 def due_date_distance_in_words(date)
248 251 if date
249 252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
250 253 end
251 254 end
252 255
253 256 # Renders a tree of projects as a nested set of unordered lists
254 257 # The given collection may be a subset of the whole project tree
255 258 # (eg. some intermediate nodes are private and can not be seen)
256 259 def render_project_nested_lists(projects)
257 260 s = ''
258 261 if projects.any?
259 262 ancestors = []
260 263 original_project = @project
261 264 projects.sort_by(&:lft).each do |project|
262 265 # set the project environment to please macros.
263 266 @project = project
264 267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
265 268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
266 269 else
267 270 ancestors.pop
268 271 s << "</li>"
269 272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
270 273 ancestors.pop
271 274 s << "</ul></li>\n"
272 275 end
273 276 end
274 277 classes = (ancestors.empty? ? 'root' : 'child')
275 278 s << "<li class='#{classes}'><div class='#{classes}'>"
276 279 s << h(block_given? ? yield(project) : project.name)
277 280 s << "</div>\n"
278 281 ancestors << project
279 282 end
280 283 s << ("</li></ul>\n" * ancestors.size)
281 284 @project = original_project
282 285 end
283 286 s.html_safe
284 287 end
285 288
286 289 def render_page_hierarchy(pages, node=nil, options={})
287 290 content = ''
288 291 if pages[node]
289 292 content << "<ul class=\"pages-hierarchy\">\n"
290 293 pages[node].each do |page|
291 294 content << "<li>"
292 295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
293 296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
294 297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
295 298 content << "</li>\n"
296 299 end
297 300 content << "</ul>\n"
298 301 end
299 302 content.html_safe
300 303 end
301 304
302 305 # Renders flash messages
303 306 def render_flash_messages
304 307 s = ''
305 308 flash.each do |k,v|
306 309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
307 310 end
308 311 s.html_safe
309 312 end
310 313
311 314 # Renders tabs and their content
312 315 def render_tabs(tabs, selected=params[:tab])
313 316 if tabs.any?
314 317 unless tabs.detect {|tab| tab[:name] == selected}
315 318 selected = nil
316 319 end
317 320 selected ||= tabs.first[:name]
318 321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
319 322 else
320 323 content_tag 'p', l(:label_no_data), :class => "nodata"
321 324 end
322 325 end
323 326
324 327 # Renders the project quick-jump box
325 328 def render_project_jump_box
326 329 return unless User.current.logged?
327 330 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
328 331 if projects.any?
329 332 options =
330 333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
331 334 '<option value="" disabled="disabled">---</option>').html_safe
332 335
333 336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
334 337 { :value => project_path(:id => p, :jump => current_menu_item) }
335 338 end
336 339
337 340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
338 341 end
339 342 end
340 343
341 344 def project_tree_options_for_select(projects, options = {})
342 345 s = ''
343 346 project_tree(projects) do |project, level|
344 347 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
345 348 tag_options = {:value => project.id}
346 349 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
347 350 tag_options[:selected] = 'selected'
348 351 else
349 352 tag_options[:selected] = nil
350 353 end
351 354 tag_options.merge!(yield(project)) if block_given?
352 355 s << content_tag('option', name_prefix + h(project), tag_options)
353 356 end
354 357 s.html_safe
355 358 end
356 359
357 360 # Yields the given block for each project with its level in the tree
358 361 #
359 362 # Wrapper for Project#project_tree
360 363 def project_tree(projects, &block)
361 364 Project.project_tree(projects, &block)
362 365 end
363 366
364 367 def principals_check_box_tags(name, principals)
365 368 s = ''
366 369 principals.each do |principal|
367 370 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
368 371 end
369 372 s.html_safe
370 373 end
371 374
372 375 # Returns a string for users/groups option tags
373 376 def principals_options_for_select(collection, selected=nil)
374 377 s = ''
375 378 if collection.include?(User.current)
376 379 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
377 380 end
378 381 groups = ''
379 382 collection.sort.each do |element|
380 383 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
381 384 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
382 385 end
383 386 unless groups.empty?
384 387 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
385 388 end
386 389 s.html_safe
387 390 end
388 391
389 392 # Options for the new membership projects combo-box
390 393 def options_for_membership_project_select(principal, projects)
391 394 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
392 395 options << project_tree_options_for_select(projects) do |p|
393 396 {:disabled => principal.projects.to_a.include?(p)}
394 397 end
395 398 options
396 399 end
397 400
398 401 def option_tag(name, text, value, selected=nil, options={})
399 402 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
400 403 end
401 404
402 405 # Truncates and returns the string as a single line
403 406 def truncate_single_line(string, *args)
404 407 ActiveSupport::Deprecation.warn(
405 408 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
406 409 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
407 410 # So, result is broken.
408 411 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
409 412 end
410 413
411 414 def truncate_single_line_raw(string, length)
412 415 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
413 416 end
414 417
415 418 # Truncates at line break after 250 characters or options[:length]
416 419 def truncate_lines(string, options={})
417 420 length = options[:length] || 250
418 421 if string.to_s =~ /\A(.{#{length}}.*?)$/m
419 422 "#{$1}..."
420 423 else
421 424 string
422 425 end
423 426 end
424 427
425 428 def anchor(text)
426 429 text.to_s.gsub(' ', '_')
427 430 end
428 431
429 432 def html_hours(text)
430 433 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
431 434 end
432 435
433 436 def authoring(created, author, options={})
434 437 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
435 438 end
436 439
437 440 def time_tag(time)
438 441 text = distance_of_time_in_words(Time.now, time)
439 442 if @project
440 443 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
441 444 else
442 445 content_tag('abbr', text, :title => format_time(time))
443 446 end
444 447 end
445 448
446 449 def syntax_highlight_lines(name, content)
447 450 lines = []
448 451 syntax_highlight(name, content).each_line { |line| lines << line }
449 452 lines
450 453 end
451 454
452 455 def syntax_highlight(name, content)
453 456 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
454 457 end
455 458
456 459 def to_path_param(path)
457 460 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
458 461 str.blank? ? nil : str
459 462 end
460 463
461 464 def reorder_links(name, url, method = :post)
462 465 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
463 466 url.merge({"#{name}[move_to]" => 'highest'}),
464 467 :method => method, :title => l(:label_sort_highest)) +
465 468 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
466 469 url.merge({"#{name}[move_to]" => 'higher'}),
467 470 :method => method, :title => l(:label_sort_higher)) +
468 471 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
469 472 url.merge({"#{name}[move_to]" => 'lower'}),
470 473 :method => method, :title => l(:label_sort_lower)) +
471 474 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
472 475 url.merge({"#{name}[move_to]" => 'lowest'}),
473 476 :method => method, :title => l(:label_sort_lowest))
474 477 end
475 478
476 479 def breadcrumb(*args)
477 480 elements = args.flatten
478 481 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
479 482 end
480 483
481 484 def other_formats_links(&block)
482 485 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
483 486 yield Redmine::Views::OtherFormatsBuilder.new(self)
484 487 concat('</p>'.html_safe)
485 488 end
486 489
487 490 def page_header_title
488 491 if @project.nil? || @project.new_record?
489 492 h(Setting.app_title)
490 493 else
491 494 b = []
492 495 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
493 496 if ancestors.any?
494 497 root = ancestors.shift
495 498 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
496 499 if ancestors.size > 2
497 500 b << "\xe2\x80\xa6"
498 501 ancestors = ancestors[-2, 2]
499 502 end
500 503 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
501 504 end
502 505 b << h(@project)
503 506 b.join(" \xc2\xbb ").html_safe
504 507 end
505 508 end
506 509
507 510 # Returns a h2 tag and sets the html title with the given arguments
508 511 def title(*args)
509 512 strings = args.map do |arg|
510 513 if arg.is_a?(Array) && arg.size >= 2
511 514 link_to(*arg)
512 515 else
513 516 h(arg.to_s)
514 517 end
515 518 end
516 519 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
517 520 content_tag('h2', strings.join(' &#187; ').html_safe)
518 521 end
519 522
520 523 # Sets the html title
521 524 # Returns the html title when called without arguments
522 525 # Current project name and app_title and automatically appended
523 526 # Exemples:
524 527 # html_title 'Foo', 'Bar'
525 528 # html_title # => 'Foo - Bar - My Project - Redmine'
526 529 def html_title(*args)
527 530 if args.empty?
528 531 title = @html_title || []
529 532 title << @project.name if @project
530 533 title << Setting.app_title unless Setting.app_title == title.last
531 534 title.reject(&:blank?).join(' - ')
532 535 else
533 536 @html_title ||= []
534 537 @html_title += args
535 538 end
536 539 end
537 540
538 541 # Returns the theme, controller name, and action as css classes for the
539 542 # HTML body.
540 543 def body_css_classes
541 544 css = []
542 545 if theme = Redmine::Themes.theme(Setting.ui_theme)
543 546 css << 'theme-' + theme.name
544 547 end
545 548
546 549 css << 'project-' + @project.identifier if @project && @project.identifier.present?
547 550 css << 'controller-' + controller_name
548 551 css << 'action-' + action_name
549 552 css.join(' ')
550 553 end
551 554
552 555 def accesskey(s)
553 556 @used_accesskeys ||= []
554 557 key = Redmine::AccessKeys.key_for(s)
555 558 return nil if @used_accesskeys.include?(key)
556 559 @used_accesskeys << key
557 560 key
558 561 end
559 562
560 563 # Formats text according to system settings.
561 564 # 2 ways to call this method:
562 565 # * with a String: textilizable(text, options)
563 566 # * with an object and one of its attribute: textilizable(issue, :description, options)
564 567 def textilizable(*args)
565 568 options = args.last.is_a?(Hash) ? args.pop : {}
566 569 case args.size
567 570 when 1
568 571 obj = options[:object]
569 572 text = args.shift
570 573 when 2
571 574 obj = args.shift
572 575 attr = args.shift
573 576 text = obj.send(attr).to_s
574 577 else
575 578 raise ArgumentError, 'invalid arguments to textilizable'
576 579 end
577 580 return '' if text.blank?
578 581 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
579 582 only_path = options.delete(:only_path) == false ? false : true
580 583
581 584 text = text.dup
582 585 macros = catch_macros(text)
583 586 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
584 587
585 588 @parsed_headings = []
586 589 @heading_anchors = {}
587 590 @current_section = 0 if options[:edit_section_links]
588 591
589 592 parse_sections(text, project, obj, attr, only_path, options)
590 593 text = parse_non_pre_blocks(text, obj, macros) do |text|
591 594 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
592 595 send method_name, text, project, obj, attr, only_path, options
593 596 end
594 597 end
595 598 parse_headings(text, project, obj, attr, only_path, options)
596 599
597 600 if @parsed_headings.any?
598 601 replace_toc(text, @parsed_headings)
599 602 end
600 603
601 604 text.html_safe
602 605 end
603 606
604 607 def parse_non_pre_blocks(text, obj, macros)
605 608 s = StringScanner.new(text)
606 609 tags = []
607 610 parsed = ''
608 611 while !s.eos?
609 612 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
610 613 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
611 614 if tags.empty?
612 615 yield text
613 616 inject_macros(text, obj, macros) if macros.any?
614 617 else
615 618 inject_macros(text, obj, macros, false) if macros.any?
616 619 end
617 620 parsed << text
618 621 if tag
619 622 if closing
620 623 if tags.last == tag.downcase
621 624 tags.pop
622 625 end
623 626 else
624 627 tags << tag.downcase
625 628 end
626 629 parsed << full_tag
627 630 end
628 631 end
629 632 # Close any non closing tags
630 633 while tag = tags.pop
631 634 parsed << "</#{tag}>"
632 635 end
633 636 parsed
634 637 end
635 638
636 639 def parse_inline_attachments(text, project, obj, attr, only_path, options)
637 640 # when using an image link, try to use an attachment, if possible
638 641 attachments = options[:attachments] || []
639 642 attachments += obj.attachments if obj.respond_to?(:attachments)
640 643 if attachments.present?
641 644 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
642 645 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
643 646 # search for the picture in attachments
644 647 if found = Attachment.latest_attach(attachments, filename)
645 648 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
646 649 desc = found.description.to_s.gsub('"', '')
647 650 if !desc.blank? && alttext.blank?
648 651 alt = " title=\"#{desc}\" alt=\"#{desc}\""
649 652 end
650 653 "src=\"#{image_url}\"#{alt}"
651 654 else
652 655 m
653 656 end
654 657 end
655 658 end
656 659 end
657 660
658 661 # Wiki links
659 662 #
660 663 # Examples:
661 664 # [[mypage]]
662 665 # [[mypage|mytext]]
663 666 # wiki links can refer other project wikis, using project name or identifier:
664 667 # [[project:]] -> wiki starting page
665 668 # [[project:|mytext]]
666 669 # [[project:mypage]]
667 670 # [[project:mypage|mytext]]
668 671 def parse_wiki_links(text, project, obj, attr, only_path, options)
669 672 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
670 673 link_project = project
671 674 esc, all, page, title = $1, $2, $3, $5
672 675 if esc.nil?
673 676 if page =~ /^([^\:]+)\:(.*)$/
674 677 identifier, page = $1, $2
675 678 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
676 679 title ||= identifier if page.blank?
677 680 end
678 681
679 682 if link_project && link_project.wiki
680 683 # extract anchor
681 684 anchor = nil
682 685 if page =~ /^(.+?)\#(.+)$/
683 686 page, anchor = $1, $2
684 687 end
685 688 anchor = sanitize_anchor_name(anchor) if anchor.present?
686 689 # check if page exists
687 690 wiki_page = link_project.wiki.find_page(page)
688 691 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
689 692 "##{anchor}"
690 693 else
691 694 case options[:wiki_links]
692 695 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
693 696 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
694 697 else
695 698 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
696 699 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
697 700 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
698 701 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
699 702 end
700 703 end
701 704 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
702 705 else
703 706 # project or wiki doesn't exist
704 707 all
705 708 end
706 709 else
707 710 all
708 711 end
709 712 end
710 713 end
711 714
712 715 # Redmine links
713 716 #
714 717 # Examples:
715 718 # Issues:
716 719 # #52 -> Link to issue #52
717 720 # Changesets:
718 721 # r52 -> Link to revision 52
719 722 # commit:a85130f -> Link to scmid starting with a85130f
720 723 # Documents:
721 724 # document#17 -> Link to document with id 17
722 725 # document:Greetings -> Link to the document with title "Greetings"
723 726 # document:"Some document" -> Link to the document with title "Some document"
724 727 # Versions:
725 728 # version#3 -> Link to version with id 3
726 729 # version:1.0.0 -> Link to version named "1.0.0"
727 730 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
728 731 # Attachments:
729 732 # attachment:file.zip -> Link to the attachment of the current object named file.zip
730 733 # Source files:
731 734 # source:some/file -> Link to the file located at /some/file in the project's repository
732 735 # source:some/file@52 -> Link to the file's revision 52
733 736 # source:some/file#L120 -> Link to line 120 of the file
734 737 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
735 738 # export:some/file -> Force the download of the file
736 739 # Forum messages:
737 740 # message#1218 -> Link to message with id 1218
738 741 # Projects:
739 742 # project:someproject -> Link to project named "someproject"
740 743 # project#3 -> Link to project with id 3
741 744 #
742 745 # Links can refer other objects from other projects, using project identifier:
743 746 # identifier:r52
744 747 # identifier:document:"Some document"
745 748 # identifier:version:1.0.0
746 749 # identifier:source:some/file
747 750 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
748 751 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
749 752 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
750 753 link = nil
751 754 project = default_project
752 755 if project_identifier
753 756 project = Project.visible.find_by_identifier(project_identifier)
754 757 end
755 758 if esc.nil?
756 759 if prefix.nil? && sep == 'r'
757 760 if project
758 761 repository = nil
759 762 if repo_identifier
760 763 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 764 else
762 765 repository = project.repository
763 766 end
764 767 # project.changesets.visible raises an SQL error because of a double join on repositories
765 768 if repository &&
766 769 (changeset = Changeset.visible.
767 770 find_by_repository_id_and_revision(repository.id, identifier))
768 771 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
769 772 {:only_path => only_path, :controller => 'repositories',
770 773 :action => 'revision', :id => project,
771 774 :repository_id => repository.identifier_param,
772 775 :rev => changeset.revision},
773 776 :class => 'changeset',
774 777 :title => truncate_single_line_raw(changeset.comments, 100))
775 778 end
776 779 end
777 780 elsif sep == '#'
778 781 oid = identifier.to_i
779 782 case prefix
780 783 when nil
781 784 if oid.to_s == identifier &&
782 785 issue = Issue.visible.includes(:status).find_by_id(oid)
783 786 anchor = comment_id ? "note-#{comment_id}" : nil
784 787 link = link_to(h("##{oid}#{comment_suffix}"),
785 788 {:only_path => only_path, :controller => 'issues',
786 789 :action => 'show', :id => oid, :anchor => anchor},
787 790 :class => issue.css_classes,
788 791 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
789 792 end
790 793 when 'document'
791 794 if document = Document.visible.find_by_id(oid)
792 795 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
793 796 :class => 'document'
794 797 end
795 798 when 'version'
796 799 if version = Version.visible.find_by_id(oid)
797 800 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
798 801 :class => 'version'
799 802 end
800 803 when 'message'
801 804 if message = Message.visible.includes(:parent).find_by_id(oid)
802 805 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
803 806 end
804 807 when 'forum'
805 808 if board = Board.visible.find_by_id(oid)
806 809 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
807 810 :class => 'board'
808 811 end
809 812 when 'news'
810 813 if news = News.visible.find_by_id(oid)
811 814 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
812 815 :class => 'news'
813 816 end
814 817 when 'project'
815 818 if p = Project.visible.find_by_id(oid)
816 819 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
817 820 end
818 821 end
819 822 elsif sep == ':'
820 823 # removes the double quotes if any
821 824 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
822 825 case prefix
823 826 when 'document'
824 827 if project && document = project.documents.visible.find_by_title(name)
825 828 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
826 829 :class => 'document'
827 830 end
828 831 when 'version'
829 832 if project && version = project.versions.visible.find_by_name(name)
830 833 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
831 834 :class => 'version'
832 835 end
833 836 when 'forum'
834 837 if project && board = project.boards.visible.find_by_name(name)
835 838 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
836 839 :class => 'board'
837 840 end
838 841 when 'news'
839 842 if project && news = project.news.visible.find_by_title(name)
840 843 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
841 844 :class => 'news'
842 845 end
843 846 when 'commit', 'source', 'export'
844 847 if project
845 848 repository = nil
846 849 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
847 850 repo_prefix, repo_identifier, name = $1, $2, $3
848 851 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
849 852 else
850 853 repository = project.repository
851 854 end
852 855 if prefix == 'commit'
853 856 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
854 857 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
855 858 :class => 'changeset',
856 859 :title => truncate_single_line_raw(changeset.comments, 100)
857 860 end
858 861 else
859 862 if repository && User.current.allowed_to?(:browse_repository, project)
860 863 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
861 864 path, rev, anchor = $1, $3, $5
862 865 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
863 866 :path => to_path_param(path),
864 867 :rev => rev,
865 868 :anchor => anchor},
866 869 :class => (prefix == 'export' ? 'source download' : 'source')
867 870 end
868 871 end
869 872 repo_prefix = nil
870 873 end
871 874 when 'attachment'
872 875 attachments = options[:attachments] || []
873 876 attachments += obj.attachments if obj.respond_to?(:attachments)
874 877 if attachments && attachment = Attachment.latest_attach(attachments, name)
875 878 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
876 879 end
877 880 when 'project'
878 881 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
879 882 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
880 883 end
881 884 end
882 885 end
883 886 end
884 887 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
885 888 end
886 889 end
887 890
888 891 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
889 892
890 893 def parse_sections(text, project, obj, attr, only_path, options)
891 894 return unless options[:edit_section_links]
892 895 text.gsub!(HEADING_RE) do
893 896 heading = $1
894 897 @current_section += 1
895 898 if @current_section > 1
896 899 content_tag('div',
897 900 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
898 901 :class => 'contextual',
899 902 :title => l(:button_edit_section),
900 903 :id => "section-#{@current_section}") + heading.html_safe
901 904 else
902 905 heading
903 906 end
904 907 end
905 908 end
906 909
907 910 # Headings and TOC
908 911 # Adds ids and links to headings unless options[:headings] is set to false
909 912 def parse_headings(text, project, obj, attr, only_path, options)
910 913 return if options[:headings] == false
911 914
912 915 text.gsub!(HEADING_RE) do
913 916 level, attrs, content = $2.to_i, $3, $4
914 917 item = strip_tags(content).strip
915 918 anchor = sanitize_anchor_name(item)
916 919 # used for single-file wiki export
917 920 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
918 921 @heading_anchors[anchor] ||= 0
919 922 idx = (@heading_anchors[anchor] += 1)
920 923 if idx > 1
921 924 anchor = "#{anchor}-#{idx}"
922 925 end
923 926 @parsed_headings << [level, anchor, item]
924 927 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
925 928 end
926 929 end
927 930
928 931 MACROS_RE = /(
929 932 (!)? # escaping
930 933 (
931 934 \{\{ # opening tag
932 935 ([\w]+) # macro name
933 936 (\(([^\n\r]*?)\))? # optional arguments
934 937 ([\n\r].*?[\n\r])? # optional block of text
935 938 \}\} # closing tag
936 939 )
937 940 )/mx unless const_defined?(:MACROS_RE)
938 941
939 942 MACRO_SUB_RE = /(
940 943 \{\{
941 944 macro\((\d+)\)
942 945 \}\}
943 946 )/x unless const_defined?(:MACRO_SUB_RE)
944 947
945 948 # Extracts macros from text
946 949 def catch_macros(text)
947 950 macros = {}
948 951 text.gsub!(MACROS_RE) do
949 952 all, macro = $1, $4.downcase
950 953 if macro_exists?(macro) || all =~ MACRO_SUB_RE
951 954 index = macros.size
952 955 macros[index] = all
953 956 "{{macro(#{index})}}"
954 957 else
955 958 all
956 959 end
957 960 end
958 961 macros
959 962 end
960 963
961 964 # Executes and replaces macros in text
962 965 def inject_macros(text, obj, macros, execute=true)
963 966 text.gsub!(MACRO_SUB_RE) do
964 967 all, index = $1, $2.to_i
965 968 orig = macros.delete(index)
966 969 if execute && orig && orig =~ MACROS_RE
967 970 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
968 971 if esc.nil?
969 972 h(exec_macro(macro, obj, args, block) || all)
970 973 else
971 974 h(all)
972 975 end
973 976 elsif orig
974 977 h(orig)
975 978 else
976 979 h(all)
977 980 end
978 981 end
979 982 end
980 983
981 984 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
982 985
983 986 # Renders the TOC with given headings
984 987 def replace_toc(text, headings)
985 988 text.gsub!(TOC_RE) do
986 989 left_align, right_align = $2, $3
987 990 # Keep only the 4 first levels
988 991 headings = headings.select{|level, anchor, item| level <= 4}
989 992 if headings.empty?
990 993 ''
991 994 else
992 995 div_class = 'toc'
993 996 div_class << ' right' if right_align
994 997 div_class << ' left' if left_align
995 998 out = "<ul class=\"#{div_class}\"><li>"
996 999 root = headings.map(&:first).min
997 1000 current = root
998 1001 started = false
999 1002 headings.each do |level, anchor, item|
1000 1003 if level > current
1001 1004 out << '<ul><li>' * (level - current)
1002 1005 elsif level < current
1003 1006 out << "</li></ul>\n" * (current - level) + "</li><li>"
1004 1007 elsif started
1005 1008 out << '</li><li>'
1006 1009 end
1007 1010 out << "<a href=\"##{anchor}\">#{item}</a>"
1008 1011 current = level
1009 1012 started = true
1010 1013 end
1011 1014 out << '</li></ul>' * (current - root)
1012 1015 out << '</li></ul>'
1013 1016 end
1014 1017 end
1015 1018 end
1016 1019
1017 1020 # Same as Rails' simple_format helper without using paragraphs
1018 1021 def simple_format_without_paragraph(text)
1019 1022 text.to_s.
1020 1023 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1021 1024 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1022 1025 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1023 1026 html_safe
1024 1027 end
1025 1028
1026 1029 def lang_options_for_select(blank=true)
1027 1030 (blank ? [["(auto)", ""]] : []) + languages_options
1028 1031 end
1029 1032
1030 1033 def label_tag_for(name, option_tags = nil, options = {})
1031 1034 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1032 1035 content_tag("label", label_text)
1033 1036 end
1034 1037
1035 1038 def labelled_form_for(*args, &proc)
1036 1039 args << {} unless args.last.is_a?(Hash)
1037 1040 options = args.last
1038 1041 if args.first.is_a?(Symbol)
1039 1042 options.merge!(:as => args.shift)
1040 1043 end
1041 1044 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1042 1045 form_for(*args, &proc)
1043 1046 end
1044 1047
1045 1048 def labelled_fields_for(*args, &proc)
1046 1049 args << {} unless args.last.is_a?(Hash)
1047 1050 options = args.last
1048 1051 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1049 1052 fields_for(*args, &proc)
1050 1053 end
1051 1054
1052 1055 def labelled_remote_form_for(*args, &proc)
1053 1056 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
1054 1057 args << {} unless args.last.is_a?(Hash)
1055 1058 options = args.last
1056 1059 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
1057 1060 form_for(*args, &proc)
1058 1061 end
1059 1062
1060 1063 def error_messages_for(*objects)
1061 1064 html = ""
1062 1065 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1063 1066 errors = objects.map {|o| o.errors.full_messages}.flatten
1064 1067 if errors.any?
1065 1068 html << "<div id='errorExplanation'><ul>\n"
1066 1069 errors.each do |error|
1067 1070 html << "<li>#{h error}</li>\n"
1068 1071 end
1069 1072 html << "</ul></div>\n"
1070 1073 end
1071 1074 html.html_safe
1072 1075 end
1073 1076
1074 1077 def delete_link(url, options={})
1075 1078 options = {
1076 1079 :method => :delete,
1077 1080 :data => {:confirm => l(:text_are_you_sure)},
1078 1081 :class => 'icon icon-del'
1079 1082 }.merge(options)
1080 1083
1081 1084 link_to l(:button_delete), url, options
1082 1085 end
1083 1086
1084 1087 def preview_link(url, form, target='preview', options={})
1085 1088 content_tag 'a', l(:label_preview), {
1086 1089 :href => "#",
1087 1090 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1088 1091 :accesskey => accesskey(:preview)
1089 1092 }.merge(options)
1090 1093 end
1091 1094
1092 1095 def link_to_function(name, function, html_options={})
1093 1096 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1094 1097 end
1095 1098
1096 1099 # Helper to render JSON in views
1097 1100 def raw_json(arg)
1098 1101 arg.to_json.to_s.gsub('/', '\/').html_safe
1099 1102 end
1100 1103
1101 1104 def back_url
1102 1105 url = params[:back_url]
1103 1106 if url.nil? && referer = request.env['HTTP_REFERER']
1104 1107 url = CGI.unescape(referer.to_s)
1105 1108 end
1106 1109 url
1107 1110 end
1108 1111
1109 1112 def back_url_hidden_field_tag
1110 1113 url = back_url
1111 1114 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1112 1115 end
1113 1116
1114 1117 def check_all_links(form_name)
1115 1118 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1116 1119 " | ".html_safe +
1117 1120 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1118 1121 end
1119 1122
1120 1123 def progress_bar(pcts, options={})
1121 1124 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1122 1125 pcts = pcts.collect(&:round)
1123 1126 pcts[1] = pcts[1] - pcts[0]
1124 1127 pcts << (100 - pcts[1] - pcts[0])
1125 1128 width = options[:width] || '100px;'
1126 1129 legend = options[:legend] || ''
1127 1130 content_tag('table',
1128 1131 content_tag('tr',
1129 1132 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1130 1133 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1131 1134 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1132 1135 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1133 1136 content_tag('p', legend, :class => 'percent').html_safe
1134 1137 end
1135 1138
1136 1139 def checked_image(checked=true)
1137 1140 if checked
1138 1141 image_tag 'toggle_check.png'
1139 1142 end
1140 1143 end
1141 1144
1142 1145 def context_menu(url)
1143 1146 unless @context_menu_included
1144 1147 content_for :header_tags do
1145 1148 javascript_include_tag('context_menu') +
1146 1149 stylesheet_link_tag('context_menu')
1147 1150 end
1148 1151 if l(:direction) == 'rtl'
1149 1152 content_for :header_tags do
1150 1153 stylesheet_link_tag('context_menu_rtl')
1151 1154 end
1152 1155 end
1153 1156 @context_menu_included = true
1154 1157 end
1155 1158 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1156 1159 end
1157 1160
1158 1161 def calendar_for(field_id)
1159 1162 include_calendar_headers_tags
1160 1163 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1161 1164 end
1162 1165
1163 1166 def include_calendar_headers_tags
1164 1167 unless @calendar_headers_tags_included
1165 1168 tags = javascript_include_tag("datepicker")
1166 1169 @calendar_headers_tags_included = true
1167 1170 content_for :header_tags do
1168 1171 start_of_week = Setting.start_of_week
1169 1172 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1170 1173 # Redmine uses 1..7 (monday..sunday) in settings and locales
1171 1174 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1172 1175 start_of_week = start_of_week.to_i % 7
1173 1176 tags << javascript_tag(
1174 1177 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1175 1178 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1176 1179 path_to_image('/images/calendar.png') +
1177 1180 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1178 1181 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1179 1182 "beforeShow: beforeShowDatePicker};")
1180 1183 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1181 1184 unless jquery_locale == 'en'
1182 1185 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1183 1186 end
1184 1187 tags
1185 1188 end
1186 1189 end
1187 1190 end
1188 1191
1189 1192 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1190 1193 # Examples:
1191 1194 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1192 1195 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1193 1196 #
1194 1197 def stylesheet_link_tag(*sources)
1195 1198 options = sources.last.is_a?(Hash) ? sources.pop : {}
1196 1199 plugin = options.delete(:plugin)
1197 1200 sources = sources.map do |source|
1198 1201 if plugin
1199 1202 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1200 1203 elsif current_theme && current_theme.stylesheets.include?(source)
1201 1204 current_theme.stylesheet_path(source)
1202 1205 else
1203 1206 source
1204 1207 end
1205 1208 end
1206 1209 super sources, options
1207 1210 end
1208 1211
1209 1212 # Overrides Rails' image_tag with themes and plugins support.
1210 1213 # Examples:
1211 1214 # image_tag('image.png') # => picks image.png from the current theme or defaults
1212 1215 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1213 1216 #
1214 1217 def image_tag(source, options={})
1215 1218 if plugin = options.delete(:plugin)
1216 1219 source = "/plugin_assets/#{plugin}/images/#{source}"
1217 1220 elsif current_theme && current_theme.images.include?(source)
1218 1221 source = current_theme.image_path(source)
1219 1222 end
1220 1223 super source, options
1221 1224 end
1222 1225
1223 1226 # Overrides Rails' javascript_include_tag with plugins support
1224 1227 # Examples:
1225 1228 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1226 1229 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1227 1230 #
1228 1231 def javascript_include_tag(*sources)
1229 1232 options = sources.last.is_a?(Hash) ? sources.pop : {}
1230 1233 if plugin = options.delete(:plugin)
1231 1234 sources = sources.map do |source|
1232 1235 if plugin
1233 1236 "/plugin_assets/#{plugin}/javascripts/#{source}"
1234 1237 else
1235 1238 source
1236 1239 end
1237 1240 end
1238 1241 end
1239 1242 super sources, options
1240 1243 end
1241 1244
1242 1245 # TODO: remove this in 2.5.0
1243 1246 def has_content?(name)
1244 1247 content_for?(name)
1245 1248 end
1246 1249
1247 1250 def sidebar_content?
1248 1251 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1249 1252 end
1250 1253
1251 1254 def view_layouts_base_sidebar_hook_response
1252 1255 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1253 1256 end
1254 1257
1255 1258 def email_delivery_enabled?
1256 1259 !!ActionMailer::Base.perform_deliveries
1257 1260 end
1258 1261
1259 1262 # Returns the avatar image tag for the given +user+ if avatars are enabled
1260 1263 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1261 1264 def avatar(user, options = { })
1262 1265 if Setting.gravatar_enabled?
1263 1266 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1264 1267 email = nil
1265 1268 if user.respond_to?(:mail)
1266 1269 email = user.mail
1267 1270 elsif user.to_s =~ %r{<(.+?)>}
1268 1271 email = $1
1269 1272 end
1270 1273 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1271 1274 else
1272 1275 ''
1273 1276 end
1274 1277 end
1275 1278
1276 1279 def sanitize_anchor_name(anchor)
1277 1280 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1278 1281 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1279 1282 else
1280 1283 # TODO: remove when ruby1.8 is no longer supported
1281 1284 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1282 1285 end
1283 1286 end
1284 1287
1285 1288 # Returns the javascript tags that are included in the html layout head
1286 1289 def javascript_heads
1287 1290 tags = javascript_include_tag('jquery-1.8.3-ui-1.9.2-ujs-2.0.3', 'application')
1288 1291 unless User.current.pref.warn_on_leaving_unsaved == '0'
1289 1292 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1290 1293 end
1291 1294 tags
1292 1295 end
1293 1296
1294 1297 def favicon
1295 1298 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1296 1299 end
1297 1300
1298 1301 # Returns the path to the favicon
1299 1302 def favicon_path
1300 1303 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1301 1304 image_path(icon)
1302 1305 end
1303 1306
1304 1307 # Returns the full URL to the favicon
1305 1308 def favicon_url
1306 1309 # TODO: use #image_url introduced in Rails4
1307 1310 path = favicon_path
1308 1311 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1309 1312 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1310 1313 end
1311 1314
1312 1315 def robot_exclusion_tag
1313 1316 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1314 1317 end
1315 1318
1316 1319 # Returns true if arg is expected in the API response
1317 1320 def include_in_api_response?(arg)
1318 1321 unless @included_in_api_response
1319 1322 param = params[:include]
1320 1323 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1321 1324 @included_in_api_response.collect!(&:strip)
1322 1325 end
1323 1326 @included_in_api_response.include?(arg.to_s)
1324 1327 end
1325 1328
1326 1329 # Returns options or nil if nometa param or X-Redmine-Nometa header
1327 1330 # was set in the request
1328 1331 def api_meta(options)
1329 1332 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1330 1333 # compatibility mode for activeresource clients that raise
1331 1334 # an error when unserializing an array with attributes
1332 1335 nil
1333 1336 else
1334 1337 options
1335 1338 end
1336 1339 end
1337 1340
1338 1341 private
1339 1342
1340 1343 def wiki_helper
1341 1344 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1342 1345 extend helper
1343 1346 return self
1344 1347 end
1345 1348
1346 1349 def link_to_content_update(text, url_params = {}, html_options = {})
1347 1350 link_to(text, url_params, html_options)
1348 1351 end
1349 1352 end
@@ -1,202 +1,198
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 include ApplicationHelper
22
21 23 def filters_options_for_select(query)
22 24 options_for_select(filters_options(query))
23 25 end
24 26
25 27 def filters_options(query)
26 28 options = [[]]
27 29 options += query.available_filters.map do |field, field_options|
28 30 [field_options[:name], field]
29 31 end
30 32 end
31 33
32 34 def query_filters_hidden_tags(query)
33 35 tags = ''.html_safe
34 36 query.filters.each do |field, options|
35 37 tags << hidden_field_tag("f[]", field, :id => nil)
36 38 tags << hidden_field_tag("op[#{field}]", options[:operator], :id => nil)
37 39 options[:values].each do |value|
38 40 tags << hidden_field_tag("v[#{field}][]", value, :id => nil)
39 41 end
40 42 end
41 43 tags
42 44 end
43 45
44 46 def query_columns_hidden_tags(query)
45 47 tags = ''.html_safe
46 48 query.columns.each do |column|
47 49 tags << hidden_field_tag("c[]", column.name, :id => nil)
48 50 end
49 51 tags
50 52 end
51 53
52 54 def query_hidden_tags(query)
53 55 query_filters_hidden_tags(query) + query_columns_hidden_tags(query)
54 56 end
55 57
56 58 def available_block_columns_tags(query)
57 59 tags = ''.html_safe
58 60 query.available_block_columns.each do |column|
59 61 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column), :id => nil) + " #{column.caption}", :class => 'inline')
60 62 end
61 63 tags
62 64 end
63 65
64 66 def query_available_inline_columns_options(query)
65 67 (query.available_inline_columns - query.columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
66 68 end
67 69
68 70 def query_selected_inline_columns_options(query)
69 71 (query.inline_columns & query.available_inline_columns).reject(&:frozen?).collect {|column| [column.caption, column.name]}
70 72 end
71 73
72 74 def render_query_columns_selection(query, options={})
73 75 tag_name = (options[:name] || 'c') + '[]'
74 76 render :partial => 'queries/columns', :locals => {:query => query, :tag_name => tag_name}
75 77 end
76 78
77 79 def column_header(column)
78 80 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
79 81 :default_order => column.default_order) :
80 82 content_tag('th', h(column.caption))
81 83 end
82 84
83 85 def column_content(column, issue)
84 value = column.value(issue)
86 value = column.value_object(issue)
85 87 if value.is_a?(Array)
86 88 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
87 89 else
88 90 column_value(column, issue, value)
89 91 end
90 92 end
91 93
92 94 def column_value(column, issue, value)
93 95 case column.name
94 96 when :id
95 97 link_to value, issue_path(issue)
96 98 when :subject
97 99 link_to value, issue_path(issue)
98 100 when :description
99 101 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
100 102 when :done_ratio
101 103 progress_bar(value, :width => '80px')
102 104 when :relations
103 105 other = value.other_issue(issue)
104 106 content_tag('span',
105 107 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
106 108 :class => value.css_classes_for(issue))
107 109 else
108 110 format_object(value)
109 111 end
110 112 end
111 113
112 114 def csv_content(column, issue)
113 value = column.value(issue)
115 value = column.value_object(issue)
114 116 if value.is_a?(Array)
115 117 value.collect {|v| csv_value(column, issue, v)}.compact.join(', ')
116 118 else
117 119 csv_value(column, issue, value)
118 120 end
119 121 end
120 122
121 123 def csv_value(column, issue, value)
122 case value.class.name
123 when 'Time'
124 format_time(value)
125 when 'Date'
126 format_date(value)
127 when 'Float'
128 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
129 when 'IssueRelation'
130 other = value.other_issue(issue)
131 l(value.label_for(issue)) + " ##{other.id}"
132 when 'TrueClass'
133 l(:general_text_Yes)
134 when 'FalseClass'
135 l(:general_text_No)
136 else
137 value.to_s
124 format_object(value, false) do |value|
125 case value.class.name
126 when 'Float'
127 sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator))
128 when 'IssueRelation'
129 other = value.other_issue(issue)
130 l(value.label_for(issue)) + " ##{other.id}"
131 else
132 value
133 end
138 134 end
139 135 end
140 136
141 137 def query_to_csv(items, query, options={})
142 138 encoding = l(:general_csv_encoding)
143 139 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
144 140 query.available_block_columns.each do |column|
145 141 if options[column.name].present?
146 142 columns << column
147 143 end
148 144 end
149 145
150 146 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
151 147 # csv header fields
152 148 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
153 149 # csv lines
154 150 items.each do |item|
155 151 csv << columns.collect {|c| Redmine::CodesetUtil.from_utf8(csv_content(c, item), encoding) }
156 152 end
157 153 end
158 154 export
159 155 end
160 156
161 157 # Retrieve query from session or build a new query
162 158 def retrieve_query
163 159 if !params[:query_id].blank?
164 160 cond = "project_id IS NULL"
165 161 cond << " OR project_id = #{@project.id}" if @project
166 162 @query = IssueQuery.where(cond).find(params[:query_id])
167 163 raise ::Unauthorized unless @query.visible?
168 164 @query.project = @project
169 165 session[:query] = {:id => @query.id, :project_id => @query.project_id}
170 166 sort_clear
171 167 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
172 168 # Give it a name, required to be valid
173 169 @query = IssueQuery.new(:name => "_")
174 170 @query.project = @project
175 171 @query.build_from_params(params)
176 172 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
177 173 else
178 174 # retrieve from session
179 175 @query = nil
180 176 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
181 177 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
182 178 @query.project = @project
183 179 end
184 180 end
185 181
186 182 def retrieve_query_from_session
187 183 if session[:query]
188 184 if session[:query][:id]
189 185 @query = IssueQuery.find_by_id(session[:query][:id])
190 186 return unless @query
191 187 else
192 188 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
193 189 end
194 190 if session[:query].has_key?(:project_id)
195 191 @query.project_id = session[:query][:project_id]
196 192 else
197 193 @query.project = @project
198 194 end
199 195 @query
200 196 end
201 197 end
202 198 end
@@ -1,869 +1,884
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @inline = options.key?(:inline) ? options[:inline] : true
31 31 @caption_key = options[:caption] || "field_#{name}".to_sym
32 32 @frozen = options[:frozen]
33 33 end
34 34
35 35 def caption
36 36 @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key
37 37 end
38 38
39 39 # Returns true if the column is sortable, otherwise false
40 40 def sortable?
41 41 !@sortable.nil?
42 42 end
43 43
44 44 def sortable
45 45 @sortable.is_a?(Proc) ? @sortable.call : @sortable
46 46 end
47 47
48 48 def inline?
49 49 @inline
50 50 end
51 51
52 52 def frozen?
53 53 @frozen
54 54 end
55 55
56 56 def value(object)
57 57 object.send name
58 58 end
59 59
60 def value_object(object)
61 object.send name
62 end
63
60 64 def css_classes
61 65 name
62 66 end
63 67 end
64 68
65 69 class QueryCustomFieldColumn < QueryColumn
66 70
67 71 def initialize(custom_field)
68 72 self.name = "cf_#{custom_field.id}".to_sym
69 73 self.sortable = custom_field.order_statement || false
70 74 self.groupable = custom_field.group_statement || false
71 75 @inline = true
72 76 @cf = custom_field
73 77 end
74 78
75 79 def caption
76 80 @cf.name
77 81 end
78 82
79 83 def custom_field
80 84 @cf
81 85 end
82 86
83 def value(object)
87 def value_object(object)
84 88 if custom_field.visible_by?(object.project, User.current)
85 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
86 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
89 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
90 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
91 else
92 nil
93 end
94 end
95
96 def value(object)
97 raw = value_object(object)
98 if raw.is_a?(Array)
99 raw.map {|r| @cf.cast_value(r.value)}
100 elsif raw
101 @cf.cast_value(raw.value)
87 102 else
88 103 nil
89 104 end
90 105 end
91 106
92 107 def css_classes
93 108 @css_classes ||= "#{name} #{@cf.field_format}"
94 109 end
95 110 end
96 111
97 112 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
98 113
99 114 def initialize(association, custom_field)
100 115 super(custom_field)
101 116 self.name = "#{association}.cf_#{custom_field.id}".to_sym
102 117 # TODO: support sorting/grouping by association custom field
103 118 self.sortable = false
104 119 self.groupable = false
105 120 @association = association
106 121 end
107 122
108 def value(object)
123 def value_object(object)
109 124 if assoc = object.send(@association)
110 125 super(assoc)
111 126 end
112 127 end
113 128
114 129 def css_classes
115 130 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
116 131 end
117 132 end
118 133
119 134 class Query < ActiveRecord::Base
120 135 class StatementInvalid < ::ActiveRecord::StatementInvalid
121 136 end
122 137
123 138 VISIBILITY_PRIVATE = 0
124 139 VISIBILITY_ROLES = 1
125 140 VISIBILITY_PUBLIC = 2
126 141
127 142 belongs_to :project
128 143 belongs_to :user
129 144 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
130 145 serialize :filters
131 146 serialize :column_names
132 147 serialize :sort_criteria, Array
133 148 serialize :options, Hash
134 149
135 150 attr_protected :project_id, :user_id
136 151
137 152 validates_presence_of :name
138 153 validates_length_of :name, :maximum => 255
139 154 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
140 155 validate :validate_query_filters
141 156 validate do |query|
142 157 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
143 158 end
144 159
145 160 after_save do |query|
146 161 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
147 162 query.roles.clear
148 163 end
149 164 end
150 165
151 166 class_attribute :operators
152 167 self.operators = {
153 168 "=" => :label_equals,
154 169 "!" => :label_not_equals,
155 170 "o" => :label_open_issues,
156 171 "c" => :label_closed_issues,
157 172 "!*" => :label_none,
158 173 "*" => :label_any,
159 174 ">=" => :label_greater_or_equal,
160 175 "<=" => :label_less_or_equal,
161 176 "><" => :label_between,
162 177 "<t+" => :label_in_less_than,
163 178 ">t+" => :label_in_more_than,
164 179 "><t+"=> :label_in_the_next_days,
165 180 "t+" => :label_in,
166 181 "t" => :label_today,
167 182 "ld" => :label_yesterday,
168 183 "w" => :label_this_week,
169 184 "lw" => :label_last_week,
170 185 "l2w" => [:label_last_n_weeks, {:count => 2}],
171 186 "m" => :label_this_month,
172 187 "lm" => :label_last_month,
173 188 "y" => :label_this_year,
174 189 ">t-" => :label_less_than_ago,
175 190 "<t-" => :label_more_than_ago,
176 191 "><t-"=> :label_in_the_past_days,
177 192 "t-" => :label_ago,
178 193 "~" => :label_contains,
179 194 "!~" => :label_not_contains,
180 195 "=p" => :label_any_issues_in_project,
181 196 "=!p" => :label_any_issues_not_in_project,
182 197 "!p" => :label_no_issues_in_project
183 198 }
184 199
185 200 class_attribute :operators_by_filter_type
186 201 self.operators_by_filter_type = {
187 202 :list => [ "=", "!" ],
188 203 :list_status => [ "o", "=", "!", "c", "*" ],
189 204 :list_optional => [ "=", "!", "!*", "*" ],
190 205 :list_subprojects => [ "*", "!*", "=" ],
191 206 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
192 207 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
193 208 :string => [ "=", "~", "!", "!~", "!*", "*" ],
194 209 :text => [ "~", "!~", "!*", "*" ],
195 210 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
196 211 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
197 212 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
198 213 }
199 214
200 215 class_attribute :available_columns
201 216 self.available_columns = []
202 217
203 218 class_attribute :queried_class
204 219
205 220 def queried_table_name
206 221 @queried_table_name ||= self.class.queried_class.table_name
207 222 end
208 223
209 224 def initialize(attributes=nil, *args)
210 225 super attributes
211 226 @is_for_all = project.nil?
212 227 end
213 228
214 229 # Builds the query from the given params
215 230 def build_from_params(params)
216 231 if params[:fields] || params[:f]
217 232 self.filters = {}
218 233 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
219 234 else
220 235 available_filters.keys.each do |field|
221 236 add_short_filter(field, params[field]) if params[field]
222 237 end
223 238 end
224 239 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
225 240 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
226 241 self
227 242 end
228 243
229 244 # Builds a new query from the given params and attributes
230 245 def self.build_from_params(params, attributes={})
231 246 new(attributes).build_from_params(params)
232 247 end
233 248
234 249 def validate_query_filters
235 250 filters.each_key do |field|
236 251 if values_for(field)
237 252 case type_for(field)
238 253 when :integer
239 254 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
240 255 when :float
241 256 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
242 257 when :date, :date_past
243 258 case operator_for(field)
244 259 when "=", ">=", "<=", "><"
245 260 add_filter_error(field, :invalid) if values_for(field).detect {|v|
246 261 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
247 262 }
248 263 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
249 264 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
250 265 end
251 266 end
252 267 end
253 268
254 269 add_filter_error(field, :blank) unless
255 270 # filter requires one or more values
256 271 (values_for(field) and !values_for(field).first.blank?) or
257 272 # filter doesn't require any value
258 273 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
259 274 end if filters
260 275 end
261 276
262 277 def add_filter_error(field, message)
263 278 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
264 279 errors.add(:base, m)
265 280 end
266 281
267 282 def editable_by?(user)
268 283 return false unless user
269 284 # Admin can edit them all and regular users can edit their private queries
270 285 return true if user.admin? || (is_private? && self.user_id == user.id)
271 286 # Members can not edit public queries that are for all project (only admin is allowed to)
272 287 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
273 288 end
274 289
275 290 def trackers
276 291 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
277 292 end
278 293
279 294 # Returns a hash of localized labels for all filter operators
280 295 def self.operators_labels
281 296 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
282 297 end
283 298
284 299 # Returns a representation of the available filters for JSON serialization
285 300 def available_filters_as_json
286 301 json = {}
287 302 available_filters.each do |field, options|
288 303 json[field] = options.slice(:type, :name, :values).stringify_keys
289 304 end
290 305 json
291 306 end
292 307
293 308 def all_projects
294 309 @all_projects ||= Project.visible.all
295 310 end
296 311
297 312 def all_projects_values
298 313 return @all_projects_values if @all_projects_values
299 314
300 315 values = []
301 316 Project.project_tree(all_projects) do |p, level|
302 317 prefix = (level > 0 ? ('--' * level + ' ') : '')
303 318 values << ["#{prefix}#{p.name}", p.id.to_s]
304 319 end
305 320 @all_projects_values = values
306 321 end
307 322
308 323 # Adds available filters
309 324 def initialize_available_filters
310 325 # implemented by sub-classes
311 326 end
312 327 protected :initialize_available_filters
313 328
314 329 # Adds an available filter
315 330 def add_available_filter(field, options)
316 331 @available_filters ||= ActiveSupport::OrderedHash.new
317 332 @available_filters[field] = options
318 333 @available_filters
319 334 end
320 335
321 336 # Removes an available filter
322 337 def delete_available_filter(field)
323 338 if @available_filters
324 339 @available_filters.delete(field)
325 340 end
326 341 end
327 342
328 343 # Return a hash of available filters
329 344 def available_filters
330 345 unless @available_filters
331 346 initialize_available_filters
332 347 @available_filters.each do |field, options|
333 348 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
334 349 end
335 350 end
336 351 @available_filters
337 352 end
338 353
339 354 def add_filter(field, operator, values=nil)
340 355 # values must be an array
341 356 return unless values.nil? || values.is_a?(Array)
342 357 # check if field is defined as an available filter
343 358 if available_filters.has_key? field
344 359 filter_options = available_filters[field]
345 360 filters[field] = {:operator => operator, :values => (values || [''])}
346 361 end
347 362 end
348 363
349 364 def add_short_filter(field, expression)
350 365 return unless expression && available_filters.has_key?(field)
351 366 field_type = available_filters[field][:type]
352 367 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
353 368 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
354 369 values = $1
355 370 add_filter field, operator, values.present? ? values.split('|') : ['']
356 371 end || add_filter(field, '=', expression.split('|'))
357 372 end
358 373
359 374 # Add multiple filters using +add_filter+
360 375 def add_filters(fields, operators, values)
361 376 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
362 377 fields.each do |field|
363 378 add_filter(field, operators[field], values && values[field])
364 379 end
365 380 end
366 381 end
367 382
368 383 def has_filter?(field)
369 384 filters and filters[field]
370 385 end
371 386
372 387 def type_for(field)
373 388 available_filters[field][:type] if available_filters.has_key?(field)
374 389 end
375 390
376 391 def operator_for(field)
377 392 has_filter?(field) ? filters[field][:operator] : nil
378 393 end
379 394
380 395 def values_for(field)
381 396 has_filter?(field) ? filters[field][:values] : nil
382 397 end
383 398
384 399 def value_for(field, index=0)
385 400 (values_for(field) || [])[index]
386 401 end
387 402
388 403 def label_for(field)
389 404 label = available_filters[field][:name] if available_filters.has_key?(field)
390 405 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
391 406 end
392 407
393 408 def self.add_available_column(column)
394 409 self.available_columns << (column) if column.is_a?(QueryColumn)
395 410 end
396 411
397 412 # Returns an array of columns that can be used to group the results
398 413 def groupable_columns
399 414 available_columns.select {|c| c.groupable}
400 415 end
401 416
402 417 # Returns a Hash of columns and the key for sorting
403 418 def sortable_columns
404 419 available_columns.inject({}) {|h, column|
405 420 h[column.name.to_s] = column.sortable
406 421 h
407 422 }
408 423 end
409 424
410 425 def columns
411 426 # preserve the column_names order
412 427 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
413 428 available_columns.find { |col| col.name == name }
414 429 end.compact
415 430 available_columns.select(&:frozen?) | cols
416 431 end
417 432
418 433 def inline_columns
419 434 columns.select(&:inline?)
420 435 end
421 436
422 437 def block_columns
423 438 columns.reject(&:inline?)
424 439 end
425 440
426 441 def available_inline_columns
427 442 available_columns.select(&:inline?)
428 443 end
429 444
430 445 def available_block_columns
431 446 available_columns.reject(&:inline?)
432 447 end
433 448
434 449 def default_columns_names
435 450 []
436 451 end
437 452
438 453 def column_names=(names)
439 454 if names
440 455 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
441 456 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
442 457 # Set column_names to nil if default columns
443 458 if names == default_columns_names
444 459 names = nil
445 460 end
446 461 end
447 462 write_attribute(:column_names, names)
448 463 end
449 464
450 465 def has_column?(column)
451 466 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
452 467 end
453 468
454 469 def has_custom_field_column?
455 470 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
456 471 end
457 472
458 473 def has_default_columns?
459 474 column_names.nil? || column_names.empty?
460 475 end
461 476
462 477 def sort_criteria=(arg)
463 478 c = []
464 479 if arg.is_a?(Hash)
465 480 arg = arg.keys.sort.collect {|k| arg[k]}
466 481 end
467 482 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
468 483 write_attribute(:sort_criteria, c)
469 484 end
470 485
471 486 def sort_criteria
472 487 read_attribute(:sort_criteria) || []
473 488 end
474 489
475 490 def sort_criteria_key(arg)
476 491 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
477 492 end
478 493
479 494 def sort_criteria_order(arg)
480 495 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
481 496 end
482 497
483 498 def sort_criteria_order_for(key)
484 499 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
485 500 end
486 501
487 502 # Returns the SQL sort order that should be prepended for grouping
488 503 def group_by_sort_order
489 504 if grouped? && (column = group_by_column)
490 505 order = sort_criteria_order_for(column.name) || column.default_order
491 506 column.sortable.is_a?(Array) ?
492 507 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
493 508 "#{column.sortable} #{order}"
494 509 end
495 510 end
496 511
497 512 # Returns true if the query is a grouped query
498 513 def grouped?
499 514 !group_by_column.nil?
500 515 end
501 516
502 517 def group_by_column
503 518 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
504 519 end
505 520
506 521 def group_by_statement
507 522 group_by_column.try(:groupable)
508 523 end
509 524
510 525 def project_statement
511 526 project_clauses = []
512 527 if project && !project.descendants.active.empty?
513 528 ids = [project.id]
514 529 if has_filter?("subproject_id")
515 530 case operator_for("subproject_id")
516 531 when '='
517 532 # include the selected subprojects
518 533 ids += values_for("subproject_id").each(&:to_i)
519 534 when '!*'
520 535 # main project only
521 536 else
522 537 # all subprojects
523 538 ids += project.descendants.collect(&:id)
524 539 end
525 540 elsif Setting.display_subprojects_issues?
526 541 ids += project.descendants.collect(&:id)
527 542 end
528 543 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
529 544 elsif project
530 545 project_clauses << "#{Project.table_name}.id = %d" % project.id
531 546 end
532 547 project_clauses.any? ? project_clauses.join(' AND ') : nil
533 548 end
534 549
535 550 def statement
536 551 # filters clauses
537 552 filters_clauses = []
538 553 filters.each_key do |field|
539 554 next if field == "subproject_id"
540 555 v = values_for(field).clone
541 556 next unless v and !v.empty?
542 557 operator = operator_for(field)
543 558
544 559 # "me" value subsitution
545 560 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
546 561 if v.delete("me")
547 562 if User.current.logged?
548 563 v.push(User.current.id.to_s)
549 564 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
550 565 else
551 566 v.push("0")
552 567 end
553 568 end
554 569 end
555 570
556 571 if field == 'project_id'
557 572 if v.delete('mine')
558 573 v += User.current.memberships.map(&:project_id).map(&:to_s)
559 574 end
560 575 end
561 576
562 577 if field =~ /cf_(\d+)$/
563 578 # custom field
564 579 filters_clauses << sql_for_custom_field(field, operator, v, $1)
565 580 elsif respond_to?("sql_for_#{field}_field")
566 581 # specific statement
567 582 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
568 583 else
569 584 # regular field
570 585 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
571 586 end
572 587 end if filters and valid?
573 588
574 589 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
575 590 # Excludes results for which the grouped custom field is not visible
576 591 filters_clauses << c.custom_field.visibility_by_project_condition
577 592 end
578 593
579 594 filters_clauses << project_statement
580 595 filters_clauses.reject!(&:blank?)
581 596
582 597 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
583 598 end
584 599
585 600 private
586 601
587 602 def sql_for_custom_field(field, operator, value, custom_field_id)
588 603 db_table = CustomValue.table_name
589 604 db_field = 'value'
590 605 filter = @available_filters[field]
591 606 return nil unless filter
592 607 if filter[:field].format.target_class && filter[:field].format.target_class <= User
593 608 if value.delete('me')
594 609 value.push User.current.id.to_s
595 610 end
596 611 end
597 612 not_in = nil
598 613 if operator == '!'
599 614 # Makes ! operator work for custom fields with multiple values
600 615 operator = '='
601 616 not_in = 'NOT'
602 617 end
603 618 customized_key = "id"
604 619 customized_class = queried_class
605 620 if field =~ /^(.+)\.cf_/
606 621 assoc = $1
607 622 customized_key = "#{assoc}_id"
608 623 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
609 624 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
610 625 end
611 626 where = sql_for_field(field, operator, value, db_table, db_field, true)
612 627 if operator =~ /[<>]/
613 628 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
614 629 end
615 630 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
616 631 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
617 632 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
618 633 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
619 634 end
620 635
621 636 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
622 637 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
623 638 sql = ''
624 639 case operator
625 640 when "="
626 641 if value.any?
627 642 case type_for(field)
628 643 when :date, :date_past
629 644 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first))
630 645 when :integer
631 646 if is_custom_filter
632 647 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
633 648 else
634 649 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
635 650 end
636 651 when :float
637 652 if is_custom_filter
638 653 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
639 654 else
640 655 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
641 656 end
642 657 else
643 658 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
644 659 end
645 660 else
646 661 # IN an empty set
647 662 sql = "1=0"
648 663 end
649 664 when "!"
650 665 if value.any?
651 666 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
652 667 else
653 668 # NOT IN an empty set
654 669 sql = "1=1"
655 670 end
656 671 when "!*"
657 672 sql = "#{db_table}.#{db_field} IS NULL"
658 673 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
659 674 when "*"
660 675 sql = "#{db_table}.#{db_field} IS NOT NULL"
661 676 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
662 677 when ">="
663 678 if [:date, :date_past].include?(type_for(field))
664 679 sql = date_clause(db_table, db_field, parse_date(value.first), nil)
665 680 else
666 681 if is_custom_filter
667 682 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
668 683 else
669 684 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
670 685 end
671 686 end
672 687 when "<="
673 688 if [:date, :date_past].include?(type_for(field))
674 689 sql = date_clause(db_table, db_field, nil, parse_date(value.first))
675 690 else
676 691 if is_custom_filter
677 692 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
678 693 else
679 694 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
680 695 end
681 696 end
682 697 when "><"
683 698 if [:date, :date_past].include?(type_for(field))
684 699 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]))
685 700 else
686 701 if is_custom_filter
687 702 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
688 703 else
689 704 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
690 705 end
691 706 end
692 707 when "o"
693 708 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
694 709 when "c"
695 710 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
696 711 when "><t-"
697 712 # between today - n days and today
698 713 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
699 714 when ">t-"
700 715 # >= today - n days
701 716 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
702 717 when "<t-"
703 718 # <= today - n days
704 719 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
705 720 when "t-"
706 721 # = n days in past
707 722 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
708 723 when "><t+"
709 724 # between today and today + n days
710 725 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
711 726 when ">t+"
712 727 # >= today + n days
713 728 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
714 729 when "<t+"
715 730 # <= today + n days
716 731 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
717 732 when "t+"
718 733 # = today + n days
719 734 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
720 735 when "t"
721 736 # = today
722 737 sql = relative_date_clause(db_table, db_field, 0, 0)
723 738 when "ld"
724 739 # = yesterday
725 740 sql = relative_date_clause(db_table, db_field, -1, -1)
726 741 when "w"
727 742 # = this week
728 743 first_day_of_week = l(:general_first_day_of_week).to_i
729 744 day_of_week = Date.today.cwday
730 745 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
731 746 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
732 747 when "lw"
733 748 # = last week
734 749 first_day_of_week = l(:general_first_day_of_week).to_i
735 750 day_of_week = Date.today.cwday
736 751 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
737 752 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
738 753 when "l2w"
739 754 # = last 2 weeks
740 755 first_day_of_week = l(:general_first_day_of_week).to_i
741 756 day_of_week = Date.today.cwday
742 757 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
743 758 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
744 759 when "m"
745 760 # = this month
746 761 date = Date.today
747 762 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
748 763 when "lm"
749 764 # = last month
750 765 date = Date.today.prev_month
751 766 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
752 767 when "y"
753 768 # = this year
754 769 date = Date.today
755 770 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
756 771 when "~"
757 772 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
758 773 when "!~"
759 774 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
760 775 else
761 776 raise "Unknown query operator #{operator}"
762 777 end
763 778
764 779 return sql
765 780 end
766 781
767 782 # Adds a filter for the given custom field
768 783 def add_custom_field_filter(field, assoc=nil)
769 784 options = field.format.query_filter_options(field, self)
770 785 if field.format.target_class && field.format.target_class <= User
771 786 if options[:values].is_a?(Array) && User.current.logged?
772 787 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
773 788 end
774 789 end
775 790
776 791 filter_id = "cf_#{field.id}"
777 792 filter_name = field.name
778 793 if assoc.present?
779 794 filter_id = "#{assoc}.#{filter_id}"
780 795 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
781 796 end
782 797 add_available_filter filter_id, options.merge({
783 798 :name => filter_name,
784 799 :field => field
785 800 })
786 801 end
787 802
788 803 # Adds filters for the given custom fields scope
789 804 def add_custom_fields_filters(scope, assoc=nil)
790 805 scope.visible.where(:is_filter => true).sorted.each do |field|
791 806 add_custom_field_filter(field, assoc)
792 807 end
793 808 end
794 809
795 810 # Adds filters for the given associations custom fields
796 811 def add_associations_custom_fields_filters(*associations)
797 812 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
798 813 associations.each do |assoc|
799 814 association_klass = queried_class.reflect_on_association(assoc).klass
800 815 fields_by_class.each do |field_class, fields|
801 816 if field_class.customized_class <= association_klass
802 817 fields.sort.each do |field|
803 818 add_custom_field_filter(field, assoc)
804 819 end
805 820 end
806 821 end
807 822 end
808 823 end
809 824
810 825 # Returns a SQL clause for a date or datetime field.
811 826 def date_clause(table, field, from, to)
812 827 s = []
813 828 if from
814 829 if from.is_a?(Date)
815 830 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
816 831 else
817 832 from = from - 1 # second
818 833 end
819 834 if self.class.default_timezone == :utc
820 835 from = from.utc
821 836 end
822 837 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from)])
823 838 end
824 839 if to
825 840 if to.is_a?(Date)
826 841 to = Time.local(to.year, to.month, to.day).end_of_day
827 842 end
828 843 if self.class.default_timezone == :utc
829 844 to = to.utc
830 845 end
831 846 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to)])
832 847 end
833 848 s.join(' AND ')
834 849 end
835 850
836 851 # Returns a SQL clause for a date or datetime field using relative dates.
837 852 def relative_date_clause(table, field, days_from, days_to)
838 853 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
839 854 end
840 855
841 856 # Returns a Date or Time from the given filter value
842 857 def parse_date(arg)
843 858 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
844 859 Time.parse(arg) rescue nil
845 860 else
846 861 Date.parse(arg) rescue nil
847 862 end
848 863 end
849 864
850 865 # Additional joins required for the given sort options
851 866 def joins_for_order_statement(order_options)
852 867 joins = []
853 868
854 869 if order_options
855 870 if order_options.include?('authors')
856 871 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
857 872 end
858 873 order_options.scan(/cf_\d+/).uniq.each do |name|
859 874 column = available_columns.detect {|c| c.name.to_s == name}
860 875 join = column && column.custom_field.join_for_order_statement
861 876 if join
862 877 joins << join
863 878 end
864 879 end
865 880 end
866 881
867 882 joins.any? ? joins.join(' ') : nil
868 883 end
869 884 end
General Comments 0
You need to be logged in to leave comments. Login now