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