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