##// END OF EJS Templates
html_safe for Rails3...
Jean-Philippe Lang -
r8151:7fdb74530c8c
parent child
Show More
@@ -1,1082 +1,1082
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 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
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << ": #{h subject}" if subject
89 89 s = "#{h issue.project} - " + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100 link_to(h(text),
101 101 {:controller => 'attachments', :action => action,
102 102 :id => attachment, :filename => attachment.filename },
103 103 options)
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, project, options={})
110 110 text = options.delete(:text) || format_revision(revision)
111 111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 112 link_to(
113 113 h(text),
114 114 {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
115 115 :title => l(:label_revision_id, format_revision(revision))
116 116 )
117 117 end
118 118
119 119 # Generates a link to a message
120 120 def link_to_message(message, options={}, html_options = nil)
121 121 link_to(
122 122 h(truncate(message.subject, :length => 60)),
123 123 { :controller => 'messages', :action => 'show',
124 124 :board_id => message.board_id,
125 125 :id => message.root,
126 126 :r => (message.parent_id && message.id),
127 127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 128 }.merge(options),
129 129 html_options
130 130 )
131 131 end
132 132
133 133 # Generates a link to a project if active
134 134 # Examples:
135 135 #
136 136 # link_to_project(project) # => link to the specified project overview
137 137 # link_to_project(project, :action=>'settings') # => link to project settings
138 138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 140 #
141 141 def link_to_project(project, options={}, html_options = nil)
142 142 if project.active?
143 143 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
144 144 link_to(h(project), url, html_options)
145 145 else
146 146 h(project)
147 147 end
148 148 end
149 149
150 150 def toggle_link(name, id, options={})
151 151 onclick = "Element.toggle('#{id}'); "
152 152 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
153 153 onclick << "return false;"
154 154 link_to(name, "#", :onclick => onclick)
155 155 end
156 156
157 157 def image_to_function(name, function, html_options = {})
158 158 html_options.symbolize_keys!
159 159 tag(:input, html_options.merge({
160 160 :type => "image", :src => image_path(name),
161 161 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
162 162 }))
163 163 end
164 164
165 165 def prompt_to_remote(name, text, param, url, html_options = {})
166 166 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
167 167 link_to name, {}, html_options
168 168 end
169 169
170 170 def format_activity_title(text)
171 171 h(truncate_single_line(text, :length => 100))
172 172 end
173 173
174 174 def format_activity_day(date)
175 175 date == Date.today ? l(:label_today).titleize : format_date(date)
176 176 end
177 177
178 178 def format_activity_description(text)
179 179 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
180 180 ).gsub(/[\r\n]+/, "<br />").html_safe
181 181 end
182 182
183 183 def format_version_name(version)
184 184 if version.project == @project
185 185 h(version)
186 186 else
187 187 h("#{version.project} - #{version}")
188 188 end
189 189 end
190 190
191 191 def due_date_distance_in_words(date)
192 192 if date
193 193 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
194 194 end
195 195 end
196 196
197 197 def render_page_hierarchy(pages, node=nil, options={})
198 198 content = ''
199 199 if pages[node]
200 200 content << "<ul class=\"pages-hierarchy\">\n"
201 201 pages[node].each do |page|
202 202 content << "<li>"
203 203 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
204 204 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
205 205 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
206 206 content << "</li>\n"
207 207 end
208 208 content << "</ul>\n"
209 209 end
210 210 content.html_safe
211 211 end
212 212
213 213 # Renders flash messages
214 214 def render_flash_messages
215 215 s = ''
216 216 flash.each do |k,v|
217 217 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
218 218 end
219 219 s.html_safe
220 220 end
221 221
222 222 # Renders tabs and their content
223 223 def render_tabs(tabs)
224 224 if tabs.any?
225 225 render :partial => 'common/tabs', :locals => {:tabs => tabs}
226 226 else
227 227 content_tag 'p', l(:label_no_data), :class => "nodata"
228 228 end
229 229 end
230 230
231 231 # Renders the project quick-jump box
232 232 def render_project_jump_box
233 233 return unless User.current.logged?
234 234 projects = User.current.memberships.collect(&:project).compact.uniq
235 235 if projects.any?
236 236 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
237 237 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
238 238 '<option value="" disabled="disabled">---</option>'
239 239 s << project_tree_options_for_select(projects, :selected => @project) do |p|
240 240 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
241 241 end
242 242 s << '</select>'
243 243 s.html_safe
244 244 end
245 245 end
246 246
247 247 def project_tree_options_for_select(projects, options = {})
248 248 s = ''
249 249 project_tree(projects) do |project, level|
250 250 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
251 251 tag_options = {:value => project.id}
252 252 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
253 253 tag_options[:selected] = 'selected'
254 254 else
255 255 tag_options[:selected] = nil
256 256 end
257 257 tag_options.merge!(yield(project)) if block_given?
258 258 s << content_tag('option', name_prefix + h(project), tag_options)
259 259 end
260 260 s.html_safe
261 261 end
262 262
263 263 # Yields the given block for each project with its level in the tree
264 264 #
265 265 # Wrapper for Project#project_tree
266 266 def project_tree(projects, &block)
267 267 Project.project_tree(projects, &block)
268 268 end
269 269
270 270 def project_nested_ul(projects, &block)
271 271 s = ''
272 272 if projects.any?
273 273 ancestors = []
274 274 projects.sort_by(&:lft).each do |project|
275 275 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
276 276 s << "<ul>\n"
277 277 else
278 278 ancestors.pop
279 279 s << "</li>"
280 280 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
281 281 ancestors.pop
282 282 s << "</ul></li>\n"
283 283 end
284 284 end
285 285 s << "<li>"
286 286 s << yield(project).to_s
287 287 ancestors << project
288 288 end
289 289 s << ("</li></ul>\n" * ancestors.size)
290 290 end
291 291 s.html_safe
292 292 end
293 293
294 294 def principals_check_box_tags(name, principals)
295 295 s = ''
296 296 principals.sort.each do |principal|
297 297 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
298 298 end
299 299 s.html_safe
300 300 end
301 301
302 302 # Returns a string for users/groups option tags
303 303 def principals_options_for_select(collection, selected=nil)
304 304 s = ''
305 305 groups = ''
306 306 collection.sort.each do |element|
307 307 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
308 308 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
309 309 end
310 310 unless groups.empty?
311 311 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
312 312 end
313 313 s
314 314 end
315 315
316 316 # Truncates and returns the string as a single line
317 317 def truncate_single_line(string, *args)
318 318 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
319 319 end
320 320
321 321 # Truncates at line break after 250 characters or options[:length]
322 322 def truncate_lines(string, options={})
323 323 length = options[:length] || 250
324 324 if string.to_s =~ /\A(.{#{length}}.*?)$/m
325 325 "#{$1}..."
326 326 else
327 327 string
328 328 end
329 329 end
330 330
331 331 def html_hours(text)
332 332 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
333 333 end
334 334
335 335 def authoring(created, author, options={})
336 336 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
337 337 end
338 338
339 339 def time_tag(time)
340 340 text = distance_of_time_in_words(Time.now, time)
341 341 if @project
342 342 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
343 343 else
344 344 content_tag('acronym', text, :title => format_time(time))
345 345 end
346 346 end
347 347
348 348 def syntax_highlight(name, content)
349 349 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
350 350 end
351 351
352 352 def to_path_param(path)
353 353 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
354 354 end
355 355
356 356 def pagination_links_full(paginator, count=nil, options={})
357 357 page_param = options.delete(:page_param) || :page
358 358 per_page_links = options.delete(:per_page_links)
359 359 url_param = params.dup
360 360
361 361 html = ''
362 362 if paginator.current.previous
363 363 # \xc2\xab(utf-8) = &#171;
364 364 html << link_to_content_update(
365 365 "\xc2\xab " + l(:label_previous),
366 366 url_param.merge(page_param => paginator.current.previous)) + ' '
367 367 end
368 368
369 369 html << (pagination_links_each(paginator, options) do |n|
370 370 link_to_content_update(n.to_s, url_param.merge(page_param => n))
371 371 end || '')
372 372
373 373 if paginator.current.next
374 374 # \xc2\xbb(utf-8) = &#187;
375 375 html << ' ' + link_to_content_update(
376 376 (l(:label_next) + " \xc2\xbb"),
377 377 url_param.merge(page_param => paginator.current.next))
378 378 end
379 379
380 380 unless count.nil?
381 381 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
382 382 if per_page_links != false && links = per_page_links(paginator.items_per_page)
383 383 html << " | #{links}"
384 384 end
385 385 end
386 386
387 387 html.html_safe
388 388 end
389 389
390 390 def per_page_links(selected=nil)
391 391 links = Setting.per_page_options_array.collect do |n|
392 392 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
393 393 end
394 394 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
395 395 end
396 396
397 397 def reorder_links(name, url, method = :post)
398 398 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
399 399 url.merge({"#{name}[move_to]" => 'highest'}),
400 400 :method => method, :title => l(:label_sort_highest)) +
401 401 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
402 402 url.merge({"#{name}[move_to]" => 'higher'}),
403 403 :method => method, :title => l(:label_sort_higher)) +
404 404 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
405 405 url.merge({"#{name}[move_to]" => 'lower'}),
406 406 :method => method, :title => l(:label_sort_lower)) +
407 407 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
408 408 url.merge({"#{name}[move_to]" => 'lowest'}),
409 409 :method => method, :title => l(:label_sort_lowest))
410 410 end
411 411
412 412 def breadcrumb(*args)
413 413 elements = args.flatten
414 414 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
415 415 end
416 416
417 417 def other_formats_links(&block)
418 418 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
419 419 yield Redmine::Views::OtherFormatsBuilder.new(self)
420 420 concat('</p>'.html_safe)
421 421 end
422 422
423 423 def page_header_title
424 424 if @project.nil? || @project.new_record?
425 425 h(Setting.app_title)
426 426 else
427 427 b = []
428 428 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
429 429 if ancestors.any?
430 430 root = ancestors.shift
431 431 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
432 432 if ancestors.size > 2
433 433 b << "\xe2\x80\xa6"
434 434 ancestors = ancestors[-2, 2]
435 435 end
436 436 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
437 437 end
438 438 b << h(@project)
439 439 b.join(" \xc2\xbb ").html_safe
440 440 end
441 441 end
442 442
443 443 def html_title(*args)
444 444 if args.empty?
445 445 title = @html_title || []
446 446 title << @project.name if @project
447 447 title << Setting.app_title unless Setting.app_title == title.last
448 448 title.select {|t| !t.blank? }.join(' - ')
449 449 else
450 450 @html_title ||= []
451 451 @html_title += args
452 452 end
453 453 end
454 454
455 455 # Returns the theme, controller name, and action as css classes for the
456 456 # HTML body.
457 457 def body_css_classes
458 458 css = []
459 459 if theme = Redmine::Themes.theme(Setting.ui_theme)
460 460 css << 'theme-' + theme.name
461 461 end
462 462
463 463 css << 'controller-' + params[:controller]
464 464 css << 'action-' + params[:action]
465 465 css.join(' ')
466 466 end
467 467
468 468 def accesskey(s)
469 469 Redmine::AccessKeys.key_for s
470 470 end
471 471
472 472 # Formats text according to system settings.
473 473 # 2 ways to call this method:
474 474 # * with a String: textilizable(text, options)
475 475 # * with an object and one of its attribute: textilizable(issue, :description, options)
476 476 def textilizable(*args)
477 477 options = args.last.is_a?(Hash) ? args.pop : {}
478 478 case args.size
479 479 when 1
480 480 obj = options[:object]
481 481 text = args.shift
482 482 when 2
483 483 obj = args.shift
484 484 attr = args.shift
485 485 text = obj.send(attr).to_s
486 486 else
487 487 raise ArgumentError, 'invalid arguments to textilizable'
488 488 end
489 489 return '' if text.blank?
490 490 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
491 491 only_path = options.delete(:only_path) == false ? false : true
492 492
493 493 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
494 494
495 495 @parsed_headings = []
496 496 @current_section = 0 if options[:edit_section_links]
497 497 text = parse_non_pre_blocks(text) do |text|
498 498 [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name|
499 499 send method_name, text, project, obj, attr, only_path, options
500 500 end
501 501 end
502 502
503 503 if @parsed_headings.any?
504 504 replace_toc(text, @parsed_headings)
505 505 end
506 506
507 text
507 text.html_safe
508 508 end
509 509
510 510 def parse_non_pre_blocks(text)
511 511 s = StringScanner.new(text)
512 512 tags = []
513 513 parsed = ''
514 514 while !s.eos?
515 515 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
516 516 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
517 517 if tags.empty?
518 518 yield text
519 519 end
520 520 parsed << text
521 521 if tag
522 522 if closing
523 523 if tags.last == tag.downcase
524 524 tags.pop
525 525 end
526 526 else
527 527 tags << tag.downcase
528 528 end
529 529 parsed << full_tag
530 530 end
531 531 end
532 532 # Close any non closing tags
533 533 while tag = tags.pop
534 534 parsed << "</#{tag}>"
535 535 end
536 536 parsed.html_safe
537 537 end
538 538
539 539 def parse_inline_attachments(text, project, obj, attr, only_path, options)
540 540 # when using an image link, try to use an attachment, if possible
541 541 if options[:attachments] || (obj && obj.respond_to?(:attachments))
542 542 attachments = options[:attachments] || obj.attachments
543 543 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
544 544 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
545 545 # search for the picture in attachments
546 546 if found = Attachment.latest_attach(attachments, filename)
547 547 image_url = url_for :only_path => only_path, :controller => 'attachments',
548 548 :action => 'download', :id => found
549 549 desc = found.description.to_s.gsub('"', '')
550 550 if !desc.blank? && alttext.blank?
551 551 alt = " title=\"#{desc}\" alt=\"#{desc}\""
552 552 end
553 553 "src=\"#{image_url}\"#{alt}".html_safe
554 554 else
555 555 m.html_safe
556 556 end
557 557 end
558 558 end
559 559 end
560 560
561 561 # Wiki links
562 562 #
563 563 # Examples:
564 564 # [[mypage]]
565 565 # [[mypage|mytext]]
566 566 # wiki links can refer other project wikis, using project name or identifier:
567 567 # [[project:]] -> wiki starting page
568 568 # [[project:|mytext]]
569 569 # [[project:mypage]]
570 570 # [[project:mypage|mytext]]
571 571 def parse_wiki_links(text, project, obj, attr, only_path, options)
572 572 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
573 573 link_project = project
574 574 esc, all, page, title = $1, $2, $3, $5
575 575 if esc.nil?
576 576 if page =~ /^([^\:]+)\:(.*)$/
577 577 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
578 578 page = $2
579 579 title ||= $1 if page.blank?
580 580 end
581 581
582 582 if link_project && link_project.wiki
583 583 # extract anchor
584 584 anchor = nil
585 585 if page =~ /^(.+?)\#(.+)$/
586 586 page, anchor = $1, $2
587 587 end
588 588 anchor = sanitize_anchor_name(anchor) if anchor.present?
589 589 # check if page exists
590 590 wiki_page = link_project.wiki.find_page(page)
591 591 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
592 592 "##{anchor}"
593 593 else
594 594 case options[:wiki_links]
595 595 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
596 596 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
597 597 else
598 598 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
599 599 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
600 600 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
601 601 :id => wiki_page_id, :anchor => anchor, :parent => parent)
602 602 end
603 603 end
604 604 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
605 605 else
606 606 # project or wiki doesn't exist
607 607 all.html_safe
608 608 end
609 609 else
610 610 all.html_safe
611 611 end
612 612 end
613 613 end
614 614
615 615 # Redmine links
616 616 #
617 617 # Examples:
618 618 # Issues:
619 619 # #52 -> Link to issue #52
620 620 # Changesets:
621 621 # r52 -> Link to revision 52
622 622 # commit:a85130f -> Link to scmid starting with a85130f
623 623 # Documents:
624 624 # document#17 -> Link to document with id 17
625 625 # document:Greetings -> Link to the document with title "Greetings"
626 626 # document:"Some document" -> Link to the document with title "Some document"
627 627 # Versions:
628 628 # version#3 -> Link to version with id 3
629 629 # version:1.0.0 -> Link to version named "1.0.0"
630 630 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
631 631 # Attachments:
632 632 # attachment:file.zip -> Link to the attachment of the current object named file.zip
633 633 # Source files:
634 634 # source:some/file -> Link to the file located at /some/file in the project's repository
635 635 # source:some/file@52 -> Link to the file's revision 52
636 636 # source:some/file#L120 -> Link to line 120 of the file
637 637 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
638 638 # export:some/file -> Force the download of the file
639 639 # Forum messages:
640 640 # message#1218 -> Link to message with id 1218
641 641 #
642 642 # Links can refer other objects from other projects, using project identifier:
643 643 # identifier:r52
644 644 # identifier:document:"Some document"
645 645 # identifier:version:1.0.0
646 646 # identifier:source:some/file
647 647 def parse_redmine_links(text, project, obj, attr, only_path, options)
648 648 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
649 649 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
650 650 link = nil
651 651 if project_identifier
652 652 project = Project.visible.find_by_identifier(project_identifier)
653 653 end
654 654 if esc.nil?
655 655 if prefix.nil? && sep == 'r'
656 656 # project.changesets.visible raises an SQL error because of a double join on repositories
657 657 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
658 658 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
659 659 :class => 'changeset',
660 660 :title => truncate_single_line(changeset.comments, :length => 100))
661 661 end
662 662 elsif sep == '#'
663 663 oid = identifier.to_i
664 664 case prefix
665 665 when nil
666 666 if issue = Issue.visible.find_by_id(oid, :include => :status)
667 667 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
668 668 :class => issue.css_classes,
669 669 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
670 670 end
671 671 when 'document'
672 672 if document = Document.visible.find_by_id(oid)
673 673 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
674 674 :class => 'document'
675 675 end
676 676 when 'version'
677 677 if version = Version.visible.find_by_id(oid)
678 678 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
679 679 :class => 'version'
680 680 end
681 681 when 'message'
682 682 if message = Message.visible.find_by_id(oid, :include => :parent)
683 683 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
684 684 end
685 685 when 'forum'
686 686 if board = Board.visible.find_by_id(oid)
687 687 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
688 688 :class => 'board'
689 689 end
690 690 when 'news'
691 691 if news = News.visible.find_by_id(oid)
692 692 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
693 693 :class => 'news'
694 694 end
695 695 when 'project'
696 696 if p = Project.visible.find_by_id(oid)
697 697 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
698 698 end
699 699 end
700 700 elsif sep == ':'
701 701 # removes the double quotes if any
702 702 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
703 703 case prefix
704 704 when 'document'
705 705 if project && document = project.documents.visible.find_by_title(name)
706 706 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
707 707 :class => 'document'
708 708 end
709 709 when 'version'
710 710 if project && version = project.versions.visible.find_by_name(name)
711 711 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
712 712 :class => 'version'
713 713 end
714 714 when 'forum'
715 715 if project && board = project.boards.visible.find_by_name(name)
716 716 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
717 717 :class => 'board'
718 718 end
719 719 when 'news'
720 720 if project && news = project.news.visible.find_by_title(name)
721 721 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
722 722 :class => 'news'
723 723 end
724 724 when 'commit'
725 725 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
726 726 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
727 727 :class => 'changeset',
728 728 :title => truncate_single_line(h(changeset.comments), :length => 100)
729 729 end
730 730 when 'source', 'export'
731 731 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
732 732 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
733 733 path, rev, anchor = $1, $3, $5
734 734 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
735 735 :path => to_path_param(path),
736 736 :rev => rev,
737 737 :anchor => anchor,
738 738 :format => (prefix == 'export' ? 'raw' : nil)},
739 739 :class => (prefix == 'export' ? 'source download' : 'source')
740 740 end
741 741 when 'attachment'
742 742 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
743 743 if attachments && attachment = attachments.detect {|a| a.filename == name }
744 744 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
745 745 :class => 'attachment'
746 746 end
747 747 when 'project'
748 748 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
749 749 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
750 750 end
751 751 end
752 752 end
753 753 end
754 754 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
755 755 end
756 756 end
757 757
758 758 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
759 759
760 760 def parse_sections(text, project, obj, attr, only_path, options)
761 761 return unless options[:edit_section_links]
762 762 text.gsub!(HEADING_RE) do
763 763 @current_section += 1
764 764 if @current_section > 1
765 765 content_tag('div',
766 766 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
767 767 :class => 'contextual',
768 768 :title => l(:button_edit_section)) + $1
769 769 else
770 770 $1
771 771 end
772 772 end
773 773 end
774 774
775 775 # Headings and TOC
776 776 # Adds ids and links to headings unless options[:headings] is set to false
777 777 def parse_headings(text, project, obj, attr, only_path, options)
778 778 return if options[:headings] == false
779 779
780 780 text.gsub!(HEADING_RE) do
781 781 level, attrs, content = $2.to_i, $3, $4
782 782 item = strip_tags(content).strip
783 783 anchor = sanitize_anchor_name(item)
784 784 # used for single-file wiki export
785 785 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
786 786 @parsed_headings << [level, anchor, item]
787 787 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
788 788 end
789 789 end
790 790
791 791 MACROS_RE = /
792 792 (!)? # escaping
793 793 (
794 794 \{\{ # opening tag
795 795 ([\w]+) # macro name
796 796 (\(([^\}]*)\))? # optional arguments
797 797 \}\} # closing tag
798 798 )
799 799 /x unless const_defined?(:MACROS_RE)
800 800
801 801 # Macros substitution
802 802 def parse_macros(text, project, obj, attr, only_path, options)
803 803 text.gsub!(MACROS_RE) do
804 804 esc, all, macro = $1, $2, $3.downcase
805 805 args = ($5 || '').split(',').each(&:strip)
806 806 if esc.nil?
807 807 begin
808 808 exec_macro(macro, obj, args)
809 809 rescue => e
810 810 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
811 811 end || all
812 812 else
813 813 all
814 814 end
815 815 end
816 816 end
817 817
818 818 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
819 819
820 820 # Renders the TOC with given headings
821 821 def replace_toc(text, headings)
822 822 text.gsub!(TOC_RE) do
823 823 if headings.empty?
824 824 ''
825 825 else
826 826 div_class = 'toc'
827 827 div_class << ' right' if $1 == '>'
828 828 div_class << ' left' if $1 == '<'
829 829 out = "<ul class=\"#{div_class}\"><li>"
830 830 root = headings.map(&:first).min
831 831 current = root
832 832 started = false
833 833 headings.each do |level, anchor, item|
834 834 if level > current
835 835 out << '<ul><li>' * (level - current)
836 836 elsif level < current
837 837 out << "</li></ul>\n" * (current - level) + "</li><li>"
838 838 elsif started
839 839 out << '</li><li>'
840 840 end
841 841 out << "<a href=\"##{anchor}\">#{item}</a>"
842 842 current = level
843 843 started = true
844 844 end
845 845 out << '</li></ul>' * (current - root)
846 846 out << '</li></ul>'
847 847 end
848 848 end
849 849 end
850 850
851 851 # Same as Rails' simple_format helper without using paragraphs
852 852 def simple_format_without_paragraph(text)
853 853 text.to_s.
854 854 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
855 855 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
856 856 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
857 857 html_safe
858 858 end
859 859
860 860 def lang_options_for_select(blank=true)
861 861 (blank ? [["(auto)", ""]] : []) +
862 862 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
863 863 end
864 864
865 865 def label_tag_for(name, option_tags = nil, options = {})
866 866 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
867 867 content_tag("label", label_text)
868 868 end
869 869
870 870 def labelled_tabular_form_for(*args, &proc)
871 871 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
872 872 args << {} unless args.last.is_a?(Hash)
873 873 options = args.last
874 874 options[:html] ||= {}
875 875 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
876 876 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
877 877 form_for(*args, &proc)
878 878 end
879 879
880 880 def labelled_form_for(*args, &proc)
881 881 args << {} unless args.last.is_a?(Hash)
882 882 options = args.last
883 883 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
884 884 form_for(*args, &proc)
885 885 end
886 886
887 887 def labelled_fields_for(*args, &proc)
888 888 args << {} unless args.last.is_a?(Hash)
889 889 options = args.last
890 890 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
891 891 fields_for(*args, &proc)
892 892 end
893 893
894 894 def labelled_remote_form_for(*args, &proc)
895 895 args << {} unless args.last.is_a?(Hash)
896 896 options = args.last
897 897 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
898 898 remote_form_for(*args, &proc)
899 899 end
900 900
901 901 def back_url_hidden_field_tag
902 902 back_url = params[:back_url] || request.env['HTTP_REFERER']
903 903 back_url = CGI.unescape(back_url.to_s)
904 904 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
905 905 end
906 906
907 907 def check_all_links(form_name)
908 908 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
909 909 " | ".html_safe +
910 910 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
911 911 end
912 912
913 913 def progress_bar(pcts, options={})
914 914 pcts = [pcts, pcts] unless pcts.is_a?(Array)
915 915 pcts = pcts.collect(&:round)
916 916 pcts[1] = pcts[1] - pcts[0]
917 917 pcts << (100 - pcts[1] - pcts[0])
918 918 width = options[:width] || '100px;'
919 919 legend = options[:legend] || ''
920 920 content_tag('table',
921 921 content_tag('tr',
922 922 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
923 923 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
924 924 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
925 925 ), :class => 'progress', :style => "width: #{width};").html_safe +
926 926 content_tag('p', legend, :class => 'pourcent').html_safe
927 927 end
928 928
929 929 def checked_image(checked=true)
930 930 if checked
931 931 image_tag 'toggle_check.png'
932 932 end
933 933 end
934 934
935 935 def context_menu(url)
936 936 unless @context_menu_included
937 937 content_for :header_tags do
938 938 javascript_include_tag('context_menu') +
939 939 stylesheet_link_tag('context_menu')
940 940 end
941 941 if l(:direction) == 'rtl'
942 942 content_for :header_tags do
943 943 stylesheet_link_tag('context_menu_rtl')
944 944 end
945 945 end
946 946 @context_menu_included = true
947 947 end
948 948 javascript_tag "new ContextMenu('#{ url_for(url) }')"
949 949 end
950 950
951 951 def context_menu_link(name, url, options={})
952 952 options[:class] ||= ''
953 953 if options.delete(:selected)
954 954 options[:class] << ' icon-checked disabled'
955 955 options[:disabled] = true
956 956 end
957 957 if options.delete(:disabled)
958 958 options.delete(:method)
959 959 options.delete(:confirm)
960 960 options.delete(:onclick)
961 961 options[:class] << ' disabled'
962 962 url = '#'
963 963 end
964 964 link_to h(name), url, options
965 965 end
966 966
967 967 def calendar_for(field_id)
968 968 include_calendar_headers_tags
969 969 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
970 970 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
971 971 end
972 972
973 973 def include_calendar_headers_tags
974 974 unless @calendar_headers_tags_included
975 975 @calendar_headers_tags_included = true
976 976 content_for :header_tags do
977 977 start_of_week = case Setting.start_of_week.to_i
978 978 when 1
979 979 'Calendar._FD = 1;' # Monday
980 980 when 7
981 981 'Calendar._FD = 0;' # Sunday
982 982 when 6
983 983 'Calendar._FD = 6;' # Saturday
984 984 else
985 985 '' # use language
986 986 end
987 987
988 988 javascript_include_tag('calendar/calendar') +
989 989 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
990 990 javascript_tag(start_of_week) +
991 991 javascript_include_tag('calendar/calendar-setup') +
992 992 stylesheet_link_tag('calendar')
993 993 end
994 994 end
995 995 end
996 996
997 997 def content_for(name, content = nil, &block)
998 998 @has_content ||= {}
999 999 @has_content[name] = true
1000 1000 super(name, content, &block)
1001 1001 end
1002 1002
1003 1003 def has_content?(name)
1004 1004 (@has_content && @has_content[name]) || false
1005 1005 end
1006 1006
1007 1007 def email_delivery_enabled?
1008 1008 !!ActionMailer::Base.perform_deliveries
1009 1009 end
1010 1010
1011 1011 # Returns the avatar image tag for the given +user+ if avatars are enabled
1012 1012 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1013 1013 def avatar(user, options = { })
1014 1014 if Setting.gravatar_enabled?
1015 1015 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1016 1016 email = nil
1017 1017 if user.respond_to?(:mail)
1018 1018 email = user.mail
1019 1019 elsif user.to_s =~ %r{<(.+?)>}
1020 1020 email = $1
1021 1021 end
1022 1022 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1023 1023 else
1024 1024 ''
1025 1025 end
1026 1026 end
1027 1027
1028 1028 def sanitize_anchor_name(anchor)
1029 1029 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1030 1030 end
1031 1031
1032 1032 # Returns the javascript tags that are included in the html layout head
1033 1033 def javascript_heads
1034 1034 tags = javascript_include_tag(:defaults)
1035 1035 unless User.current.pref.warn_on_leaving_unsaved == '0'
1036 1036 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1037 1037 end
1038 1038 tags
1039 1039 end
1040 1040
1041 1041 def favicon
1042 1042 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1043 1043 end
1044 1044
1045 1045 def robot_exclusion_tag
1046 1046 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1047 1047 end
1048 1048
1049 1049 # Returns true if arg is expected in the API response
1050 1050 def include_in_api_response?(arg)
1051 1051 unless @included_in_api_response
1052 1052 param = params[:include]
1053 1053 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1054 1054 @included_in_api_response.collect!(&:strip)
1055 1055 end
1056 1056 @included_in_api_response.include?(arg.to_s)
1057 1057 end
1058 1058
1059 1059 # Returns options or nil if nometa param or X-Redmine-Nometa header
1060 1060 # was set in the request
1061 1061 def api_meta(options)
1062 1062 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1063 1063 # compatibility mode for activeresource clients that raise
1064 1064 # an error when unserializing an array with attributes
1065 1065 nil
1066 1066 else
1067 1067 options
1068 1068 end
1069 1069 end
1070 1070
1071 1071 private
1072 1072
1073 1073 def wiki_helper
1074 1074 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1075 1075 extend helper
1076 1076 return self
1077 1077 end
1078 1078
1079 1079 def link_to_content_update(text, url_params = {}, html_options = {})
1080 1080 link_to(text, url_params, html_options)
1081 1081 end
1082 1082 end
General Comments 0
You need to be logged in to leave comments. Login now