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