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