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