##// END OF EJS Templates
Rails3: helper: html_safe for render_flash_messages method at ApplicationHelper...
Toshi MARUYAMA -
r8001:6d805cfca22f
parent child
Show More
@@ -1,1065 +1,1065
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 s << content_tag('div', v, :class => "flash #{k}")
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 507 text
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 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
600 600 end
601 601 end
602 602 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
603 603 else
604 604 # project or wiki doesn't exist
605 605 all.html_safe
606 606 end
607 607 else
608 608 all.html_safe
609 609 end
610 610 end
611 611 end
612 612
613 613 # Redmine links
614 614 #
615 615 # Examples:
616 616 # Issues:
617 617 # #52 -> Link to issue #52
618 618 # Changesets:
619 619 # r52 -> Link to revision 52
620 620 # commit:a85130f -> Link to scmid starting with a85130f
621 621 # Documents:
622 622 # document#17 -> Link to document with id 17
623 623 # document:Greetings -> Link to the document with title "Greetings"
624 624 # document:"Some document" -> Link to the document with title "Some document"
625 625 # Versions:
626 626 # version#3 -> Link to version with id 3
627 627 # version:1.0.0 -> Link to version named "1.0.0"
628 628 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
629 629 # Attachments:
630 630 # attachment:file.zip -> Link to the attachment of the current object named file.zip
631 631 # Source files:
632 632 # source:some/file -> Link to the file located at /some/file in the project's repository
633 633 # source:some/file@52 -> Link to the file's revision 52
634 634 # source:some/file#L120 -> Link to line 120 of the file
635 635 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
636 636 # export:some/file -> Force the download of the file
637 637 # Forum messages:
638 638 # message#1218 -> Link to message with id 1218
639 639 #
640 640 # Links can refer other objects from other projects, using project identifier:
641 641 # identifier:r52
642 642 # identifier:document:"Some document"
643 643 # identifier:version:1.0.0
644 644 # identifier:source:some/file
645 645 def parse_redmine_links(text, project, obj, attr, only_path, options)
646 646 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|
647 647 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
648 648 link = nil
649 649 if project_identifier
650 650 project = Project.visible.find_by_identifier(project_identifier)
651 651 end
652 652 if esc.nil?
653 653 if prefix.nil? && sep == 'r'
654 654 # project.changesets.visible raises an SQL error because of a double join on repositories
655 655 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
656 656 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
657 657 :class => 'changeset',
658 658 :title => truncate_single_line(changeset.comments, :length => 100))
659 659 end
660 660 elsif sep == '#'
661 661 oid = identifier.to_i
662 662 case prefix
663 663 when nil
664 664 if issue = Issue.visible.find_by_id(oid, :include => :status)
665 665 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
666 666 :class => issue.css_classes,
667 667 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
668 668 end
669 669 when 'document'
670 670 if document = Document.visible.find_by_id(oid)
671 671 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
672 672 :class => 'document'
673 673 end
674 674 when 'version'
675 675 if version = Version.visible.find_by_id(oid)
676 676 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
677 677 :class => 'version'
678 678 end
679 679 when 'message'
680 680 if message = Message.visible.find_by_id(oid, :include => :parent)
681 681 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
682 682 end
683 683 when 'forum'
684 684 if board = Board.visible.find_by_id(oid)
685 685 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
686 686 :class => 'board'
687 687 end
688 688 when 'news'
689 689 if news = News.visible.find_by_id(oid)
690 690 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
691 691 :class => 'news'
692 692 end
693 693 when 'project'
694 694 if p = Project.visible.find_by_id(oid)
695 695 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
696 696 end
697 697 end
698 698 elsif sep == ':'
699 699 # removes the double quotes if any
700 700 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
701 701 case prefix
702 702 when 'document'
703 703 if project && document = project.documents.visible.find_by_title(name)
704 704 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
705 705 :class => 'document'
706 706 end
707 707 when 'version'
708 708 if project && version = project.versions.visible.find_by_name(name)
709 709 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
710 710 :class => 'version'
711 711 end
712 712 when 'forum'
713 713 if project && board = project.boards.visible.find_by_name(name)
714 714 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
715 715 :class => 'board'
716 716 end
717 717 when 'news'
718 718 if project && news = project.news.visible.find_by_title(name)
719 719 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
720 720 :class => 'news'
721 721 end
722 722 when 'commit'
723 723 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
724 724 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
725 725 :class => 'changeset',
726 726 :title => truncate_single_line(h(changeset.comments), :length => 100)
727 727 end
728 728 when 'source', 'export'
729 729 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
730 730 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
731 731 path, rev, anchor = $1, $3, $5
732 732 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
733 733 :path => to_path_param(path),
734 734 :rev => rev,
735 735 :anchor => anchor,
736 736 :format => (prefix == 'export' ? 'raw' : nil)},
737 737 :class => (prefix == 'export' ? 'source download' : 'source')
738 738 end
739 739 when 'attachment'
740 740 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
741 741 if attachments && attachment = attachments.detect {|a| a.filename == name }
742 742 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
743 743 :class => 'attachment'
744 744 end
745 745 when 'project'
746 746 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
747 747 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
748 748 end
749 749 end
750 750 end
751 751 end
752 752 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
753 753 end
754 754 end
755 755
756 756 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
757 757
758 758 def parse_sections(text, project, obj, attr, only_path, options)
759 759 return unless options[:edit_section_links]
760 760 text.gsub!(HEADING_RE) do
761 761 @current_section += 1
762 762 if @current_section > 1
763 763 content_tag('div',
764 764 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
765 765 :class => 'contextual',
766 766 :title => l(:button_edit_section)) + $1
767 767 else
768 768 $1
769 769 end
770 770 end
771 771 end
772 772
773 773 # Headings and TOC
774 774 # Adds ids and links to headings unless options[:headings] is set to false
775 775 def parse_headings(text, project, obj, attr, only_path, options)
776 776 return if options[:headings] == false
777 777
778 778 text.gsub!(HEADING_RE) do
779 779 level, attrs, content = $2.to_i, $3, $4
780 780 item = strip_tags(content).strip
781 781 anchor = sanitize_anchor_name(item)
782 782 # used for single-file wiki export
783 783 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
784 784 @parsed_headings << [level, anchor, item]
785 785 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
786 786 end
787 787 end
788 788
789 789 MACROS_RE = /
790 790 (!)? # escaping
791 791 (
792 792 \{\{ # opening tag
793 793 ([\w]+) # macro name
794 794 (\(([^\}]*)\))? # optional arguments
795 795 \}\} # closing tag
796 796 )
797 797 /x unless const_defined?(:MACROS_RE)
798 798
799 799 # Macros substitution
800 800 def parse_macros(text, project, obj, attr, only_path, options)
801 801 text.gsub!(MACROS_RE) do
802 802 esc, all, macro = $1, $2, $3.downcase
803 803 args = ($5 || '').split(',').each(&:strip)
804 804 if esc.nil?
805 805 begin
806 806 exec_macro(macro, obj, args)
807 807 rescue => e
808 808 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
809 809 end || all
810 810 else
811 811 all
812 812 end
813 813 end
814 814 end
815 815
816 816 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
817 817
818 818 # Renders the TOC with given headings
819 819 def replace_toc(text, headings)
820 820 text.gsub!(TOC_RE) do
821 821 if headings.empty?
822 822 ''
823 823 else
824 824 div_class = 'toc'
825 825 div_class << ' right' if $1 == '>'
826 826 div_class << ' left' if $1 == '<'
827 827 out = "<ul class=\"#{div_class}\"><li>"
828 828 root = headings.map(&:first).min
829 829 current = root
830 830 started = false
831 831 headings.each do |level, anchor, item|
832 832 if level > current
833 833 out << '<ul><li>' * (level - current)
834 834 elsif level < current
835 835 out << "</li></ul>\n" * (current - level) + "</li><li>"
836 836 elsif started
837 837 out << '</li><li>'
838 838 end
839 839 out << "<a href=\"##{anchor}\">#{item}</a>"
840 840 current = level
841 841 started = true
842 842 end
843 843 out << '</li></ul>' * (current - root)
844 844 out << '</li></ul>'
845 845 end
846 846 end
847 847 end
848 848
849 849 # Same as Rails' simple_format helper without using paragraphs
850 850 def simple_format_without_paragraph(text)
851 851 text.to_s.
852 852 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
853 853 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
854 854 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
855 855 html_safe
856 856 end
857 857
858 858 def lang_options_for_select(blank=true)
859 859 (blank ? [["(auto)", ""]] : []) +
860 860 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
861 861 end
862 862
863 863 def label_tag_for(name, option_tags = nil, options = {})
864 864 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
865 865 content_tag("label", label_text)
866 866 end
867 867
868 868 def labelled_tabular_form_for(*args, &proc)
869 869 args << {} unless args.last.is_a?(Hash)
870 870 options = args.last
871 871 options[:html] ||= {}
872 872 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
873 873 options.merge!({:builder => TabularFormBuilder})
874 874 form_for(*args, &proc)
875 875 end
876 876
877 877 def labelled_form_for(*args, &proc)
878 878 args << {} unless args.last.is_a?(Hash)
879 879 options = args.last
880 880 options.merge!({:builder => TabularFormBuilder})
881 881 form_for(*args, &proc)
882 882 end
883 883
884 884 def back_url_hidden_field_tag
885 885 back_url = params[:back_url] || request.env['HTTP_REFERER']
886 886 back_url = CGI.unescape(back_url.to_s)
887 887 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
888 888 end
889 889
890 890 def check_all_links(form_name)
891 891 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
892 892 " | ".html_safe +
893 893 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
894 894 end
895 895
896 896 def progress_bar(pcts, options={})
897 897 pcts = [pcts, pcts] unless pcts.is_a?(Array)
898 898 pcts = pcts.collect(&:round)
899 899 pcts[1] = pcts[1] - pcts[0]
900 900 pcts << (100 - pcts[1] - pcts[0])
901 901 width = options[:width] || '100px;'
902 902 legend = options[:legend] || ''
903 903 content_tag('table',
904 904 content_tag('tr',
905 905 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
906 906 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
907 907 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
908 908 ), :class => 'progress', :style => "width: #{width};").html_safe +
909 909 content_tag('p', legend, :class => 'pourcent').html_safe
910 910 end
911 911
912 912 def checked_image(checked=true)
913 913 if checked
914 914 image_tag 'toggle_check.png'
915 915 end
916 916 end
917 917
918 918 def context_menu(url)
919 919 unless @context_menu_included
920 920 content_for :header_tags do
921 921 javascript_include_tag('context_menu') +
922 922 stylesheet_link_tag('context_menu')
923 923 end
924 924 if l(:direction) == 'rtl'
925 925 content_for :header_tags do
926 926 stylesheet_link_tag('context_menu_rtl')
927 927 end
928 928 end
929 929 @context_menu_included = true
930 930 end
931 931 javascript_tag "new ContextMenu('#{ url_for(url) }')"
932 932 end
933 933
934 934 def context_menu_link(name, url, options={})
935 935 options[:class] ||= ''
936 936 if options.delete(:selected)
937 937 options[:class] << ' icon-checked disabled'
938 938 options[:disabled] = true
939 939 end
940 940 if options.delete(:disabled)
941 941 options.delete(:method)
942 942 options.delete(:confirm)
943 943 options.delete(:onclick)
944 944 options[:class] << ' disabled'
945 945 url = '#'
946 946 end
947 947 link_to h(name), url, options
948 948 end
949 949
950 950 def calendar_for(field_id)
951 951 include_calendar_headers_tags
952 952 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
953 953 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
954 954 end
955 955
956 956 def include_calendar_headers_tags
957 957 unless @calendar_headers_tags_included
958 958 @calendar_headers_tags_included = true
959 959 content_for :header_tags do
960 960 start_of_week = case Setting.start_of_week.to_i
961 961 when 1
962 962 'Calendar._FD = 1;' # Monday
963 963 when 7
964 964 'Calendar._FD = 0;' # Sunday
965 965 when 6
966 966 'Calendar._FD = 6;' # Saturday
967 967 else
968 968 '' # use language
969 969 end
970 970
971 971 javascript_include_tag('calendar/calendar') +
972 972 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
973 973 javascript_tag(start_of_week) +
974 974 javascript_include_tag('calendar/calendar-setup') +
975 975 stylesheet_link_tag('calendar')
976 976 end
977 977 end
978 978 end
979 979
980 980 def content_for(name, content = nil, &block)
981 981 @has_content ||= {}
982 982 @has_content[name] = true
983 983 super(name, content, &block)
984 984 end
985 985
986 986 def has_content?(name)
987 987 (@has_content && @has_content[name]) || false
988 988 end
989 989
990 990 def email_delivery_enabled?
991 991 !!ActionMailer::Base.perform_deliveries
992 992 end
993 993
994 994 # Returns the avatar image tag for the given +user+ if avatars are enabled
995 995 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
996 996 def avatar(user, options = { })
997 997 if Setting.gravatar_enabled?
998 998 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
999 999 email = nil
1000 1000 if user.respond_to?(:mail)
1001 1001 email = user.mail
1002 1002 elsif user.to_s =~ %r{<(.+?)>}
1003 1003 email = $1
1004 1004 end
1005 1005 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1006 1006 else
1007 1007 ''
1008 1008 end
1009 1009 end
1010 1010
1011 1011 def sanitize_anchor_name(anchor)
1012 1012 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1013 1013 end
1014 1014
1015 1015 # Returns the javascript tags that are included in the html layout head
1016 1016 def javascript_heads
1017 1017 tags = javascript_include_tag(:defaults)
1018 1018 unless User.current.pref.warn_on_leaving_unsaved == '0'
1019 1019 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1020 1020 end
1021 1021 tags
1022 1022 end
1023 1023
1024 1024 def favicon
1025 1025 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1026 1026 end
1027 1027
1028 1028 def robot_exclusion_tag
1029 1029 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1030 1030 end
1031 1031
1032 1032 # Returns true if arg is expected in the API response
1033 1033 def include_in_api_response?(arg)
1034 1034 unless @included_in_api_response
1035 1035 param = params[:include]
1036 1036 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1037 1037 @included_in_api_response.collect!(&:strip)
1038 1038 end
1039 1039 @included_in_api_response.include?(arg.to_s)
1040 1040 end
1041 1041
1042 1042 # Returns options or nil if nometa param or X-Redmine-Nometa header
1043 1043 # was set in the request
1044 1044 def api_meta(options)
1045 1045 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1046 1046 # compatibility mode for activeresource clients that raise
1047 1047 # an error when unserializing an array with attributes
1048 1048 nil
1049 1049 else
1050 1050 options
1051 1051 end
1052 1052 end
1053 1053
1054 1054 private
1055 1055
1056 1056 def wiki_helper
1057 1057 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1058 1058 extend helper
1059 1059 return self
1060 1060 end
1061 1061
1062 1062 def link_to_content_update(text, url_params = {}, html_options = {})
1063 1063 link_to(text, url_params, html_options)
1064 1064 end
1065 1065 end
General Comments 0
You need to be logged in to leave comments. Login now