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