##// END OF EJS Templates
Adds a optgroup for groups in users/groups select tags....
Jean-Philippe Lang -
r6187:ed01ae121daf
parent child
Show More
@@ -1,946 +1,960
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'forwardable'
19 19 require 'cgi'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23 include Redmine::I18n
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 # Return true if user is authorized for controller/action, otherwise false
30 30 def authorize_for(controller, action)
31 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 32 end
33 33
34 34 # Display a link if user is authorized
35 35 #
36 36 # @param [String] name Anchor text (passed to link_to)
37 37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 38 # @param [optional, Hash] html_options Options passed to link_to
39 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 42 end
43 43
44 44 # Display a link to remote if user is authorized
45 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 46 url = options[:url] || {}
47 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active?
55 55 link_to name, :controller => 'users', :action => 'show', :id => user
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 #
72 72 def link_to_issue(issue, options={})
73 73 title = nil
74 74 subject = nil
75 75 if options[:subject] == false
76 76 title = truncate(issue.subject, :length => 60)
77 77 else
78 78 subject = issue.subject
79 79 if options[:truncate]
80 80 subject = truncate(subject, :length => options[:truncate])
81 81 end
82 82 end
83 83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 84 :class => issue.css_classes,
85 85 :title => title
86 86 s << ": #{h subject}" if subject
87 87 s = "#{h issue.project} - " + s if options[:project]
88 88 s
89 89 end
90 90
91 91 # Generates a link to an attachment.
92 92 # Options:
93 93 # * :text - Link text (default to attachment filename)
94 94 # * :download - Force download (default: false)
95 95 def link_to_attachment(attachment, options={})
96 96 text = options.delete(:text) || attachment.filename
97 97 action = options.delete(:download) ? 'download' : 'show'
98 98
99 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, project, options={})
106 106 text = options.delete(:text) || format_revision(revision)
107 107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108 108
109 109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 110 :title => l(:label_revision_id, format_revision(revision)))
111 111 end
112 112
113 113 # Generates a link to a message
114 114 def link_to_message(message, options={}, html_options = nil)
115 115 link_to(
116 116 h(truncate(message.subject, :length => 60)),
117 117 { :controller => 'messages', :action => 'show',
118 118 :board_id => message.board_id,
119 119 :id => message.root,
120 120 :r => (message.parent_id && message.id),
121 121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 122 }.merge(options),
123 123 html_options
124 124 )
125 125 end
126 126
127 127 # Generates a link to a project if active
128 128 # Examples:
129 129 #
130 130 # link_to_project(project) # => link to the specified project overview
131 131 # link_to_project(project, :action=>'settings') # => link to project settings
132 132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 134 #
135 135 def link_to_project(project, options={}, html_options = nil)
136 136 if project.active?
137 137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
138 138 link_to(h(project), url, html_options)
139 139 else
140 140 h(project)
141 141 end
142 142 end
143 143
144 144 def toggle_link(name, id, options={})
145 145 onclick = "Element.toggle('#{id}'); "
146 146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
147 147 onclick << "return false;"
148 148 link_to(name, "#", :onclick => onclick)
149 149 end
150 150
151 151 def image_to_function(name, function, html_options = {})
152 152 html_options.symbolize_keys!
153 153 tag(:input, html_options.merge({
154 154 :type => "image", :src => image_path(name),
155 155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
156 156 }))
157 157 end
158 158
159 159 def prompt_to_remote(name, text, param, url, html_options = {})
160 160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
161 161 link_to name, {}, html_options
162 162 end
163 163
164 164 def format_activity_title(text)
165 165 h(truncate_single_line(text, :length => 100))
166 166 end
167 167
168 168 def format_activity_day(date)
169 169 date == Date.today ? l(:label_today).titleize : format_date(date)
170 170 end
171 171
172 172 def format_activity_description(text)
173 173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
174 174 end
175 175
176 176 def format_version_name(version)
177 177 if version.project == @project
178 178 h(version)
179 179 else
180 180 h("#{version.project} - #{version}")
181 181 end
182 182 end
183 183
184 184 def due_date_distance_in_words(date)
185 185 if date
186 186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
187 187 end
188 188 end
189 189
190 190 def render_page_hierarchy(pages, node=nil, options={})
191 191 content = ''
192 192 if pages[node]
193 193 content << "<ul class=\"pages-hierarchy\">\n"
194 194 pages[node].each do |page|
195 195 content << "<li>"
196 196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
197 197 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
198 198 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
199 199 content << "</li>\n"
200 200 end
201 201 content << "</ul>\n"
202 202 end
203 203 content
204 204 end
205 205
206 206 # Renders flash messages
207 207 def render_flash_messages
208 208 s = ''
209 209 flash.each do |k,v|
210 210 s << content_tag('div', v, :class => "flash #{k}")
211 211 end
212 212 s
213 213 end
214 214
215 215 # Renders tabs and their content
216 216 def render_tabs(tabs)
217 217 if tabs.any?
218 218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
219 219 else
220 220 content_tag 'p', l(:label_no_data), :class => "nodata"
221 221 end
222 222 end
223 223
224 224 # Renders the project quick-jump box
225 225 def render_project_jump_box
226 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
295 # Returns a string for users/groups option tags
296 def principals_options_for_select(collection, selected=nil)
297 s = ''
298 groups = ''
299 collection.sort.each do |element|
300 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
301 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
302 end
303 unless groups.empty?
304 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
305 end
306 s
307 end
294 308
295 309 # Truncates and returns the string as a single line
296 310 def truncate_single_line(string, *args)
297 311 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
298 312 end
299 313
300 314 # Truncates at line break after 250 characters or options[:length]
301 315 def truncate_lines(string, options={})
302 316 length = options[:length] || 250
303 317 if string.to_s =~ /\A(.{#{length}}.*?)$/m
304 318 "#{$1}..."
305 319 else
306 320 string
307 321 end
308 322 end
309 323
310 324 def html_hours(text)
311 325 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
312 326 end
313 327
314 328 def authoring(created, author, options={})
315 329 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
316 330 end
317 331
318 332 def time_tag(time)
319 333 text = distance_of_time_in_words(Time.now, time)
320 334 if @project
321 335 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
322 336 else
323 337 content_tag('acronym', text, :title => format_time(time))
324 338 end
325 339 end
326 340
327 341 def syntax_highlight(name, content)
328 342 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
329 343 end
330 344
331 345 def to_path_param(path)
332 346 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
333 347 end
334 348
335 349 def pagination_links_full(paginator, count=nil, options={})
336 350 page_param = options.delete(:page_param) || :page
337 351 per_page_links = options.delete(:per_page_links)
338 352 url_param = params.dup
339 353
340 354 html = ''
341 355 if paginator.current.previous
342 356 html << link_to_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
343 357 end
344 358
345 359 html << (pagination_links_each(paginator, options) do |n|
346 360 link_to_content_update(n.to_s, url_param.merge(page_param => n))
347 361 end || '')
348 362
349 363 if paginator.current.next
350 364 html << ' ' + link_to_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
351 365 end
352 366
353 367 unless count.nil?
354 368 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
355 369 if per_page_links != false && links = per_page_links(paginator.items_per_page)
356 370 html << " | #{links}"
357 371 end
358 372 end
359 373
360 374 html
361 375 end
362 376
363 377 def per_page_links(selected=nil)
364 378 links = Setting.per_page_options_array.collect do |n|
365 379 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
366 380 end
367 381 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
368 382 end
369 383
370 384 def reorder_links(name, url)
371 385 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
372 386 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
373 387 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
374 388 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
375 389 end
376 390
377 391 def breadcrumb(*args)
378 392 elements = args.flatten
379 393 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
380 394 end
381 395
382 396 def other_formats_links(&block)
383 397 concat('<p class="other-formats">' + l(:label_export_to))
384 398 yield Redmine::Views::OtherFormatsBuilder.new(self)
385 399 concat('</p>')
386 400 end
387 401
388 402 def page_header_title
389 403 if @project.nil? || @project.new_record?
390 404 h(Setting.app_title)
391 405 else
392 406 b = []
393 407 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
394 408 if ancestors.any?
395 409 root = ancestors.shift
396 410 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
397 411 if ancestors.size > 2
398 412 b << '&#8230;'
399 413 ancestors = ancestors[-2, 2]
400 414 end
401 415 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
402 416 end
403 417 b << h(@project)
404 418 b.join(' &#187; ')
405 419 end
406 420 end
407 421
408 422 def html_title(*args)
409 423 if args.empty?
410 424 title = []
411 425 title << @project.name if @project
412 426 title += @html_title if @html_title
413 427 title << Setting.app_title
414 428 title.select {|t| !t.blank? }.join(' - ')
415 429 else
416 430 @html_title ||= []
417 431 @html_title += args
418 432 end
419 433 end
420 434
421 435 # Returns the theme, controller name, and action as css classes for the
422 436 # HTML body.
423 437 def body_css_classes
424 438 css = []
425 439 if theme = Redmine::Themes.theme(Setting.ui_theme)
426 440 css << 'theme-' + theme.name
427 441 end
428 442
429 443 css << 'controller-' + params[:controller]
430 444 css << 'action-' + params[:action]
431 445 css.join(' ')
432 446 end
433 447
434 448 def accesskey(s)
435 449 Redmine::AccessKeys.key_for s
436 450 end
437 451
438 452 # Formats text according to system settings.
439 453 # 2 ways to call this method:
440 454 # * with a String: textilizable(text, options)
441 455 # * with an object and one of its attribute: textilizable(issue, :description, options)
442 456 def textilizable(*args)
443 457 options = args.last.is_a?(Hash) ? args.pop : {}
444 458 case args.size
445 459 when 1
446 460 obj = options[:object]
447 461 text = args.shift
448 462 when 2
449 463 obj = args.shift
450 464 attr = args.shift
451 465 text = obj.send(attr).to_s
452 466 else
453 467 raise ArgumentError, 'invalid arguments to textilizable'
454 468 end
455 469 return '' if text.blank?
456 470 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
457 471 only_path = options.delete(:only_path) == false ? false : true
458 472
459 473 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
460 474
461 475 @parsed_headings = []
462 476 text = parse_non_pre_blocks(text) do |text|
463 477 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
464 478 send method_name, text, project, obj, attr, only_path, options
465 479 end
466 480 end
467 481
468 482 if @parsed_headings.any?
469 483 replace_toc(text, @parsed_headings)
470 484 end
471 485
472 486 text
473 487 end
474 488
475 489 def parse_non_pre_blocks(text)
476 490 s = StringScanner.new(text)
477 491 tags = []
478 492 parsed = ''
479 493 while !s.eos?
480 494 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
481 495 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
482 496 if tags.empty?
483 497 yield text
484 498 end
485 499 parsed << text
486 500 if tag
487 501 if closing
488 502 if tags.last == tag.downcase
489 503 tags.pop
490 504 end
491 505 else
492 506 tags << tag.downcase
493 507 end
494 508 parsed << full_tag
495 509 end
496 510 end
497 511 # Close any non closing tags
498 512 while tag = tags.pop
499 513 parsed << "</#{tag}>"
500 514 end
501 515 parsed
502 516 end
503 517
504 518 def parse_inline_attachments(text, project, obj, attr, only_path, options)
505 519 # when using an image link, try to use an attachment, if possible
506 520 if options[:attachments] || (obj && obj.respond_to?(:attachments))
507 521 attachments = nil
508 522 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
509 523 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
510 524 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
511 525 # search for the picture in attachments
512 526 if found = attachments.detect { |att| att.filename.downcase == filename }
513 527 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
514 528 desc = found.description.to_s.gsub('"', '')
515 529 if !desc.blank? && alttext.blank?
516 530 alt = " title=\"#{desc}\" alt=\"#{desc}\""
517 531 end
518 532 "src=\"#{image_url}\"#{alt}"
519 533 else
520 534 m
521 535 end
522 536 end
523 537 end
524 538 end
525 539
526 540 # Wiki links
527 541 #
528 542 # Examples:
529 543 # [[mypage]]
530 544 # [[mypage|mytext]]
531 545 # wiki links can refer other project wikis, using project name or identifier:
532 546 # [[project:]] -> wiki starting page
533 547 # [[project:|mytext]]
534 548 # [[project:mypage]]
535 549 # [[project:mypage|mytext]]
536 550 def parse_wiki_links(text, project, obj, attr, only_path, options)
537 551 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
538 552 link_project = project
539 553 esc, all, page, title = $1, $2, $3, $5
540 554 if esc.nil?
541 555 if page =~ /^([^\:]+)\:(.*)$/
542 556 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
543 557 page = $2
544 558 title ||= $1 if page.blank?
545 559 end
546 560
547 561 if link_project && link_project.wiki
548 562 # extract anchor
549 563 anchor = nil
550 564 if page =~ /^(.+?)\#(.+)$/
551 565 page, anchor = $1, $2
552 566 end
553 567 # check if page exists
554 568 wiki_page = link_project.wiki.find_page(page)
555 569 url = case options[:wiki_links]
556 570 when :local; "#{title}.html"
557 571 when :anchor; "##{title}" # used for single-file wiki export
558 572 else
559 573 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
560 574 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
561 575 end
562 576 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
563 577 else
564 578 # project or wiki doesn't exist
565 579 all
566 580 end
567 581 else
568 582 all
569 583 end
570 584 end
571 585 end
572 586
573 587 # Redmine links
574 588 #
575 589 # Examples:
576 590 # Issues:
577 591 # #52 -> Link to issue #52
578 592 # Changesets:
579 593 # r52 -> Link to revision 52
580 594 # commit:a85130f -> Link to scmid starting with a85130f
581 595 # Documents:
582 596 # document#17 -> Link to document with id 17
583 597 # document:Greetings -> Link to the document with title "Greetings"
584 598 # document:"Some document" -> Link to the document with title "Some document"
585 599 # Versions:
586 600 # version#3 -> Link to version with id 3
587 601 # version:1.0.0 -> Link to version named "1.0.0"
588 602 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
589 603 # Attachments:
590 604 # attachment:file.zip -> Link to the attachment of the current object named file.zip
591 605 # Source files:
592 606 # source:some/file -> Link to the file located at /some/file in the project's repository
593 607 # source:some/file@52 -> Link to the file's revision 52
594 608 # source:some/file#L120 -> Link to line 120 of the file
595 609 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
596 610 # export:some/file -> Force the download of the file
597 611 # Forum messages:
598 612 # message#1218 -> Link to message with id 1218
599 613 #
600 614 # Links can refer other objects from other projects, using project identifier:
601 615 # identifier:r52
602 616 # identifier:document:"Some document"
603 617 # identifier:version:1.0.0
604 618 # identifier:source:some/file
605 619 def parse_redmine_links(text, project, obj, attr, only_path, options)
606 620 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
607 621 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
608 622 link = nil
609 623 if project_identifier
610 624 project = Project.visible.find_by_identifier(project_identifier)
611 625 end
612 626 if esc.nil?
613 627 if prefix.nil? && sep == 'r'
614 628 # project.changesets.visible raises an SQL error because of a double join on repositories
615 629 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
616 630 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
617 631 :class => 'changeset',
618 632 :title => truncate_single_line(changeset.comments, :length => 100))
619 633 end
620 634 elsif sep == '#'
621 635 oid = identifier.to_i
622 636 case prefix
623 637 when nil
624 638 if issue = Issue.visible.find_by_id(oid, :include => :status)
625 639 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
626 640 :class => issue.css_classes,
627 641 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
628 642 end
629 643 when 'document'
630 644 if document = Document.visible.find_by_id(oid)
631 645 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
632 646 :class => 'document'
633 647 end
634 648 when 'version'
635 649 if version = Version.visible.find_by_id(oid)
636 650 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
637 651 :class => 'version'
638 652 end
639 653 when 'message'
640 654 if message = Message.visible.find_by_id(oid, :include => :parent)
641 655 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
642 656 end
643 657 when 'project'
644 658 if p = Project.visible.find_by_id(oid)
645 659 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
646 660 end
647 661 end
648 662 elsif sep == ':'
649 663 # removes the double quotes if any
650 664 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
651 665 case prefix
652 666 when 'document'
653 667 if project && document = project.documents.visible.find_by_title(name)
654 668 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
655 669 :class => 'document'
656 670 end
657 671 when 'version'
658 672 if project && version = project.versions.visible.find_by_name(name)
659 673 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
660 674 :class => 'version'
661 675 end
662 676 when 'commit'
663 677 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
664 678 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
665 679 :class => 'changeset',
666 680 :title => truncate_single_line(changeset.comments, :length => 100)
667 681 end
668 682 when 'source', 'export'
669 683 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
670 684 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
671 685 path, rev, anchor = $1, $3, $5
672 686 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
673 687 :path => to_path_param(path),
674 688 :rev => rev,
675 689 :anchor => anchor,
676 690 :format => (prefix == 'export' ? 'raw' : nil)},
677 691 :class => (prefix == 'export' ? 'source download' : 'source')
678 692 end
679 693 when 'attachment'
680 694 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
681 695 if attachments && attachment = attachments.detect {|a| a.filename == name }
682 696 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
683 697 :class => 'attachment'
684 698 end
685 699 when 'project'
686 700 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
687 701 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
688 702 end
689 703 end
690 704 end
691 705 end
692 706 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
693 707 end
694 708 end
695 709
696 710 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
697 711
698 712 # Headings and TOC
699 713 # Adds ids and links to headings unless options[:headings] is set to false
700 714 def parse_headings(text, project, obj, attr, only_path, options)
701 715 return if options[:headings] == false
702 716
703 717 text.gsub!(HEADING_RE) do
704 718 level, attrs, content = $1.to_i, $2, $3
705 719 item = strip_tags(content).strip
706 720 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
707 721 @parsed_headings << [level, anchor, item]
708 722 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
709 723 end
710 724 end
711 725
712 726 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
713 727
714 728 # Renders the TOC with given headings
715 729 def replace_toc(text, headings)
716 730 text.gsub!(TOC_RE) do
717 731 if headings.empty?
718 732 ''
719 733 else
720 734 div_class = 'toc'
721 735 div_class << ' right' if $1 == '>'
722 736 div_class << ' left' if $1 == '<'
723 737 out = "<ul class=\"#{div_class}\"><li>"
724 738 root = headings.map(&:first).min
725 739 current = root
726 740 started = false
727 741 headings.each do |level, anchor, item|
728 742 if level > current
729 743 out << '<ul><li>' * (level - current)
730 744 elsif level < current
731 745 out << "</li></ul>\n" * (current - level) + "</li><li>"
732 746 elsif started
733 747 out << '</li><li>'
734 748 end
735 749 out << "<a href=\"##{anchor}\">#{item}</a>"
736 750 current = level
737 751 started = true
738 752 end
739 753 out << '</li></ul>' * (current - root)
740 754 out << '</li></ul>'
741 755 end
742 756 end
743 757 end
744 758
745 759 # Same as Rails' simple_format helper without using paragraphs
746 760 def simple_format_without_paragraph(text)
747 761 text.to_s.
748 762 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
749 763 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
750 764 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
751 765 end
752 766
753 767 def lang_options_for_select(blank=true)
754 768 (blank ? [["(auto)", ""]] : []) +
755 769 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
756 770 end
757 771
758 772 def label_tag_for(name, option_tags = nil, options = {})
759 773 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
760 774 content_tag("label", label_text)
761 775 end
762 776
763 777 def labelled_tabular_form_for(name, object, options, &proc)
764 778 options[:html] ||= {}
765 779 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
766 780 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
767 781 end
768 782
769 783 def back_url_hidden_field_tag
770 784 back_url = params[:back_url] || request.env['HTTP_REFERER']
771 785 back_url = CGI.unescape(back_url.to_s)
772 786 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
773 787 end
774 788
775 789 def check_all_links(form_name)
776 790 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
777 791 " | " +
778 792 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
779 793 end
780 794
781 795 def progress_bar(pcts, options={})
782 796 pcts = [pcts, pcts] unless pcts.is_a?(Array)
783 797 pcts = pcts.collect(&:round)
784 798 pcts[1] = pcts[1] - pcts[0]
785 799 pcts << (100 - pcts[1] - pcts[0])
786 800 width = options[:width] || '100px;'
787 801 legend = options[:legend] || ''
788 802 content_tag('table',
789 803 content_tag('tr',
790 804 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
791 805 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
792 806 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
793 807 ), :class => 'progress', :style => "width: #{width};") +
794 808 content_tag('p', legend, :class => 'pourcent')
795 809 end
796 810
797 811 def checked_image(checked=true)
798 812 if checked
799 813 image_tag 'toggle_check.png'
800 814 end
801 815 end
802 816
803 817 def context_menu(url)
804 818 unless @context_menu_included
805 819 content_for :header_tags do
806 820 javascript_include_tag('context_menu') +
807 821 stylesheet_link_tag('context_menu')
808 822 end
809 823 if l(:direction) == 'rtl'
810 824 content_for :header_tags do
811 825 stylesheet_link_tag('context_menu_rtl')
812 826 end
813 827 end
814 828 @context_menu_included = true
815 829 end
816 830 javascript_tag "new ContextMenu('#{ url_for(url) }')"
817 831 end
818 832
819 833 def context_menu_link(name, url, options={})
820 834 options[:class] ||= ''
821 835 if options.delete(:selected)
822 836 options[:class] << ' icon-checked disabled'
823 837 options[:disabled] = true
824 838 end
825 839 if options.delete(:disabled)
826 840 options.delete(:method)
827 841 options.delete(:confirm)
828 842 options.delete(:onclick)
829 843 options[:class] << ' disabled'
830 844 url = '#'
831 845 end
832 846 link_to name, url, options
833 847 end
834 848
835 849 def calendar_for(field_id)
836 850 include_calendar_headers_tags
837 851 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
838 852 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
839 853 end
840 854
841 855 def include_calendar_headers_tags
842 856 unless @calendar_headers_tags_included
843 857 @calendar_headers_tags_included = true
844 858 content_for :header_tags do
845 859 start_of_week = case Setting.start_of_week.to_i
846 860 when 1
847 861 'Calendar._FD = 1;' # Monday
848 862 when 7
849 863 'Calendar._FD = 0;' # Sunday
850 864 when 6
851 865 'Calendar._FD = 6;' # Saturday
852 866 else
853 867 '' # use language
854 868 end
855 869
856 870 javascript_include_tag('calendar/calendar') +
857 871 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
858 872 javascript_tag(start_of_week) +
859 873 javascript_include_tag('calendar/calendar-setup') +
860 874 stylesheet_link_tag('calendar')
861 875 end
862 876 end
863 877 end
864 878
865 879 def content_for(name, content = nil, &block)
866 880 @has_content ||= {}
867 881 @has_content[name] = true
868 882 super(name, content, &block)
869 883 end
870 884
871 885 def has_content?(name)
872 886 (@has_content && @has_content[name]) || false
873 887 end
874 888
875 889 def email_delivery_enabled?
876 890 !!ActionMailer::Base.perform_deliveries
877 891 end
878 892
879 893 # Returns the avatar image tag for the given +user+ if avatars are enabled
880 894 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
881 895 def avatar(user, options = { })
882 896 if Setting.gravatar_enabled?
883 897 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
884 898 email = nil
885 899 if user.respond_to?(:mail)
886 900 email = user.mail
887 901 elsif user.to_s =~ %r{<(.+?)>}
888 902 email = $1
889 903 end
890 904 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
891 905 else
892 906 ''
893 907 end
894 908 end
895 909
896 910 # Returns the javascript tags that are included in the html layout head
897 911 def javascript_heads
898 912 tags = javascript_include_tag(:defaults)
899 913 unless User.current.pref.warn_on_leaving_unsaved == '0'
900 914 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
901 915 end
902 916 tags
903 917 end
904 918
905 919 def favicon
906 920 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
907 921 end
908 922
909 923 def robot_exclusion_tag
910 924 '<meta name="robots" content="noindex,follow,noarchive" />'
911 925 end
912 926
913 927 # Returns true if arg is expected in the API response
914 928 def include_in_api_response?(arg)
915 929 unless @included_in_api_response
916 930 param = params[:include]
917 931 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
918 932 @included_in_api_response.collect!(&:strip)
919 933 end
920 934 @included_in_api_response.include?(arg.to_s)
921 935 end
922 936
923 937 # Returns options or nil if nometa param or X-Redmine-Nometa header
924 938 # was set in the request
925 939 def api_meta(options)
926 940 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
927 941 # compatibility mode for activeresource clients that raise
928 942 # an error when unserializing an array with attributes
929 943 nil
930 944 else
931 945 options
932 946 end
933 947 end
934 948
935 949 private
936 950
937 951 def wiki_helper
938 952 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
939 953 extend helper
940 954 return self
941 955 end
942 956
943 957 def link_to_content_update(text, url_params = {}, html_options = {})
944 958 link_to(text, url_params, html_options)
945 959 end
946 960 end
@@ -1,6 +1,6
1 1 <%= error_messages_for 'category' %>
2 2
3 3 <div class="box">
4 4 <p><%= f.text_field :name, :size => 30, :required => true %></p>
5 <p><%= f.select :assigned_to_id, @project.assignable_users.sort.collect{|u| [u.name, u.id]}, :include_blank => true %></p>
5 <p><%= f.select :assigned_to_id, principals_options_for_select(@project.assignable_users, @category.assigned_to), :include_blank => true %></p>
6 6 </div>
@@ -1,79 +1,79
1 1 <h2><%= @copy ? l(:button_copy) : l(:button_move) %></h2>
2 2
3 3 <ul>
4 4 <% @issues.each do |issue| -%>
5 5 <li><%= link_to_issue issue %></li>
6 6 <% end -%>
7 7 </ul>
8 8
9 9 <% form_tag({:action => 'create'}, :id => 'move_form') do %>
10 10 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
11 11
12 12 <div class="box tabular">
13 13 <fieldset class="attributes">
14 14 <legend><%= l(:label_change_properties) %></legend>
15 15
16 16 <div class="splitcontentleft">
17 17 <p><label for="new_project_id"><%=l(:field_project)%>:</label>
18 18 <%= select_tag "new_project_id",
19 19 project_tree_options_for_select(@allowed_projects, :selected => @target_project),
20 20 :onchange => remote_function(:url => { :action => 'new' },
21 21 :method => :get,
22 22 :update => 'content',
23 23 :with => "Form.serialize('move_form')") %></p>
24 24
25 25 <p><label for="new_tracker_id"><%=l(:field_tracker)%>:</label>
26 26 <%= select_tag "new_tracker_id", "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, "id", "name") %></p>
27 27
28 28 <p>
29 29 <label><%= l(:field_status) %></label>
30 30 <%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %>
31 31 </p>
32 32
33 33 <p>
34 34 <label><%= l(:field_priority) %></label>
35 35 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
36 36 </p>
37 37
38 38 <p>
39 39 <label><%= l(:field_assigned_to) %></label>
40 40 <%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
41 41 content_tag('option', l(:label_nobody), :value => 'none') +
42 options_from_collection_for_select(@target_project.assignable_users, :id, :name)) %>
42 principals_options_for_select(@target_project.assignable_users)) %>
43 43 </p>
44 44 </div>
45 45
46 46 <div class="splitcontentright">
47 47 <p>
48 48 <label><%= l(:field_start_date) %></label>
49 49 <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %>
50 50 </p>
51 51
52 52 <p>
53 53 <label><%= l(:field_due_date) %></label>
54 54 <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %>
55 55 </p>
56 56 </div>
57 57
58 58 </fieldset>
59 59
60 60 <fieldset><legend><%= l(:field_notes) %></legend>
61 61 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
62 62 <%= wikitoolbar_for 'notes' %>
63 63 </fieldset>
64 64
65 65 <%= call_hook(:view_issues_move_bottom, :issues => @issues, :target_project => @target_project, :copy => !!@copy) %>
66 66 </div>
67 67
68 68 <% if @copy %>
69 69 <%= hidden_field_tag("copy_options[copy]", "1") %>
70 70 <%= submit_tag l(:button_copy) %>
71 71 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
72 72 <% else %>
73 73 <%= submit_tag l(:button_move) %>
74 74 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
75 75 <% end %>
76 76 <% end %>
77 77 <% content_for :header_tags do %>
78 78 <%= robot_exclusion_tag %>
79 79 <% end %>
@@ -1,50 +1,50
1 1 <% fields_for :issue, @issue, :builder => TabularFormBuilder do |f| %>
2 2
3 3 <div class="splitcontentleft">
4 4 <% if @issue.new_record? || @allowed_statuses.any? %>
5 5 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
6 6 <% else %>
7 7 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
8 8 <% end %>
9 9
10 10 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %></p>
11 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
11 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
12 12 <% unless @project.issue_categories.empty? %>
13 13 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
14 14 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
15 15 l(:label_issue_category_new),
16 16 'category[name]',
17 17 {:controller => 'issue_categories', :action => 'new', :project_id => @project},
18 18 :title => l(:label_issue_category_new),
19 19 :tabindex => 199) if authorize_for('issue_categories', 'new') %></p>
20 20 <% end %>
21 21 <% unless @issue.assignable_versions.empty? %>
22 22 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %>
23 23 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
24 24 l(:label_version_new),
25 25 'version[name]',
26 26 {:controller => 'versions', :action => 'create', :project_id => @project},
27 27 :title => l(:label_version_new),
28 28 :tabindex => 200) if authorize_for('versions', 'new') %>
29 29 </p>
30 30 <% end %>
31 31 </div>
32 32
33 33 <div class="splitcontentright">
34 34 <% if User.current.allowed_to?(:manage_subtasks, @project) %>
35 35 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10 %></p>
36 36 <div id="parent_issue_candidates" class="autocomplete"></div>
37 37 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @project) }')" %>
38 38 <% end %>
39 39 <p><%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_start_date') if @issue.leaf? %></p>
40 40 <p><%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf? %><%= calendar_for('issue_due_date') if @issue.leaf? %></p>
41 41 <p><%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf? %> <%= l(:field_hours) %></p>
42 42 <% if @issue.leaf? && Issue.use_field_for_done_ratio? %>
43 43 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
44 44 <% end %>
45 45 </div>
46 46
47 47 <div style="clear:both;"> </div>
48 48 <%= render :partial => 'issues/form_custom_fields' %>
49 49
50 50 <% end %>
@@ -1,14 +1,14
1 1 <div class="attributes">
2 2 <div class="splitcontentleft">
3 3 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
4 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
4 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true %></p>
5 5 </div>
6 6 <div class="splitcontentright">
7 7 <% if Issue.use_field_for_done_ratio? %>
8 8 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
9 9 <% end %>
10 10 <% unless @issue.assignable_versions.empty? %>
11 11 <p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p>
12 12 <% end %>
13 13 </div>
14 14 </div>
@@ -1,91 +1,91
1 1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
2 2
3 3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
4 4
5 5 <% form_tag(:action => 'bulk_update') do %>
6 6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
7 7 <div class="box tabular">
8 8 <fieldset class="attributes">
9 9 <legend><%= l(:label_change_properties) %></legend>
10 10
11 11 <div class="splitcontentleft">
12 12 <p>
13 13 <label><%= l(:field_tracker) %></label>
14 14 <%= select_tag('issue[tracker_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@trackers, :id, :name)) %>
15 15 </p>
16 16 <% if @available_statuses.any? %>
17 17 <p>
18 18 <label><%= l(:field_status) %></label>
19 19 <%= select_tag('issue[status_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %>
20 20 </p>
21 21 <% end %>
22 22 <p>
23 23 <label><%= l(:field_priority) %></label>
24 24 <%= select_tag('issue[priority_id]', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(IssuePriority.active, :id, :name)) %>
25 25 </p>
26 26 <p>
27 27 <label><%= l(:field_assigned_to) %></label>
28 28 <%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') +
29 29 content_tag('option', l(:label_nobody), :value => 'none') +
30 options_from_collection_for_select(@assignables, :id, :name)) %>
30 principals_options_for_select(@assignables)) %>
31 31 </p>
32 32 <% if @project %>
33 33 <p>
34 34 <label><%= l(:field_category) %></label>
35 35 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
36 36 content_tag('option', l(:label_none), :value => 'none') +
37 37 options_from_collection_for_select(@project.issue_categories, :id, :name)) %>
38 38 </p>
39 39 <% end %>
40 40 <% #TODO: allow editing versions when multiple projects %>
41 41 <% if @project %>
42 42 <p>
43 43 <label><%= l(:field_fixed_version) %></label>
44 44 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
45 45 content_tag('option', l(:label_none), :value => 'none') +
46 46 version_options_for_select(@project.shared_versions.open.sort)) %>
47 47 </p>
48 48 <% end %>
49 49
50 50 <% @custom_fields.each do |custom_field| %>
51 51 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('issue', custom_field, @projects) %></p>
52 52 <% end %>
53 53
54 54 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
55 55 </div>
56 56
57 57 <div class="splitcontentright">
58 58 <% if @project && User.current.allowed_to?(:manage_subtasks, @project) %>
59 59 <p>
60 60 <label><%= l(:field_parent_issue) %></label>
61 61 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10 %>
62 62 </p>
63 63 <div id="parent_issue_candidates" class="autocomplete"></div>
64 64 <%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:project_id => @project) }')" %>
65 65 <% end %>
66 66 <p>
67 67 <label><%= l(:field_start_date) %></label>
68 68 <%= text_field_tag 'issue[start_date]', '', :size => 10 %><%= calendar_for('issue_start_date') %>
69 69 </p>
70 70 <p>
71 71 <label><%= l(:field_due_date) %></label>
72 72 <%= text_field_tag 'issue[due_date]', '', :size => 10 %><%= calendar_for('issue_due_date') %>
73 73 </p>
74 74 <% if Issue.use_field_for_done_ratio? %>
75 75 <p>
76 76 <label><%= l(:field_done_ratio) %></label>
77 77 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>
78 78 </p>
79 79 <% end %>
80 80 </div>
81 81
82 82 </fieldset>
83 83
84 84 <fieldset><legend><%= l(:field_notes) %></legend>
85 85 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
86 86 <%= wikitoolbar_for 'notes' %>
87 87 </fieldset>
88 88 </div>
89 89
90 90 <p><%= submit_tag l(:button_submit) %></p>
91 91 <% end %>
@@ -1,678 +1,701
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 File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class ApplicationHelperTest < ActionView::TestCase
21 21
22 22 fixtures :projects, :roles, :enabled_modules, :users,
23 23 :repositories, :changesets,
24 24 :trackers, :issue_statuses, :issues, :versions, :documents,
25 25 :wikis, :wiki_pages, :wiki_contents,
26 26 :boards, :messages,
27 27 :attachments,
28 28 :enumerations
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 context "#link_to_if_authorized" do
35 35 context "authorized user" do
36 36 should "be tested"
37 37 end
38 38
39 39 context "unauthorized user" do
40 40 should "be tested"
41 41 end
42 42
43 43 should "allow using the :controller and :action for the target link" do
44 44 User.current = User.find_by_login('admin')
45 45
46 46 @project = Issue.first.project # Used by helper
47 47 response = link_to_if_authorized("By controller/action",
48 48 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
49 49 assert_match /href/, response
50 50 end
51 51
52 52 end
53 53
54 54 def test_auto_links
55 55 to_test = {
56 56 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
57 57 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
58 58 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
59 59 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
60 60 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
61 61 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
62 62 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
63 63 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
64 64 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
65 65 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
66 66 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
67 67 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
68 68 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
69 69 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
70 70 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
71 71 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
72 72 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
73 73 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
74 74 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
75 75 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
76 76 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
77 77 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
78 78 # two exclamation marks
79 79 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
80 80 # escaping
81 81 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
82 82 # wrap in angle brackets
83 83 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
84 84 }
85 85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
86 86 end
87 87
88 88 def test_auto_mailto
89 89 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
90 90 textilizable('test@foo.bar')
91 91 end
92 92
93 93 def test_inline_images
94 94 to_test = {
95 95 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
96 96 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
97 97 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
98 98 # inline styles should be stripped
99 99 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
100 100 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
101 101 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
102 102 }
103 103 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
104 104 end
105 105
106 106 def test_inline_images_inside_tags
107 107 raw = <<-RAW
108 108 h1. !foo.png! Heading
109 109
110 110 Centered image:
111 111
112 112 p=. !bar.gif!
113 113 RAW
114 114
115 115 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
116 116 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
117 117 end
118 118
119 119 def test_attached_images
120 120 to_test = {
121 121 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
122 122 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
123 123 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
124 124 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
125 125 # link image
126 126 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
127 127 }
128 128 attachments = Attachment.find(:all)
129 129 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
130 130 end
131 131
132 132 def test_textile_external_links
133 133 to_test = {
134 134 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
135 135 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
136 136 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
137 137 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
138 138 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
139 139 # no multiline link text
140 140 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
141 141 # mailto link
142 142 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
143 143 # two exclamation marks
144 144 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
145 145 # escaping
146 146 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
147 147 }
148 148 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
149 149 end
150 150
151 151 def test_redmine_links
152 152 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
153 153 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
154 154
155 155 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
156 156 :class => 'changeset', :title => 'My very first commit')
157 157 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
158 158 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
159 159
160 160 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
161 161 :class => 'document')
162 162
163 163 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
164 164 :class => 'version')
165 165
166 166 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
167 167
168 168 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
169 169
170 170 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
171 171 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
172 172
173 173 to_test = {
174 174 # tickets
175 175 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
176 176 # changesets
177 177 'r1' => changeset_link,
178 178 'r1.' => "#{changeset_link}.",
179 179 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
180 180 'r1,r2' => "#{changeset_link},#{changeset_link2}",
181 181 # documents
182 182 'document#1' => document_link,
183 183 'document:"Test document"' => document_link,
184 184 # versions
185 185 'version#2' => version_link,
186 186 'version:1.0' => version_link,
187 187 'version:"1.0"' => version_link,
188 188 # source
189 189 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
190 190 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
191 191 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
192 192 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
193 193 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
194 194 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
195 195 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
196 196 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
197 197 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
198 198 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
199 199 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
200 200 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
201 201 # message
202 202 'message#4' => link_to('Post 2', message_url, :class => 'message'),
203 203 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
204 204 # project
205 205 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
206 206 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
207 207 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
208 208 # escaping
209 209 '!#3.' => '#3.',
210 210 '!r1' => 'r1',
211 211 '!document#1' => 'document#1',
212 212 '!document:"Test document"' => 'document:"Test document"',
213 213 '!version#2' => 'version#2',
214 214 '!version:1.0' => 'version:1.0',
215 215 '!version:"1.0"' => 'version:"1.0"',
216 216 '!source:/some/file' => 'source:/some/file',
217 217 # not found
218 218 '#0123456789' => '#0123456789',
219 219 # invalid expressions
220 220 'source:' => 'source:',
221 221 # url hash
222 222 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
223 223 }
224 224 @project = Project.find(1)
225 225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
226 226 end
227 227
228 228 def test_cross_project_redmine_links
229 229 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
230 230 :class => 'source')
231 231
232 232 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
233 233 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
234 234
235 235 to_test = {
236 236 # documents
237 237 'document:"Test document"' => 'document:"Test document"',
238 238 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
239 239 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
240 240 # versions
241 241 'version:"1.0"' => 'version:"1.0"',
242 242 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
243 243 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
244 244 # changeset
245 245 'r2' => 'r2',
246 246 'ecookbook:r2' => changeset_link,
247 247 'invalid:r2' => 'invalid:r2',
248 248 # source
249 249 'source:/some/file' => 'source:/some/file',
250 250 'ecookbook:source:/some/file' => source_link,
251 251 'invalid:source:/some/file' => 'invalid:source:/some/file',
252 252 }
253 253 @project = Project.find(3)
254 254 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
255 255 end
256 256
257 257 def test_redmine_links_git_commit
258 258 changeset_link = link_to('abcd',
259 259 {
260 260 :controller => 'repositories',
261 261 :action => 'revision',
262 262 :id => 'subproject1',
263 263 :rev => 'abcd',
264 264 },
265 265 :class => 'changeset', :title => 'test commit')
266 266 to_test = {
267 267 'commit:abcd' => changeset_link,
268 268 }
269 269 @project = Project.find(3)
270 270 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
271 271 assert r
272 272 c = Changeset.new(:repository => r,
273 273 :committed_on => Time.now,
274 274 :revision => 'abcd',
275 275 :scmid => 'abcd',
276 276 :comments => 'test commit')
277 277 assert( c.save )
278 278 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
279 279 end
280 280
281 281 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
282 282 def test_redmine_links_darcs_commit
283 283 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
284 284 {
285 285 :controller => 'repositories',
286 286 :action => 'revision',
287 287 :id => 'subproject1',
288 288 :rev => '123',
289 289 },
290 290 :class => 'changeset', :title => 'test commit')
291 291 to_test = {
292 292 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
293 293 }
294 294 @project = Project.find(3)
295 295 r = Repository::Darcs.create!(
296 296 :project => @project, :url => '/tmp/test/darcs',
297 297 :log_encoding => 'UTF-8')
298 298 assert r
299 299 c = Changeset.new(:repository => r,
300 300 :committed_on => Time.now,
301 301 :revision => '123',
302 302 :scmid => '20080308225258-98289-abcd456efg.gz',
303 303 :comments => 'test commit')
304 304 assert( c.save )
305 305 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
306 306 end
307 307
308 308 def test_redmine_links_mercurial_commit
309 309 changeset_link_rev = link_to('r123',
310 310 {
311 311 :controller => 'repositories',
312 312 :action => 'revision',
313 313 :id => 'subproject1',
314 314 :rev => '123' ,
315 315 },
316 316 :class => 'changeset', :title => 'test commit')
317 317 changeset_link_commit = link_to('abcd',
318 318 {
319 319 :controller => 'repositories',
320 320 :action => 'revision',
321 321 :id => 'subproject1',
322 322 :rev => 'abcd' ,
323 323 },
324 324 :class => 'changeset', :title => 'test commit')
325 325 to_test = {
326 326 'r123' => changeset_link_rev,
327 327 'commit:abcd' => changeset_link_commit,
328 328 }
329 329 @project = Project.find(3)
330 330 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
331 331 assert r
332 332 c = Changeset.new(:repository => r,
333 333 :committed_on => Time.now,
334 334 :revision => '123',
335 335 :scmid => 'abcd',
336 336 :comments => 'test commit')
337 337 assert( c.save )
338 338 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
339 339 end
340 340
341 341 def test_attachment_links
342 342 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
343 343 to_test = {
344 344 'attachment:error281.txt' => attachment_link
345 345 }
346 346 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
347 347 end
348 348
349 349 def test_wiki_links
350 350 to_test = {
351 351 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
352 352 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
353 353 # link with anchor
354 354 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
355 355 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
356 356 # page that doesn't exist
357 357 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
358 358 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
359 359 # link to another project wiki
360 360 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
361 361 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
362 362 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
363 363 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
364 364 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
365 365 # striked through link
366 366 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
367 367 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
368 368 # escaping
369 369 '![[Another page|Page]]' => '[[Another page|Page]]',
370 370 # project does not exist
371 371 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
372 372 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
373 373 }
374 374 @project = Project.find(1)
375 375 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
376 376 end
377 377
378 378 def test_html_tags
379 379 to_test = {
380 380 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
381 381 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
382 382 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
383 383 # do not escape pre/code tags
384 384 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
385 385 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
386 386 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
387 387 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
388 388 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
389 389 # remove attributes except class
390 390 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
391 391 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
392 392 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
393 393 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
394 394 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
395 395 # xss
396 396 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
397 397 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
398 398 }
399 399 to_test.each { |text, result| assert_equal result, textilizable(text) }
400 400 end
401 401
402 402 def test_allowed_html_tags
403 403 to_test = {
404 404 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
405 405 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
406 406 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
407 407 }
408 408 to_test.each { |text, result| assert_equal result, textilizable(text) }
409 409 end
410 410
411 411 def test_pre_tags
412 412 raw = <<-RAW
413 413 Before
414 414
415 415 <pre>
416 416 <prepared-statement-cache-size>32</prepared-statement-cache-size>
417 417 </pre>
418 418
419 419 After
420 420 RAW
421 421
422 422 expected = <<-EXPECTED
423 423 <p>Before</p>
424 424 <pre>
425 425 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
426 426 </pre>
427 427 <p>After</p>
428 428 EXPECTED
429 429
430 430 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
431 431 end
432 432
433 433 def test_pre_content_should_not_parse_wiki_and_redmine_links
434 434 raw = <<-RAW
435 435 [[CookBook documentation]]
436 436
437 437 #1
438 438
439 439 <pre>
440 440 [[CookBook documentation]]
441 441
442 442 #1
443 443 </pre>
444 444 RAW
445 445
446 446 expected = <<-EXPECTED
447 447 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
448 448 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
449 449 <pre>
450 450 [[CookBook documentation]]
451 451
452 452 #1
453 453 </pre>
454 454 EXPECTED
455 455
456 456 @project = Project.find(1)
457 457 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
458 458 end
459 459
460 460 def test_non_closing_pre_blocks_should_be_closed
461 461 raw = <<-RAW
462 462 <pre><code>
463 463 RAW
464 464
465 465 expected = <<-EXPECTED
466 466 <pre><code>
467 467 </code></pre>
468 468 EXPECTED
469 469
470 470 @project = Project.find(1)
471 471 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
472 472 end
473 473
474 474 def test_syntax_highlight
475 475 raw = <<-RAW
476 476 <pre><code class="ruby">
477 477 # Some ruby code here
478 478 </code></pre>
479 479 RAW
480 480
481 481 expected = <<-EXPECTED
482 482 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
483 483 </code></pre>
484 484 EXPECTED
485 485
486 486 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
487 487 end
488 488
489 489 def test_wiki_links_in_tables
490 490 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
491 491 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
492 492 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
493 493 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
494 494 }
495 495 @project = Project.find(1)
496 496 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
497 497 end
498 498
499 499 def test_text_formatting
500 500 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
501 501 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
502 502 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
503 503 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
504 504 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
505 505 }
506 506 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
507 507 end
508 508
509 509 def test_wiki_horizontal_rule
510 510 assert_equal '<hr />', textilizable('---')
511 511 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
512 512 end
513 513
514 514 def test_footnotes
515 515 raw = <<-RAW
516 516 This is some text[1].
517 517
518 518 fn1. This is the foot note
519 519 RAW
520 520
521 521 expected = <<-EXPECTED
522 522 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
523 523 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
524 524 EXPECTED
525 525
526 526 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
527 527 end
528 528
529 529 def test_headings
530 530 raw = 'h1. Some heading'
531 531 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
532 532
533 533 assert_equal expected, textilizable(raw)
534 534 end
535 535
536 536 def test_table_of_content
537 537 raw = <<-RAW
538 538 {{toc}}
539 539
540 540 h1. Title
541 541
542 542 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
543 543
544 544 h2. Subtitle with a [[Wiki]] link
545 545
546 546 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
547 547
548 548 h2. Subtitle with [[Wiki|another Wiki]] link
549 549
550 550 h2. Subtitle with %{color:red}red text%
551 551
552 552 <pre>
553 553 some code
554 554 </pre>
555 555
556 556 h3. Subtitle with *some* _modifiers_
557 557
558 558 h1. Another title
559 559
560 560 h3. An "Internet link":http://www.redmine.org/ inside subtitle
561 561
562 562 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
563 563
564 564 RAW
565 565
566 566 expected = '<ul class="toc">' +
567 567 '<li><a href="#Title">Title</a>' +
568 568 '<ul>' +
569 569 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
570 570 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
571 571 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
572 572 '<ul>' +
573 573 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
574 574 '</ul>' +
575 575 '</li>' +
576 576 '</ul>' +
577 577 '</li>' +
578 578 '<li><a href="#Another-title">Another title</a>' +
579 579 '<ul>' +
580 580 '<li>' +
581 581 '<ul>' +
582 582 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
583 583 '</ul>' +
584 584 '</li>' +
585 585 '<li><a href="#Project-Name">Project Name</a></li>' +
586 586 '</ul>' +
587 587 '</li>' +
588 588 '</ul>'
589 589
590 590 @project = Project.find(1)
591 591 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
592 592 end
593 593
594 594 def test_table_of_content_should_contain_included_page_headings
595 595 raw = <<-RAW
596 596 {{toc}}
597 597
598 598 h1. Included
599 599
600 600 {{include(Child_1)}}
601 601 RAW
602 602
603 603 expected = '<ul class="toc">' +
604 604 '<li><a href="#Included">Included</a></li>' +
605 605 '<li><a href="#Child-page-1">Child page 1</a></li>' +
606 606 '</ul>'
607 607
608 608 @project = Project.find(1)
609 609 assert textilizable(raw).gsub("\n", "").include?(expected)
610 610 end
611 611
612 612 def test_default_formatter
613 613 Setting.text_formatting = 'unknown'
614 614 text = 'a *link*: http://www.example.net/'
615 615 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
616 616 Setting.text_formatting = 'textile'
617 617 end
618 618
619 619 def test_due_date_distance_in_words
620 620 to_test = { Date.today => 'Due in 0 days',
621 621 Date.today + 1 => 'Due in 1 day',
622 622 Date.today + 100 => 'Due in about 3 months',
623 623 Date.today + 20000 => 'Due in over 54 years',
624 624 Date.today - 1 => '1 day late',
625 625 Date.today - 100 => 'about 3 months late',
626 626 Date.today - 20000 => 'over 54 years late',
627 627 }
628 628 ::I18n.locale = :en
629 629 to_test.each do |date, expected|
630 630 assert_equal expected, due_date_distance_in_words(date)
631 631 end
632 632 end
633 633
634 634 def test_avatar
635 635 # turn on avatars
636 636 Setting.gravatar_enabled = '1'
637 637 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
638 638 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
639 639 assert_nil avatar('jsmith')
640 640 assert_nil avatar(nil)
641 641
642 642 # turn off avatars
643 643 Setting.gravatar_enabled = '0'
644 644 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
645 645 end
646 646
647 647 def test_link_to_user
648 648 user = User.find(2)
649 649 t = link_to_user(user)
650 650 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
651 651 end
652 652
653 653 def test_link_to_user_should_not_link_to_locked_user
654 654 user = User.find(5)
655 655 assert user.locked?
656 656 t = link_to_user(user)
657 657 assert_equal user.name, t
658 658 end
659 659
660 660 def test_link_to_user_should_not_link_to_anonymous
661 661 user = User.anonymous
662 662 assert user.anonymous?
663 663 t = link_to_user(user)
664 664 assert_equal ::I18n.t(:label_user_anonymous), t
665 665 end
666 666
667 667 def test_link_to_project
668 668 project = Project.find(1)
669 669 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
670 670 link_to_project(project)
671 671 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
672 672 link_to_project(project, :action => 'settings')
673 673 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
674 674 link_to_project(project, {:only_path => false, :jump => 'blah'})
675 675 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
676 676 link_to_project(project, {:action => 'settings'}, :class => "project")
677 677 end
678
679 def test_principals_options_for_select_with_users
680 users = [User.find(2), User.find(4)]
681 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
682 principals_options_for_select(users)
683 end
684
685 def test_principals_options_for_select_with_selected
686 users = [User.find(2), User.find(4)]
687 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
688 principals_options_for_select(users, User.find(4))
689 end
690
691 def test_principals_options_for_select_with_users_and_groups
692 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
693 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
694 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
695 principals_options_for_select(users)
696 end
697
698 def test_principals_options_for_select_with_empty_collection
699 assert_equal '', principals_options_for_select([])
700 end
678 701 end
General Comments 0
You need to be logged in to leave comments. Login now