##// END OF EJS Templates
Rails3: use String#html_safe for render_project_jump_box() at ApplicationHelper....
Toshi MARUYAMA -
r6357:80638bce45e8
parent child
Show More
@@ -1,966 +1,966
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 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.html_safe
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 s
236 s.html_safe
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 # \xc2\xab(utf-8) = &#171;
357 357 html << link_to_content_update(
358 358 "\xc2\xab " + l(:label_previous),
359 359 url_param.merge(page_param => paginator.current.previous)) + ' '
360 360 end
361 361
362 362 html << (pagination_links_each(paginator, options) do |n|
363 363 link_to_content_update(n.to_s, url_param.merge(page_param => n))
364 364 end || '')
365 365
366 366 if paginator.current.next
367 367 # \xc2\xbb(utf-8) = &#187;
368 368 html << ' ' + link_to_content_update(
369 369 (l(:label_next) + " \xc2\xbb"),
370 370 url_param.merge(page_param => paginator.current.next))
371 371 end
372 372
373 373 unless count.nil?
374 374 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
375 375 if per_page_links != false && links = per_page_links(paginator.items_per_page)
376 376 html << " | #{links}"
377 377 end
378 378 end
379 379
380 380 html
381 381 end
382 382
383 383 def per_page_links(selected=nil)
384 384 links = Setting.per_page_options_array.collect do |n|
385 385 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
386 386 end
387 387 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
388 388 end
389 389
390 390 def reorder_links(name, url)
391 391 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
392 392 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
393 393 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
394 394 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
395 395 end
396 396
397 397 def breadcrumb(*args)
398 398 elements = args.flatten
399 399 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
400 400 end
401 401
402 402 def other_formats_links(&block)
403 403 concat('<p class="other-formats">' + l(:label_export_to))
404 404 yield Redmine::Views::OtherFormatsBuilder.new(self)
405 405 concat('</p>')
406 406 end
407 407
408 408 def page_header_title
409 409 if @project.nil? || @project.new_record?
410 410 h(Setting.app_title)
411 411 else
412 412 b = []
413 413 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
414 414 if ancestors.any?
415 415 root = ancestors.shift
416 416 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
417 417 if ancestors.size > 2
418 418 b << '&#8230;'
419 419 ancestors = ancestors[-2, 2]
420 420 end
421 421 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
422 422 end
423 423 b << h(@project)
424 424 b.join(' &#187; ')
425 425 end
426 426 end
427 427
428 428 def html_title(*args)
429 429 if args.empty?
430 430 title = []
431 431 title << h(@project.name) if @project
432 432 title += @html_title if @html_title
433 433 title << Setting.app_title
434 434 title.select {|t| !t.blank? }.join(' - ')
435 435 else
436 436 @html_title ||= []
437 437 @html_title += args
438 438 end
439 439 end
440 440
441 441 # Returns the theme, controller name, and action as css classes for the
442 442 # HTML body.
443 443 def body_css_classes
444 444 css = []
445 445 if theme = Redmine::Themes.theme(Setting.ui_theme)
446 446 css << 'theme-' + theme.name
447 447 end
448 448
449 449 css << 'controller-' + params[:controller]
450 450 css << 'action-' + params[:action]
451 451 css.join(' ')
452 452 end
453 453
454 454 def accesskey(s)
455 455 Redmine::AccessKeys.key_for s
456 456 end
457 457
458 458 # Formats text according to system settings.
459 459 # 2 ways to call this method:
460 460 # * with a String: textilizable(text, options)
461 461 # * with an object and one of its attribute: textilizable(issue, :description, options)
462 462 def textilizable(*args)
463 463 options = args.last.is_a?(Hash) ? args.pop : {}
464 464 case args.size
465 465 when 1
466 466 obj = options[:object]
467 467 text = args.shift
468 468 when 2
469 469 obj = args.shift
470 470 attr = args.shift
471 471 text = obj.send(attr).to_s
472 472 else
473 473 raise ArgumentError, 'invalid arguments to textilizable'
474 474 end
475 475 return '' if text.blank?
476 476 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
477 477 only_path = options.delete(:only_path) == false ? false : true
478 478
479 479 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
480 480
481 481 @parsed_headings = []
482 482 text = parse_non_pre_blocks(text) do |text|
483 483 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
484 484 send method_name, text, project, obj, attr, only_path, options
485 485 end
486 486 end
487 487
488 488 if @parsed_headings.any?
489 489 replace_toc(text, @parsed_headings)
490 490 end
491 491
492 492 text
493 493 end
494 494
495 495 def parse_non_pre_blocks(text)
496 496 s = StringScanner.new(text)
497 497 tags = []
498 498 parsed = ''
499 499 while !s.eos?
500 500 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
501 501 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
502 502 if tags.empty?
503 503 yield text
504 504 end
505 505 parsed << text
506 506 if tag
507 507 if closing
508 508 if tags.last == tag.downcase
509 509 tags.pop
510 510 end
511 511 else
512 512 tags << tag.downcase
513 513 end
514 514 parsed << full_tag
515 515 end
516 516 end
517 517 # Close any non closing tags
518 518 while tag = tags.pop
519 519 parsed << "</#{tag}>"
520 520 end
521 521 parsed
522 522 end
523 523
524 524 def parse_inline_attachments(text, project, obj, attr, only_path, options)
525 525 # when using an image link, try to use an attachment, if possible
526 526 if options[:attachments] || (obj && obj.respond_to?(:attachments))
527 527 attachments = nil
528 528 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
529 529 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
530 530 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
531 531 # search for the picture in attachments
532 532 if found = attachments.detect { |att| att.filename.downcase == filename }
533 533 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
534 534 desc = found.description.to_s.gsub('"', '')
535 535 if !desc.blank? && alttext.blank?
536 536 alt = " title=\"#{desc}\" alt=\"#{desc}\""
537 537 end
538 538 "src=\"#{image_url}\"#{alt}"
539 539 else
540 540 m
541 541 end
542 542 end
543 543 end
544 544 end
545 545
546 546 # Wiki links
547 547 #
548 548 # Examples:
549 549 # [[mypage]]
550 550 # [[mypage|mytext]]
551 551 # wiki links can refer other project wikis, using project name or identifier:
552 552 # [[project:]] -> wiki starting page
553 553 # [[project:|mytext]]
554 554 # [[project:mypage]]
555 555 # [[project:mypage|mytext]]
556 556 def parse_wiki_links(text, project, obj, attr, only_path, options)
557 557 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
558 558 link_project = project
559 559 esc, all, page, title = $1, $2, $3, $5
560 560 if esc.nil?
561 561 if page =~ /^([^\:]+)\:(.*)$/
562 562 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
563 563 page = $2
564 564 title ||= $1 if page.blank?
565 565 end
566 566
567 567 if link_project && link_project.wiki
568 568 # extract anchor
569 569 anchor = nil
570 570 if page =~ /^(.+?)\#(.+)$/
571 571 page, anchor = $1, $2
572 572 end
573 573 # check if page exists
574 574 wiki_page = link_project.wiki.find_page(page)
575 575 url = case options[:wiki_links]
576 576 when :local; "#{title}.html"
577 577 when :anchor; "##{title}" # used for single-file wiki export
578 578 else
579 579 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
580 580 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
581 581 end
582 582 link_to(h(title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
583 583 else
584 584 # project or wiki doesn't exist
585 585 all
586 586 end
587 587 else
588 588 all
589 589 end
590 590 end
591 591 end
592 592
593 593 # Redmine links
594 594 #
595 595 # Examples:
596 596 # Issues:
597 597 # #52 -> Link to issue #52
598 598 # Changesets:
599 599 # r52 -> Link to revision 52
600 600 # commit:a85130f -> Link to scmid starting with a85130f
601 601 # Documents:
602 602 # document#17 -> Link to document with id 17
603 603 # document:Greetings -> Link to the document with title "Greetings"
604 604 # document:"Some document" -> Link to the document with title "Some document"
605 605 # Versions:
606 606 # version#3 -> Link to version with id 3
607 607 # version:1.0.0 -> Link to version named "1.0.0"
608 608 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
609 609 # Attachments:
610 610 # attachment:file.zip -> Link to the attachment of the current object named file.zip
611 611 # Source files:
612 612 # source:some/file -> Link to the file located at /some/file in the project's repository
613 613 # source:some/file@52 -> Link to the file's revision 52
614 614 # source:some/file#L120 -> Link to line 120 of the file
615 615 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
616 616 # export:some/file -> Force the download of the file
617 617 # Forum messages:
618 618 # message#1218 -> Link to message with id 1218
619 619 #
620 620 # Links can refer other objects from other projects, using project identifier:
621 621 # identifier:r52
622 622 # identifier:document:"Some document"
623 623 # identifier:version:1.0.0
624 624 # identifier:source:some/file
625 625 def parse_redmine_links(text, project, obj, attr, only_path, options)
626 626 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
627 627 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
628 628 link = nil
629 629 if project_identifier
630 630 project = Project.visible.find_by_identifier(project_identifier)
631 631 end
632 632 if esc.nil?
633 633 if prefix.nil? && sep == 'r'
634 634 # project.changesets.visible raises an SQL error because of a double join on repositories
635 635 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
636 636 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
637 637 :class => 'changeset',
638 638 :title => truncate_single_line(changeset.comments, :length => 100))
639 639 end
640 640 elsif sep == '#'
641 641 oid = identifier.to_i
642 642 case prefix
643 643 when nil
644 644 if issue = Issue.visible.find_by_id(oid, :include => :status)
645 645 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
646 646 :class => issue.css_classes,
647 647 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
648 648 end
649 649 when 'document'
650 650 if document = Document.visible.find_by_id(oid)
651 651 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
652 652 :class => 'document'
653 653 end
654 654 when 'version'
655 655 if version = Version.visible.find_by_id(oid)
656 656 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
657 657 :class => 'version'
658 658 end
659 659 when 'message'
660 660 if message = Message.visible.find_by_id(oid, :include => :parent)
661 661 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
662 662 end
663 663 when 'project'
664 664 if p = Project.visible.find_by_id(oid)
665 665 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
666 666 end
667 667 end
668 668 elsif sep == ':'
669 669 # removes the double quotes if any
670 670 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
671 671 case prefix
672 672 when 'document'
673 673 if project && document = project.documents.visible.find_by_title(name)
674 674 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
675 675 :class => 'document'
676 676 end
677 677 when 'version'
678 678 if project && version = project.versions.visible.find_by_name(name)
679 679 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
680 680 :class => 'version'
681 681 end
682 682 when 'commit'
683 683 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
684 684 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
685 685 :class => 'changeset',
686 686 :title => truncate_single_line(h(changeset.comments), :length => 100)
687 687 end
688 688 when 'source', 'export'
689 689 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
690 690 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
691 691 path, rev, anchor = $1, $3, $5
692 692 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
693 693 :path => to_path_param(path),
694 694 :rev => rev,
695 695 :anchor => anchor,
696 696 :format => (prefix == 'export' ? 'raw' : nil)},
697 697 :class => (prefix == 'export' ? 'source download' : 'source')
698 698 end
699 699 when 'attachment'
700 700 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
701 701 if attachments && attachment = attachments.detect {|a| a.filename == name }
702 702 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
703 703 :class => 'attachment'
704 704 end
705 705 when 'project'
706 706 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
707 707 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
708 708 end
709 709 end
710 710 end
711 711 end
712 712 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
713 713 end
714 714 end
715 715
716 716 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
717 717
718 718 # Headings and TOC
719 719 # Adds ids and links to headings unless options[:headings] is set to false
720 720 def parse_headings(text, project, obj, attr, only_path, options)
721 721 return if options[:headings] == false
722 722
723 723 text.gsub!(HEADING_RE) do
724 724 level, attrs, content = $1.to_i, $2, $3
725 725 item = strip_tags(content).strip
726 726 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
727 727 @parsed_headings << [level, anchor, item]
728 728 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
729 729 end
730 730 end
731 731
732 732 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
733 733
734 734 # Renders the TOC with given headings
735 735 def replace_toc(text, headings)
736 736 text.gsub!(TOC_RE) do
737 737 if headings.empty?
738 738 ''
739 739 else
740 740 div_class = 'toc'
741 741 div_class << ' right' if $1 == '>'
742 742 div_class << ' left' if $1 == '<'
743 743 out = "<ul class=\"#{div_class}\"><li>"
744 744 root = headings.map(&:first).min
745 745 current = root
746 746 started = false
747 747 headings.each do |level, anchor, item|
748 748 if level > current
749 749 out << '<ul><li>' * (level - current)
750 750 elsif level < current
751 751 out << "</li></ul>\n" * (current - level) + "</li><li>"
752 752 elsif started
753 753 out << '</li><li>'
754 754 end
755 755 out << "<a href=\"##{anchor}\">#{item}</a>"
756 756 current = level
757 757 started = true
758 758 end
759 759 out << '</li></ul>' * (current - root)
760 760 out << '</li></ul>'
761 761 end
762 762 end
763 763 end
764 764
765 765 # Same as Rails' simple_format helper without using paragraphs
766 766 def simple_format_without_paragraph(text)
767 767 text.to_s.
768 768 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
769 769 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
770 770 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
771 771 end
772 772
773 773 def lang_options_for_select(blank=true)
774 774 (blank ? [["(auto)", ""]] : []) +
775 775 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
776 776 end
777 777
778 778 def label_tag_for(name, option_tags = nil, options = {})
779 779 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
780 780 content_tag("label", label_text)
781 781 end
782 782
783 783 def labelled_tabular_form_for(name, object, options, &proc)
784 784 options[:html] ||= {}
785 785 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
786 786 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
787 787 end
788 788
789 789 def back_url_hidden_field_tag
790 790 back_url = params[:back_url] || request.env['HTTP_REFERER']
791 791 back_url = CGI.unescape(back_url.to_s)
792 792 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
793 793 end
794 794
795 795 def check_all_links(form_name)
796 796 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
797 797 " | " +
798 798 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
799 799 end
800 800
801 801 def progress_bar(pcts, options={})
802 802 pcts = [pcts, pcts] unless pcts.is_a?(Array)
803 803 pcts = pcts.collect(&:round)
804 804 pcts[1] = pcts[1] - pcts[0]
805 805 pcts << (100 - pcts[1] - pcts[0])
806 806 width = options[:width] || '100px;'
807 807 legend = options[:legend] || ''
808 808 content_tag('table',
809 809 content_tag('tr',
810 810 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
811 811 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
812 812 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
813 813 ), :class => 'progress', :style => "width: #{width};") +
814 814 content_tag('p', legend, :class => 'pourcent')
815 815 end
816 816
817 817 def checked_image(checked=true)
818 818 if checked
819 819 image_tag 'toggle_check.png'
820 820 end
821 821 end
822 822
823 823 def context_menu(url)
824 824 unless @context_menu_included
825 825 content_for :header_tags do
826 826 javascript_include_tag('context_menu') +
827 827 stylesheet_link_tag('context_menu')
828 828 end
829 829 if l(:direction) == 'rtl'
830 830 content_for :header_tags do
831 831 stylesheet_link_tag('context_menu_rtl')
832 832 end
833 833 end
834 834 @context_menu_included = true
835 835 end
836 836 javascript_tag "new ContextMenu('#{ url_for(url) }')"
837 837 end
838 838
839 839 def context_menu_link(name, url, options={})
840 840 options[:class] ||= ''
841 841 if options.delete(:selected)
842 842 options[:class] << ' icon-checked disabled'
843 843 options[:disabled] = true
844 844 end
845 845 if options.delete(:disabled)
846 846 options.delete(:method)
847 847 options.delete(:confirm)
848 848 options.delete(:onclick)
849 849 options[:class] << ' disabled'
850 850 url = '#'
851 851 end
852 852 link_to h(name), url, options
853 853 end
854 854
855 855 def calendar_for(field_id)
856 856 include_calendar_headers_tags
857 857 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
858 858 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
859 859 end
860 860
861 861 def include_calendar_headers_tags
862 862 unless @calendar_headers_tags_included
863 863 @calendar_headers_tags_included = true
864 864 content_for :header_tags do
865 865 start_of_week = case Setting.start_of_week.to_i
866 866 when 1
867 867 'Calendar._FD = 1;' # Monday
868 868 when 7
869 869 'Calendar._FD = 0;' # Sunday
870 870 when 6
871 871 'Calendar._FD = 6;' # Saturday
872 872 else
873 873 '' # use language
874 874 end
875 875
876 876 javascript_include_tag('calendar/calendar') +
877 877 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
878 878 javascript_tag(start_of_week) +
879 879 javascript_include_tag('calendar/calendar-setup') +
880 880 stylesheet_link_tag('calendar')
881 881 end
882 882 end
883 883 end
884 884
885 885 def content_for(name, content = nil, &block)
886 886 @has_content ||= {}
887 887 @has_content[name] = true
888 888 super(name, content, &block)
889 889 end
890 890
891 891 def has_content?(name)
892 892 (@has_content && @has_content[name]) || false
893 893 end
894 894
895 895 def email_delivery_enabled?
896 896 !!ActionMailer::Base.perform_deliveries
897 897 end
898 898
899 899 # Returns the avatar image tag for the given +user+ if avatars are enabled
900 900 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
901 901 def avatar(user, options = { })
902 902 if Setting.gravatar_enabled?
903 903 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
904 904 email = nil
905 905 if user.respond_to?(:mail)
906 906 email = user.mail
907 907 elsif user.to_s =~ %r{<(.+?)>}
908 908 email = $1
909 909 end
910 910 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
911 911 else
912 912 ''
913 913 end
914 914 end
915 915
916 916 # Returns the javascript tags that are included in the html layout head
917 917 def javascript_heads
918 918 tags = javascript_include_tag(:defaults)
919 919 unless User.current.pref.warn_on_leaving_unsaved == '0'
920 920 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
921 921 end
922 922 tags
923 923 end
924 924
925 925 def favicon
926 926 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
927 927 end
928 928
929 929 def robot_exclusion_tag
930 930 '<meta name="robots" content="noindex,follow,noarchive" />'
931 931 end
932 932
933 933 # Returns true if arg is expected in the API response
934 934 def include_in_api_response?(arg)
935 935 unless @included_in_api_response
936 936 param = params[:include]
937 937 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
938 938 @included_in_api_response.collect!(&:strip)
939 939 end
940 940 @included_in_api_response.include?(arg.to_s)
941 941 end
942 942
943 943 # Returns options or nil if nometa param or X-Redmine-Nometa header
944 944 # was set in the request
945 945 def api_meta(options)
946 946 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
947 947 # compatibility mode for activeresource clients that raise
948 948 # an error when unserializing an array with attributes
949 949 nil
950 950 else
951 951 options
952 952 end
953 953 end
954 954
955 955 private
956 956
957 957 def wiki_helper
958 958 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
959 959 extend helper
960 960 return self
961 961 end
962 962
963 963 def link_to_content_update(text, url_params = {}, html_options = {})
964 964 link_to(text, url_params, html_options)
965 965 end
966 966 end
General Comments 0
You need to be logged in to leave comments. Login now