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