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