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