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