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