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