##// END OF EJS Templates
Merged r3924 from trunk....
Eric Davis -
r3860:c7e719fc4b44
parent child
Show More
@@ -1,818 +1,833
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
36 36 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
37 37 end
38 38
39 39 # Display a link to remote if user is authorized
40 40 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
41 41 url = options[:url] || {}
42 42 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
43 43 end
44 44
45 45 # Displays a link to user's account page if active
46 46 def link_to_user(user, options={})
47 47 if user.is_a?(User)
48 48 name = h(user.name(options[:format]))
49 49 if user.active?
50 50 link_to name, :controller => 'users', :action => 'show', :id => user
51 51 else
52 52 name
53 53 end
54 54 else
55 55 h(user.to_s)
56 56 end
57 57 end
58 58
59 59 # Displays a link to +issue+ with its subject.
60 60 # Examples:
61 61 #
62 62 # link_to_issue(issue) # => Defect #6: This is the subject
63 63 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
64 64 # link_to_issue(issue, :subject => false) # => Defect #6
65 65 # link_to_issue(issue, :project => true) # => Foo - Defect #6
66 66 #
67 67 def link_to_issue(issue, options={})
68 68 title = nil
69 69 subject = nil
70 70 if options[:subject] == false
71 71 title = truncate(issue.subject, :length => 60)
72 72 else
73 73 subject = issue.subject
74 74 if options[:truncate]
75 75 subject = truncate(subject, :length => options[:truncate])
76 76 end
77 77 end
78 78 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
79 79 :class => issue.css_classes,
80 80 :title => title
81 81 s << ": #{h subject}" if subject
82 82 s = "#{h issue.project} - " + s if options[:project]
83 83 s
84 84 end
85 85
86 86 # Generates a link to an attachment.
87 87 # Options:
88 88 # * :text - Link text (default to attachment filename)
89 89 # * :download - Force download (default: false)
90 90 def link_to_attachment(attachment, options={})
91 91 text = options.delete(:text) || attachment.filename
92 92 action = options.delete(:download) ? 'download' : 'show'
93 93
94 94 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
95 95 end
96 96
97 97 # Generates a link to a SCM revision
98 98 # Options:
99 99 # * :text - Link text (default to the formatted revision)
100 100 def link_to_revision(revision, project, options={})
101 101 text = options.delete(:text) || format_revision(revision)
102 102
103 103 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
104 104 end
105 105
106 # Generates a link to a project if active
107 # Examples:
108 #
109 # link_to_project(project) # => link to the specified project overview
110 # link_to_project(project, :action=>'settings') # => link to project settings
111 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
112 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
113 #
114 def link_to_project(project, options={}, html_options = nil)
115 if project.active?
116 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
117 link_to(h(project), url, html_options)
118 else
119 h(project)
120 end
121 end
122
106 123 def toggle_link(name, id, options={})
107 124 onclick = "Element.toggle('#{id}'); "
108 125 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
109 126 onclick << "return false;"
110 127 link_to(name, "#", :onclick => onclick)
111 128 end
112 129
113 130 def image_to_function(name, function, html_options = {})
114 131 html_options.symbolize_keys!
115 132 tag(:input, html_options.merge({
116 133 :type => "image", :src => image_path(name),
117 134 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
118 135 }))
119 136 end
120 137
121 138 def prompt_to_remote(name, text, param, url, html_options = {})
122 139 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
123 140 link_to name, {}, html_options
124 141 end
125 142
126 143 def format_activity_title(text)
127 144 h(truncate_single_line(text, :length => 100))
128 145 end
129 146
130 147 def format_activity_day(date)
131 148 date == Date.today ? l(:label_today).titleize : format_date(date)
132 149 end
133 150
134 151 def format_activity_description(text)
135 152 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
136 153 end
137 154
138 155 def format_version_name(version)
139 156 if version.project == @project
140 157 h(version)
141 158 else
142 159 h("#{version.project} - #{version}")
143 160 end
144 161 end
145 162
146 163 def due_date_distance_in_words(date)
147 164 if date
148 165 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
149 166 end
150 167 end
151 168
152 169 def render_page_hierarchy(pages, node=nil)
153 170 content = ''
154 171 if pages[node]
155 172 content << "<ul class=\"pages-hierarchy\">\n"
156 173 pages[node].each do |page|
157 174 content << "<li>"
158 175 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
159 176 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
160 177 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
161 178 content << "</li>\n"
162 179 end
163 180 content << "</ul>\n"
164 181 end
165 182 content
166 183 end
167 184
168 185 # Renders flash messages
169 186 def render_flash_messages
170 187 s = ''
171 188 flash.each do |k,v|
172 189 s << content_tag('div', v, :class => "flash #{k}")
173 190 end
174 191 s
175 192 end
176 193
177 194 # Renders tabs and their content
178 195 def render_tabs(tabs)
179 196 if tabs.any?
180 197 render :partial => 'common/tabs', :locals => {:tabs => tabs}
181 198 else
182 199 content_tag 'p', l(:label_no_data), :class => "nodata"
183 200 end
184 201 end
185 202
186 203 # Renders the project quick-jump box
187 204 def render_project_jump_box
188 205 # Retrieve them now to avoid a COUNT query
189 206 projects = User.current.projects.all
190 207 if projects.any?
191 208 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
192 209 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
193 210 '<option value="" disabled="disabled">---</option>'
194 211 s << project_tree_options_for_select(projects, :selected => @project) do |p|
195 212 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
196 213 end
197 214 s << '</select>'
198 215 s
199 216 end
200 217 end
201 218
202 219 def project_tree_options_for_select(projects, options = {})
203 220 s = ''
204 221 project_tree(projects) do |project, level|
205 222 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
206 223 tag_options = {:value => project.id}
207 224 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
208 225 tag_options[:selected] = 'selected'
209 226 else
210 227 tag_options[:selected] = nil
211 228 end
212 229 tag_options.merge!(yield(project)) if block_given?
213 230 s << content_tag('option', name_prefix + h(project), tag_options)
214 231 end
215 232 s
216 233 end
217 234
218 235 # Yields the given block for each project with its level in the tree
219 236 def project_tree(projects, &block)
220 237 ancestors = []
221 238 projects.sort_by(&:lft).each do |project|
222 239 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
223 240 ancestors.pop
224 241 end
225 242 yield project, ancestors.size
226 243 ancestors << project
227 244 end
228 245 end
229 246
230 247 def project_nested_ul(projects, &block)
231 248 s = ''
232 249 if projects.any?
233 250 ancestors = []
234 251 projects.sort_by(&:lft).each do |project|
235 252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
236 253 s << "<ul>\n"
237 254 else
238 255 ancestors.pop
239 256 s << "</li>"
240 257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
241 258 ancestors.pop
242 259 s << "</ul></li>\n"
243 260 end
244 261 end
245 262 s << "<li>"
246 263 s << yield(project).to_s
247 264 ancestors << project
248 265 end
249 266 s << ("</li></ul>\n" * ancestors.size)
250 267 end
251 268 s
252 269 end
253 270
254 271 def principals_check_box_tags(name, principals)
255 272 s = ''
256 273 principals.sort.each do |principal|
257 274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
258 275 end
259 276 s
260 277 end
261 278
262 279 # Truncates and returns the string as a single line
263 280 def truncate_single_line(string, *args)
264 281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
265 282 end
266 283
267 284 # Truncates at line break after 250 characters or options[:length]
268 285 def truncate_lines(string, options={})
269 286 length = options[:length] || 250
270 287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
271 288 "#{$1}..."
272 289 else
273 290 string
274 291 end
275 292 end
276 293
277 294 def html_hours(text)
278 295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
279 296 end
280 297
281 298 def authoring(created, author, options={})
282 299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
283 300 end
284 301
285 302 def time_tag(time)
286 303 text = distance_of_time_in_words(Time.now, time)
287 304 if @project
288 305 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
289 306 else
290 307 content_tag('acronym', text, :title => format_time(time))
291 308 end
292 309 end
293 310
294 311 def syntax_highlight(name, content)
295 312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
296 313 end
297 314
298 315 def to_path_param(path)
299 316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
300 317 end
301 318
302 319 def pagination_links_full(paginator, count=nil, options={})
303 320 page_param = options.delete(:page_param) || :page
304 321 per_page_links = options.delete(:per_page_links)
305 322 url_param = params.dup
306 323 # don't reuse query params if filters are present
307 324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
308 325
309 326 html = ''
310 327 if paginator.current.previous
311 328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
312 329 end
313 330
314 331 html << (pagination_links_each(paginator, options) do |n|
315 332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
316 333 end || '')
317 334
318 335 if paginator.current.next
319 336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
320 337 end
321 338
322 339 unless count.nil?
323 340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
324 341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
325 342 html << " | #{links}"
326 343 end
327 344 end
328 345
329 346 html
330 347 end
331 348
332 349 def per_page_links(selected=nil)
333 350 url_param = params.dup
334 351 url_param.clear if url_param.has_key?(:set_filter)
335 352
336 353 links = Setting.per_page_options_array.collect do |n|
337 354 n == selected ? n : link_to_remote(n, {:update => "content",
338 355 :url => params.dup.merge(:per_page => n),
339 356 :method => :get},
340 357 {:href => url_for(url_param.merge(:per_page => n))})
341 358 end
342 359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
343 360 end
344 361
345 362 def reorder_links(name, url)
346 363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
347 364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
348 365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
349 366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
350 367 end
351 368
352 369 def breadcrumb(*args)
353 370 elements = args.flatten
354 371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
355 372 end
356 373
357 374 def other_formats_links(&block)
358 375 concat('<p class="other-formats">' + l(:label_export_to))
359 376 yield Redmine::Views::OtherFormatsBuilder.new(self)
360 377 concat('</p>')
361 378 end
362 379
363 380 def page_header_title
364 381 if @project.nil? || @project.new_record?
365 382 h(Setting.app_title)
366 383 else
367 384 b = []
368 385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
369 386 if ancestors.any?
370 387 root = ancestors.shift
371 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
372 389 if ancestors.size > 2
373 390 b << '&#8230;'
374 391 ancestors = ancestors[-2, 2]
375 392 end
376 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
377 394 end
378 395 b << h(@project)
379 396 b.join(' &#187; ')
380 397 end
381 398 end
382 399
383 400 def html_title(*args)
384 401 if args.empty?
385 402 title = []
386 403 title << @project.name if @project
387 404 title += @html_title if @html_title
388 405 title << Setting.app_title
389 406 title.select {|t| !t.blank? }.join(' - ')
390 407 else
391 408 @html_title ||= []
392 409 @html_title += args
393 410 end
394 411 end
395 412
396 413 # Returns the theme, controller name, and action as css classes for the
397 414 # HTML body.
398 415 def body_css_classes
399 416 css = []
400 417 if theme = Redmine::Themes.theme(Setting.ui_theme)
401 418 css << 'theme-' + theme.name
402 419 end
403 420
404 421 css << 'controller-' + params[:controller]
405 422 css << 'action-' + params[:action]
406 423 css.join(' ')
407 424 end
408 425
409 426 def accesskey(s)
410 427 Redmine::AccessKeys.key_for s
411 428 end
412 429
413 430 # Formats text according to system settings.
414 431 # 2 ways to call this method:
415 432 # * with a String: textilizable(text, options)
416 433 # * with an object and one of its attribute: textilizable(issue, :description, options)
417 434 def textilizable(*args)
418 435 options = args.last.is_a?(Hash) ? args.pop : {}
419 436 case args.size
420 437 when 1
421 438 obj = options[:object]
422 439 text = args.shift
423 440 when 2
424 441 obj = args.shift
425 442 attr = args.shift
426 443 text = obj.send(attr).to_s
427 444 else
428 445 raise ArgumentError, 'invalid arguments to textilizable'
429 446 end
430 447 return '' if text.blank?
431 448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
432 449 only_path = options.delete(:only_path) == false ? false : true
433 450
434 451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
435 452
436 453 parse_non_pre_blocks(text) do |text|
437 454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
438 455 send method_name, text, project, obj, attr, only_path, options
439 456 end
440 457 end
441 458 end
442 459
443 460 def parse_non_pre_blocks(text)
444 461 s = StringScanner.new(text)
445 462 tags = []
446 463 parsed = ''
447 464 while !s.eos?
448 465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
449 466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
450 467 if tags.empty?
451 468 yield text
452 469 end
453 470 parsed << text
454 471 if tag
455 472 if closing
456 473 if tags.last == tag.downcase
457 474 tags.pop
458 475 end
459 476 else
460 477 tags << tag.downcase
461 478 end
462 479 parsed << full_tag
463 480 end
464 481 end
465 482 # Close any non closing tags
466 483 while tag = tags.pop
467 484 parsed << "</#{tag}>"
468 485 end
469 486 parsed
470 487 end
471 488
472 489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
473 490 # when using an image link, try to use an attachment, if possible
474 491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
475 492 attachments = nil
476 493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
477 494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
478 495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
479 496 # search for the picture in attachments
480 497 if found = attachments.detect { |att| att.filename.downcase == filename }
481 498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
482 499 desc = found.description.to_s.gsub('"', '')
483 500 if !desc.blank? && alttext.blank?
484 501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
485 502 end
486 503 "src=\"#{image_url}\"#{alt}"
487 504 else
488 505 m
489 506 end
490 507 end
491 508 end
492 509 end
493 510
494 511 # Wiki links
495 512 #
496 513 # Examples:
497 514 # [[mypage]]
498 515 # [[mypage|mytext]]
499 516 # wiki links can refer other project wikis, using project name or identifier:
500 517 # [[project:]] -> wiki starting page
501 518 # [[project:|mytext]]
502 519 # [[project:mypage]]
503 520 # [[project:mypage|mytext]]
504 521 def parse_wiki_links(text, project, obj, attr, only_path, options)
505 522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
506 523 link_project = project
507 524 esc, all, page, title = $1, $2, $3, $5
508 525 if esc.nil?
509 526 if page =~ /^([^\:]+)\:(.*)$/
510 527 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
511 528 page = $2
512 529 title ||= $1 if page.blank?
513 530 end
514 531
515 532 if link_project && link_project.wiki
516 533 # extract anchor
517 534 anchor = nil
518 535 if page =~ /^(.+?)\#(.+)$/
519 536 page, anchor = $1, $2
520 537 end
521 538 # check if page exists
522 539 wiki_page = link_project.wiki.find_page(page)
523 540 url = case options[:wiki_links]
524 541 when :local; "#{title}.html"
525 542 when :anchor; "##{title}" # used for single-file wiki export
526 543 else
527 544 url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => link_project, :page => Wiki.titleize(page), :anchor => anchor)
528 545 end
529 546 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
530 547 else
531 548 # project or wiki doesn't exist
532 549 all
533 550 end
534 551 else
535 552 all
536 553 end
537 554 end
538 555 end
539 556
540 557 # Redmine links
541 558 #
542 559 # Examples:
543 560 # Issues:
544 561 # #52 -> Link to issue #52
545 562 # Changesets:
546 563 # r52 -> Link to revision 52
547 564 # commit:a85130f -> Link to scmid starting with a85130f
548 565 # Documents:
549 566 # document#17 -> Link to document with id 17
550 567 # document:Greetings -> Link to the document with title "Greetings"
551 568 # document:"Some document" -> Link to the document with title "Some document"
552 569 # Versions:
553 570 # version#3 -> Link to version with id 3
554 571 # version:1.0.0 -> Link to version named "1.0.0"
555 572 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
556 573 # Attachments:
557 574 # attachment:file.zip -> Link to the attachment of the current object named file.zip
558 575 # Source files:
559 576 # source:some/file -> Link to the file located at /some/file in the project's repository
560 577 # source:some/file@52 -> Link to the file's revision 52
561 578 # source:some/file#L120 -> Link to line 120 of the file
562 579 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
563 580 # export:some/file -> Force the download of the file
564 581 # Forum messages:
565 582 # message#1218 -> Link to message with id 1218
566 583 def parse_redmine_links(text, project, obj, attr, only_path, options)
567 584 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
568 585 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
569 586 link = nil
570 587 if esc.nil?
571 588 if prefix.nil? && sep == 'r'
572 589 if project && (changeset = project.changesets.find_by_revision(identifier))
573 590 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
574 591 :class => 'changeset',
575 592 :title => truncate_single_line(changeset.comments, :length => 100))
576 593 end
577 594 elsif sep == '#'
578 595 oid = identifier.to_i
579 596 case prefix
580 597 when nil
581 598 if issue = Issue.visible.find_by_id(oid, :include => :status)
582 599 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
583 600 :class => issue.css_classes,
584 601 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
585 602 end
586 603 when 'document'
587 604 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
588 605 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
589 606 :class => 'document'
590 607 end
591 608 when 'version'
592 609 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
593 610 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
594 611 :class => 'version'
595 612 end
596 613 when 'message'
597 614 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
598 615 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
599 616 :controller => 'messages',
600 617 :action => 'show',
601 618 :board_id => message.board,
602 619 :id => message.root,
603 620 :anchor => (message.parent ? "message-#{message.id}" : nil)},
604 621 :class => 'message'
605 622 end
606 623 when 'project'
607 624 if p = Project.visible.find_by_id(oid)
608 link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
609 :class => 'project'
625 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
610 626 end
611 627 end
612 628 elsif sep == ':'
613 629 # removes the double quotes if any
614 630 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
615 631 case prefix
616 632 when 'document'
617 633 if project && document = project.documents.find_by_title(name)
618 634 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
619 635 :class => 'document'
620 636 end
621 637 when 'version'
622 638 if project && version = project.versions.find_by_name(name)
623 639 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
624 640 :class => 'version'
625 641 end
626 642 when 'commit'
627 643 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
628 644 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
629 645 :class => 'changeset',
630 646 :title => truncate_single_line(changeset.comments, :length => 100)
631 647 end
632 648 when 'source', 'export'
633 649 if project && project.repository
634 650 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
635 651 path, rev, anchor = $1, $3, $5
636 652 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
637 653 :path => to_path_param(path),
638 654 :rev => rev,
639 655 :anchor => anchor,
640 656 :format => (prefix == 'export' ? 'raw' : nil)},
641 657 :class => (prefix == 'export' ? 'source download' : 'source')
642 658 end
643 659 when 'attachment'
644 660 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
645 661 if attachments && attachment = attachments.detect {|a| a.filename == name }
646 662 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
647 663 :class => 'attachment'
648 664 end
649 665 when 'project'
650 666 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
651 link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p},
652 :class => 'project'
667 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
653 668 end
654 669 end
655 670 end
656 671 end
657 672 leading + (link || "#{prefix}#{sep}#{identifier}")
658 673 end
659 674 end
660 675
661 676 # Same as Rails' simple_format helper without using paragraphs
662 677 def simple_format_without_paragraph(text)
663 678 text.to_s.
664 679 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
665 680 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
666 681 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
667 682 end
668 683
669 684 def lang_options_for_select(blank=true)
670 685 (blank ? [["(auto)", ""]] : []) +
671 686 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
672 687 end
673 688
674 689 def label_tag_for(name, option_tags = nil, options = {})
675 690 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
676 691 content_tag("label", label_text)
677 692 end
678 693
679 694 def labelled_tabular_form_for(name, object, options, &proc)
680 695 options[:html] ||= {}
681 696 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
682 697 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
683 698 end
684 699
685 700 def back_url_hidden_field_tag
686 701 back_url = params[:back_url] || request.env['HTTP_REFERER']
687 702 back_url = CGI.unescape(back_url.to_s)
688 703 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
689 704 end
690 705
691 706 def check_all_links(form_name)
692 707 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
693 708 " | " +
694 709 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
695 710 end
696 711
697 712 def progress_bar(pcts, options={})
698 713 pcts = [pcts, pcts] unless pcts.is_a?(Array)
699 714 pcts = pcts.collect(&:round)
700 715 pcts[1] = pcts[1] - pcts[0]
701 716 pcts << (100 - pcts[1] - pcts[0])
702 717 width = options[:width] || '100px;'
703 718 legend = options[:legend] || ''
704 719 content_tag('table',
705 720 content_tag('tr',
706 721 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
707 722 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
708 723 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
709 724 ), :class => 'progress', :style => "width: #{width};") +
710 725 content_tag('p', legend, :class => 'pourcent')
711 726 end
712 727
713 728 def checked_image(checked=true)
714 729 if checked
715 730 image_tag 'toggle_check.png'
716 731 end
717 732 end
718 733
719 734 def context_menu(url)
720 735 unless @context_menu_included
721 736 content_for :header_tags do
722 737 javascript_include_tag('context_menu') +
723 738 stylesheet_link_tag('context_menu')
724 739 end
725 740 @context_menu_included = true
726 741 end
727 742 javascript_tag "new ContextMenu('#{ url_for(url) }')"
728 743 end
729 744
730 745 def context_menu_link(name, url, options={})
731 746 options[:class] ||= ''
732 747 if options.delete(:selected)
733 748 options[:class] << ' icon-checked disabled'
734 749 options[:disabled] = true
735 750 end
736 751 if options.delete(:disabled)
737 752 options.delete(:method)
738 753 options.delete(:confirm)
739 754 options.delete(:onclick)
740 755 options[:class] << ' disabled'
741 756 url = '#'
742 757 end
743 758 link_to name, url, options
744 759 end
745 760
746 761 def calendar_for(field_id)
747 762 include_calendar_headers_tags
748 763 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
749 764 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
750 765 end
751 766
752 767 def include_calendar_headers_tags
753 768 unless @calendar_headers_tags_included
754 769 @calendar_headers_tags_included = true
755 770 content_for :header_tags do
756 771 start_of_week = case Setting.start_of_week.to_i
757 772 when 1
758 773 'Calendar._FD = 1;' # Monday
759 774 when 7
760 775 'Calendar._FD = 0;' # Sunday
761 776 else
762 777 '' # use language
763 778 end
764 779
765 780 javascript_include_tag('calendar/calendar') +
766 781 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
767 782 javascript_tag(start_of_week) +
768 783 javascript_include_tag('calendar/calendar-setup') +
769 784 stylesheet_link_tag('calendar')
770 785 end
771 786 end
772 787 end
773 788
774 789 def content_for(name, content = nil, &block)
775 790 @has_content ||= {}
776 791 @has_content[name] = true
777 792 super(name, content, &block)
778 793 end
779 794
780 795 def has_content?(name)
781 796 (@has_content && @has_content[name]) || false
782 797 end
783 798
784 799 # Returns the avatar image tag for the given +user+ if avatars are enabled
785 800 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
786 801 def avatar(user, options = { })
787 802 if Setting.gravatar_enabled?
788 803 options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
789 804 email = nil
790 805 if user.respond_to?(:mail)
791 806 email = user.mail
792 807 elsif user.to_s =~ %r{<(.+?)>}
793 808 email = $1
794 809 end
795 810 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
796 811 end
797 812 end
798 813
799 814 def favicon
800 815 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
801 816 end
802 817
803 818 private
804 819
805 820 def wiki_helper
806 821 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
807 822 extend helper
808 823 return self
809 824 end
810 825
811 826 def link_to_remote_content_update(text, url_params)
812 827 link_to_remote(text,
813 828 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
814 829 {:href => url_for(:params => url_params)}
815 830 )
816 831 end
817 832
818 833 end
@@ -1,108 +1,108
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 module ProjectsHelper
19 19 def link_to_version(version, options = {})
20 20 return '' unless version && version.is_a?(Version)
21 21 link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
22 22 end
23 23
24 24 def project_settings_tabs
25 25 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
26 26 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
27 27 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
28 28 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
29 29 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
30 30 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
31 31 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
32 32 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
33 33 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
34 34 ]
35 35 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
36 36 end
37 37
38 38 def parent_project_select_tag(project)
39 39 selected = project.parent
40 40 # retrieve the requested parent project
41 41 parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
42 42 if parent_id
43 43 selected = (parent_id.blank? ? nil : Project.find(parent_id))
44 44 end
45 45
46 46 options = ''
47 47 options << "<option value=''></option>" if project.allowed_parents.include?(nil)
48 48 options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
49 49 content_tag('select', options, :name => 'project[parent_id]', :id => 'project_parent_id')
50 50 end
51 51
52 52 # Renders a tree of projects as a nested set of unordered lists
53 53 # The given collection may be a subset of the whole project tree
54 54 # (eg. some intermediate nodes are private and can not be seen)
55 55 def render_project_hierarchy(projects)
56 56 s = ''
57 57 if projects.any?
58 58 ancestors = []
59 59 original_project = @project
60 60 projects.each do |project|
61 61 # set the project environment to please macros.
62 62 @project = project
63 63 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
64 64 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
65 65 else
66 66 ancestors.pop
67 67 s << "</li>"
68 68 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
69 69 ancestors.pop
70 70 s << "</ul></li>\n"
71 71 end
72 72 end
73 73 classes = (ancestors.empty? ? 'root' : 'child')
74 74 s << "<li class='#{classes}'><div class='#{classes}'>" +
75 link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
75 link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
76 76 s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
77 77 s << "</div>\n"
78 78 ancestors << project
79 79 end
80 80 s << ("</li></ul>\n" * ancestors.size)
81 81 @project = original_project
82 82 end
83 83 s
84 84 end
85 85
86 86 # Returns a set of options for a select field, grouped by project.
87 87 def version_options_for_select(versions, selected=nil)
88 88 grouped = Hash.new {|h,k| h[k] = []}
89 89 versions.each do |version|
90 90 grouped[version.project.name] << [version.name, version.id]
91 91 end
92 92 # Add in the selected
93 93 if selected && !versions.include?(selected)
94 94 grouped[selected.project.name] << [selected.name, selected.id]
95 95 end
96 96
97 97 if grouped.keys.size > 1
98 98 grouped_options_for_select(grouped, selected && selected.id)
99 99 else
100 100 options_for_select((grouped.values.first || []), selected && selected.id)
101 101 end
102 102 end
103 103
104 104 def format_version_sharing(sharing)
105 105 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
106 106 l("label_version_sharing_#{sharing}")
107 107 end
108 108 end
@@ -1,100 +1,100
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 module QueriesHelper
19 19
20 20 def operators_for_select(filter_type)
21 21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
22 22 end
23 23
24 24 def column_header(column)
25 25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
26 26 :default_order => column.default_order) :
27 27 content_tag('th', column.caption)
28 28 end
29 29
30 30 def column_content(column, issue)
31 31 value = column.value(issue)
32 32
33 33 case value.class.name
34 34 when 'String'
35 35 if column.name == :subject
36 36 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
37 37 else
38 38 h(value)
39 39 end
40 40 when 'Time'
41 41 format_time(value)
42 42 when 'Date'
43 43 format_date(value)
44 44 when 'Fixnum', 'Float'
45 45 if column.name == :done_ratio
46 46 progress_bar(value, :width => '80px')
47 47 else
48 48 value.to_s
49 49 end
50 50 when 'User'
51 51 link_to_user value
52 52 when 'Project'
53 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
53 link_to_project value
54 54 when 'Version'
55 55 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
56 56 when 'TrueClass'
57 57 l(:general_text_Yes)
58 58 when 'FalseClass'
59 59 l(:general_text_No)
60 60 when 'Issue'
61 61 link_to_issue(value, :subject => false)
62 62 else
63 63 h(value)
64 64 end
65 65 end
66 66
67 67 # Retrieve query from session or build a new query
68 68 def retrieve_query
69 69 if !params[:query_id].blank?
70 70 cond = "project_id IS NULL"
71 71 cond << " OR project_id = #{@project.id}" if @project
72 72 @query = Query.find(params[:query_id], :conditions => cond)
73 73 @query.project = @project
74 74 session[:query] = {:id => @query.id, :project_id => @query.project_id}
75 75 sort_clear
76 76 else
77 77 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
78 78 # Give it a name, required to be valid
79 79 @query = Query.new(:name => "_")
80 80 @query.project = @project
81 81 if params[:fields] and params[:fields].is_a? Array
82 82 params[:fields].each do |field|
83 83 @query.add_filter(field, params[:operators][field], params[:values][field])
84 84 end
85 85 else
86 86 @query.available_filters.keys.each do |field|
87 87 @query.add_short_filter(field, params[field]) if params[field]
88 88 end
89 89 end
90 90 @query.group_by = params[:group_by]
91 91 @query.column_names = params[:query] && params[:query][:column_names]
92 92 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
93 93 else
94 94 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
95 95 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
96 96 @query.project = @project
97 97 end
98 98 end
99 99 end
100 100 end
@@ -1,46 +1,46
1 1 <div class="contextual">
2 2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_project_plural)%></h2>
6 6
7 7 <% form_tag({}, :method => :get) do %>
8 8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 9 <label><%= l(:field_status) %> :</label>
10 10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 11 <label><%= l(:label_project) %>:</label>
12 12 <%= text_field_tag 'name', params[:name], :size => 30 %>
13 13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
14 14 </fieldset>
15 15 <% end %>
16 16 &nbsp;
17 17
18 18 <div class="autoscroll">
19 19 <table class="list">
20 20 <thead><tr>
21 21 <th><%=l(:label_project)%></th>
22 22 <th><%=l(:field_description)%></th>
23 23 <th><%=l(:field_is_public)%></th>
24 24 <th><%=l(:field_created_on)%></th>
25 25 <th></th>
26 26 </tr></thead>
27 27 <tbody>
28 28 <% project_tree(@projects) do |project, level| %>
29 29 <tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
30 <td class="name"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
30 <td class="name"><%= link_to_project(project, :action => 'settings') %></td>
31 31 <td><%= textilizable project.short_description, :project => project %></td>
32 32 <td align="center"><%= checked_image project.is_public? %></td>
33 33 <td align="center"><%= format_date(project.created_on) %></td>
34 34 <td class="buttons">
35 35 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
36 36 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
37 37 <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
38 38 <%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %>
39 39 </td>
40 40 </tr>
41 41 <% end %>
42 42 </tbody>
43 43 </table>
44 44 </div>
45 45
46 46 <% html_title(l(:label_project_plural)) -%>
@@ -1,29 +1,29
1 1 <% if issues && issues.any? %>
2 2 <% form_tag({}) do %>
3 3 <table class="list issues">
4 4 <thead><tr>
5 5 <th>#</th>
6 6 <th><%=l(:field_project)%></th>
7 7 <th><%=l(:field_tracker)%></th>
8 8 <th><%=l(:field_subject)%></th>
9 9 </tr></thead>
10 10 <tbody>
11 11 <% for issue in issues %>
12 12 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
13 13 <td class="id">
14 14 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %>
15 15 <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
16 16 </td>
17 <td class="project"><%= link_to(h(issue.project), :controller => 'projects', :action => 'show', :id => issue.project) %></td>
17 <td class="project"><%= link_to_project(issue.project) %></td>
18 18 <td class="tracker"><%=h issue.tracker %></td>
19 19 <td class="subject">
20 20 <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>)
21 21 </td>
22 22 </tr>
23 23 <% end %>
24 24 </tbody>
25 25 </table>
26 26 <% end %>
27 27 <% else %>
28 28 <p class="nodata"><%= l(:label_no_data) %></p>
29 29 <% end %>
@@ -1,6 +1,6
1 <p><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless @project %>
1 <p><%= link_to_project(news.project) + ': ' unless @project %>
2 2 <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
3 3 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %>
4 4 <br />
5 5 <% unless news.summary.blank? %><span class="summary"><%=h news.summary %></span><br /><% end %>
6 6 <span class="author"><%= authoring news.created_on, news.author %></span></p>
@@ -1,50 +1,50
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized(l(:label_news_new),
3 3 {:controller => 'news', :action => 'new', :project_id => @project},
4 4 :class => 'icon icon-add',
5 5 :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project %>
6 6 </div>
7 7
8 8 <div id="add-news" style="display:none;">
9 9 <h2><%=l(:label_news_new)%></h2>
10 10 <% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project },
11 11 :html => { :id => 'news-form' } do |f| %>
12 12 <%= render :partial => 'news/form', :locals => { :f => f } %>
13 13 <%= submit_tag l(:button_create) %>
14 14 <%= link_to_remote l(:label_preview),
15 15 { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
16 16 :method => 'post',
17 17 :update => 'preview',
18 18 :with => "Form.serialize('news-form')"
19 19 }, :accesskey => accesskey(:preview) %> |
20 20 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-news")' %>
21 21 <% end if @project %>
22 22 <div id="preview" class="wiki"></div>
23 23 </div>
24 24
25 25 <h2><%=l(:label_news_plural)%></h2>
26 26
27 27 <% if @newss.empty? %>
28 28 <p class="nodata"><%= l(:label_no_data) %></p>
29 29 <% else %>
30 30 <% @newss.each do |news| %>
31 <h3><%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless news.project == @project %>
31 <h3><%= link_to_project(news.project) + ': ' unless news.project == @project %>
32 32 <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
33 33 <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
34 34 <p class="author"><%= authoring news.created_on, news.author %></p>
35 35 <div class="wiki">
36 36 <%= textilizable(news.description) %>
37 37 </div>
38 38 <% end %>
39 39 <% end %>
40 40 <p class="pagination"><%= pagination_links_full @news_pages %></p>
41 41
42 42 <% other_formats_links do |f| %>
43 43 <%= f.link_to 'Atom', :url => {:project_id => @project, :key => User.current.rss_key} %>
44 44 <% end %>
45 45
46 46 <% content_for :header_tags do %>
47 47 <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %>
48 48 <% end %>
49 49
50 50 <% html_title(l(:label_news_plural)) -%>
@@ -1,62 +1,62
1 1 <% roles = Role.find_all_givable %>
2 2 <% projects = Project.active.find(:all, :order => 'lft') %>
3 3
4 4 <div class="splitcontentleft">
5 5 <% if @user.memberships.any? %>
6 6 <table class="list memberships">
7 7 <thead><tr>
8 8 <th><%= l(:label_project) %></th>
9 9 <th><%= l(:label_role_plural) %></th>
10 10 <th style="width:15%"></th>
11 11 <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
12 12 </tr></thead>
13 13 <tbody>
14 14 <% @user.memberships.each do |membership| %>
15 15 <% next if membership.new_record? %>
16 16 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
17 17 <td class="project">
18 <%= link_to h(membership.project), {:controller => 'projects', :action => 'show', :id => membership.project} %>
18 <%= link_to_project membership.project %>
19 19 </td>
20 20 <td class="roles">
21 21 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
22 22 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership },
23 23 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
24 24 <p><% roles.each do |role| %>
25 25 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
26 26 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
27 27 <% end %></p>
28 28 <%= hidden_field_tag 'membership[role_ids][]', '' %>
29 29 <p><%= submit_tag l(:button_change) %>
30 30 <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
31 31 <% end %>
32 32 </td>
33 33 <td class="buttons">
34 34 <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
35 35 <%= link_to_remote(l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
36 36 :method => :post },
37 37 :class => 'icon icon-del') if membership.deletable? %>
38 38 </td>
39 39 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
40 40 </tr>
41 41 </tbody>
42 42 <% end; reset_cycle %>
43 43 </table>
44 44 <% else %>
45 45 <p class="nodata"><%= l(:label_no_data) %></p>
46 46 <% end %>
47 47 </div>
48 48
49 49 <div class="splitcontentright">
50 50 <% if projects.any? %>
51 51 <fieldset><legend><%=l(:label_project_new)%></legend>
52 52 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user }) do %>
53 53 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
54 54 <p><%= l(:label_role_plural) %>:
55 55 <% roles.each do |role| %>
56 56 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
57 57 <% end %></p>
58 58 <p><%= submit_tag l(:button_add) %></p>
59 59 <% end %>
60 60 </fieldset>
61 61 <% end %>
62 62 </div>
@@ -1,70 +1,70
1 1 <div class="contextual">
2 2 <%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %>
3 3 </div>
4 4
5 5 <h2><%= avatar @user, :size => "50" %> <%=h @user.name %></h2>
6 6
7 7 <div class="splitcontentleft">
8 8 <ul>
9 9 <% unless @user.pref.hide_mail %>
10 10 <li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li>
11 11 <% end %>
12 12 <% for custom_value in @custom_values %>
13 13 <% if !custom_value.value.blank? %>
14 14 <li><%=h custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
15 15 <% end %>
16 16 <% end %>
17 17 <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
18 18 <% unless @user.last_login_on.nil? %>
19 19 <li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li>
20 20 <% end %>
21 21 </ul>
22 22
23 23 <% unless @memberships.empty? %>
24 24 <h3><%=l(:label_project_plural)%></h3>
25 25 <ul>
26 26 <% for membership in @memberships %>
27 <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %>
27 <li><%= link_to_project(membership.project) %>
28 28 (<%=h membership.roles.sort.collect(&:to_s).join(', ') %>, <%= format_date(membership.created_on) %>)</li>
29 29 <% end %>
30 30 </ul>
31 31 <% end %>
32 32 <%= call_hook :view_account_left_bottom, :user => @user %>
33 33 </div>
34 34
35 35 <div class="splitcontentright">
36 36
37 37 <% unless @events_by_day.empty? %>
38 38 <h3><%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :from => @events_by_day.keys.first %></h3>
39 39
40 40 <p>
41 41 <%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
42 42 </p>
43 43
44 44 <div id="activity">
45 45 <% @events_by_day.keys.sort.reverse.each do |day| %>
46 46 <h4><%= format_activity_day(day) %></h4>
47 47 <dl>
48 48 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
49 49 <dt class="<%= e.event_type %>">
50 50 <span class="time"><%= format_time(e.event_datetime, false) %></span>
51 51 <%= content_tag('span', h(e.project), :class => 'project') %>
52 52 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
53 53 <dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd>
54 54 <% end -%>
55 55 </dl>
56 56 <% end -%>
57 57 </div>
58 58
59 59 <% other_formats_links do |f| %>
60 60 <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :key => User.current.rss_key} %>
61 61 <% end %>
62 62
63 63 <% content_for :header_tags do %>
64 64 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %>
65 65 <% end %>
66 66 <% end %>
67 67 <%= call_hook :view_account_right_bottom, :user => @user %>
68 68 </div>
69 69
70 70 <% html_title @user.name %>
@@ -1,39 +1,39
1 1 <h2><%= l(:label_home) %></h2>
2 2
3 3 <div class="splitcontentleft">
4 4 <%= textilizable Setting.welcome_text %>
5 5 <% if @news.any? %>
6 6 <div class="news box">
7 7 <h3><%=l(:label_news_latest)%></h3>
8 8 <%= render :partial => 'news/news', :collection => @news %>
9 9 <%= link_to l(:label_news_view_all), :controller => 'news' %>
10 10 </div>
11 11 <% end %>
12 12 <%= call_hook(:view_welcome_index_left, :projects => @projects) %>
13 13 </div>
14 14
15 15 <div class="splitcontentright">
16 16 <% if @projects.any? %>
17 17 <div class="projects box">
18 18 <h3><%=l(:label_project_latest)%></h3>
19 19 <ul>
20 20 <% for project in @projects %>
21 21 <% @project = project %>
22 22 <li>
23 <%= link_to h(project.name), :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>)
23 <%= link_to_project project %> (<%= format_time(project.created_on) %>)
24 24 <%= textilizable project.short_description, :project => project %>
25 25 </li>
26 26 <% end %>
27 27 <% @project = nil %>
28 28 </ul>
29 29 </div>
30 30 <% end %>
31 31 <%= call_hook(:view_welcome_index_right, :projects => @projects) %>
32 32 </div>
33 33
34 34 <% content_for :header_tags do %>
35 35 <%= auto_discovery_link_tag(:atom, {:controller => 'news', :action => 'index', :key => User.current.rss_key, :format => 'atom'},
36 36 :title => "#{Setting.app_title}: #{l(:label_news_latest)}") %>
37 37 <%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :key => User.current.rss_key, :format => 'atom'},
38 38 :title => "#{Setting.app_title}: #{l(:label_activity)}") %>
39 39 <% end %>
@@ -1,600 +1,612
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 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.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 include ActionView::Helpers::DateHelper
24 24
25 25 fixtures :projects, :roles, :enabled_modules, :users,
26 26 :repositories, :changesets,
27 27 :trackers, :issue_statuses, :issues, :versions, :documents,
28 28 :wikis, :wiki_pages, :wiki_contents,
29 29 :boards, :messages,
30 30 :attachments,
31 31 :enumerations
32 32
33 33 def setup
34 34 super
35 35 end
36 36
37 37 def test_auto_links
38 38 to_test = {
39 39 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
40 40 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
41 41 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
42 42 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
43 43 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
44 44 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
45 45 '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>.',
46 46 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
47 47 '(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>)',
48 48 '(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>)',
49 49 '(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>).',
50 50 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
51 51 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
52 52 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
53 53 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
54 54 '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>',
55 55 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
56 56 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
57 57 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
58 58 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
59 59 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
60 60 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
61 61 # two exclamation marks
62 62 '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>',
63 63 # escaping
64 64 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
65 65 }
66 66 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
67 67 end
68 68
69 69 def test_auto_mailto
70 70 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
71 71 textilizable('test@foo.bar')
72 72 end
73 73
74 74 def test_inline_images
75 75 to_test = {
76 76 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
77 77 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
78 78 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
79 79 # inline styles should be stripped
80 80 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
81 81 '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" />',
82 82 '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;" />',
83 83 }
84 84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
85 85 end
86 86
87 87 def test_inline_images_inside_tags
88 88 raw = <<-RAW
89 89 h1. !foo.png! Heading
90 90
91 91 Centered image:
92 92
93 93 p=. !bar.gif!
94 94 RAW
95 95
96 96 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
97 97 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
98 98 end
99 99
100 100 def test_acronyms
101 101 to_test = {
102 102 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
103 103 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>',
104 104 }
105 105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 106
107 107 end
108 108
109 109 def test_attached_images
110 110 to_test = {
111 111 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
112 112 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
113 113 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
114 114 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
115 115 # link image
116 116 '!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>',
117 117 }
118 118 attachments = Attachment.find(:all)
119 119 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
120 120 end
121 121
122 122 def test_textile_external_links
123 123 to_test = {
124 124 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
125 125 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
126 126 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
127 127 '"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>',
128 128 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
129 129 # no multiline link text
130 130 "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",
131 131 # mailto link
132 132 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
133 133 # two exclamation marks
134 134 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
135 135 # escaping
136 136 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
137 137 }
138 138 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
139 139 end
140 140
141 141 def test_redmine_links
142 142 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
143 143 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
144 144
145 145 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
146 146 :class => 'changeset', :title => 'My very first commit')
147 147 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
148 148 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
149 149
150 150 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
151 151 :class => 'document')
152 152
153 153 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
154 154 :class => 'version')
155 155
156 156 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
157 157
158 158 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
159 159
160 160 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
161 161 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
162 162
163 163 to_test = {
164 164 # tickets
165 165 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
166 166 # changesets
167 167 'r1' => changeset_link,
168 168 'r1.' => "#{changeset_link}.",
169 169 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
170 170 'r1,r2' => "#{changeset_link},#{changeset_link2}",
171 171 # documents
172 172 'document#1' => document_link,
173 173 'document:"Test document"' => document_link,
174 174 # versions
175 175 'version#2' => version_link,
176 176 'version:1.0' => version_link,
177 177 'version:"1.0"' => version_link,
178 178 # source
179 179 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
180 180 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
181 181 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
182 182 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
183 183 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
184 184 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
185 185 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
186 186 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
187 187 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
188 188 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
189 189 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
190 190 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
191 191 # message
192 192 'message#4' => link_to('Post 2', message_url, :class => 'message'),
193 193 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
194 194 # project
195 195 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
196 196 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
197 197 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
198 198 # escaping
199 199 '!#3.' => '#3.',
200 200 '!r1' => 'r1',
201 201 '!document#1' => 'document#1',
202 202 '!document:"Test document"' => 'document:"Test document"',
203 203 '!version#2' => 'version#2',
204 204 '!version:1.0' => 'version:1.0',
205 205 '!version:"1.0"' => 'version:"1.0"',
206 206 '!source:/some/file' => 'source:/some/file',
207 207 # not found
208 208 '#0123456789' => '#0123456789',
209 209 # invalid expressions
210 210 'source:' => 'source:',
211 211 # url hash
212 212 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
213 213 }
214 214 @project = Project.find(1)
215 215 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
216 216 end
217 217
218 218 def test_attachment_links
219 219 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
220 220 to_test = {
221 221 'attachment:error281.txt' => attachment_link
222 222 }
223 223 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
224 224 end
225 225
226 226 def test_wiki_links
227 227 to_test = {
228 228 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
229 229 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
230 230 # link with anchor
231 231 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
232 232 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
233 233 # page that doesn't exist
234 234 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
235 235 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
236 236 # link to another project wiki
237 237 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki/" class="wiki-page">onlinestore</a>',
238 238 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki/" class="wiki-page">Wiki</a>',
239 239 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
240 240 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
241 241 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
242 242 # striked through link
243 243 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
244 244 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
245 245 # escaping
246 246 '![[Another page|Page]]' => '[[Another page|Page]]',
247 247 # project does not exist
248 248 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
249 249 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
250 250 }
251 251 @project = Project.find(1)
252 252 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
253 253 end
254 254
255 255 def test_html_tags
256 256 to_test = {
257 257 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
258 258 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
259 259 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
260 260 # do not escape pre/code tags
261 261 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
262 262 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
263 263 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
264 264 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
265 265 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
266 266 # remove attributes except class
267 267 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
268 268 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
269 269 }
270 270 to_test.each { |text, result| assert_equal result, textilizable(text) }
271 271 end
272 272
273 273 def test_allowed_html_tags
274 274 to_test = {
275 275 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
276 276 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
277 277 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
278 278 }
279 279 to_test.each { |text, result| assert_equal result, textilizable(text) }
280 280 end
281 281
282 282 def test_pre_tags
283 283 raw = <<-RAW
284 284 Before
285 285
286 286 <pre>
287 287 <prepared-statement-cache-size>32</prepared-statement-cache-size>
288 288 </pre>
289 289
290 290 After
291 291 RAW
292 292
293 293 expected = <<-EXPECTED
294 294 <p>Before</p>
295 295 <pre>
296 296 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
297 297 </pre>
298 298 <p>After</p>
299 299 EXPECTED
300 300
301 301 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
302 302 end
303 303
304 304 def test_pre_content_should_not_parse_wiki_and_redmine_links
305 305 raw = <<-RAW
306 306 [[CookBook documentation]]
307 307
308 308 #1
309 309
310 310 <pre>
311 311 [[CookBook documentation]]
312 312
313 313 #1
314 314 </pre>
315 315 RAW
316 316
317 317 expected = <<-EXPECTED
318 318 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
319 319 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
320 320 <pre>
321 321 [[CookBook documentation]]
322 322
323 323 #1
324 324 </pre>
325 325 EXPECTED
326 326
327 327 @project = Project.find(1)
328 328 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
329 329 end
330 330
331 331 def test_non_closing_pre_blocks_should_be_closed
332 332 raw = <<-RAW
333 333 <pre><code>
334 334 RAW
335 335
336 336 expected = <<-EXPECTED
337 337 <pre><code>
338 338 </code></pre>
339 339 EXPECTED
340 340
341 341 @project = Project.find(1)
342 342 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
343 343 end
344 344
345 345 def test_syntax_highlight
346 346 raw = <<-RAW
347 347 <pre><code class="ruby">
348 348 # Some ruby code here
349 349 </code></pre>
350 350 RAW
351 351
352 352 expected = <<-EXPECTED
353 353 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
354 354 </code></pre>
355 355 EXPECTED
356 356
357 357 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
358 358 end
359 359
360 360 def test_wiki_links_in_tables
361 361 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
362 362 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
363 363 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
364 364 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
365 365 }
366 366 @project = Project.find(1)
367 367 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
368 368 end
369 369
370 370 def test_text_formatting
371 371 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
372 372 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
373 373 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
374 374 '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>',
375 375 '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',
376 376 }
377 377 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
378 378 end
379 379
380 380 def test_wiki_horizontal_rule
381 381 assert_equal '<hr />', textilizable('---')
382 382 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
383 383 end
384 384
385 385 def test_acronym
386 386 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
387 387 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
388 388 end
389 389
390 390 def test_footnotes
391 391 raw = <<-RAW
392 392 This is some text[1].
393 393
394 394 fn1. This is the foot note
395 395 RAW
396 396
397 397 expected = <<-EXPECTED
398 398 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
399 399 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
400 400 EXPECTED
401 401
402 402 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
403 403 end
404 404
405 405 def test_table_of_content
406 406 raw = <<-RAW
407 407 {{toc}}
408 408
409 409 h1. Title
410 410
411 411 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
412 412
413 413 h2. Subtitle with a [[Wiki]] link
414 414
415 415 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
416 416
417 417 h2. Subtitle with [[Wiki|another Wiki]] link
418 418
419 419 h2. Subtitle with %{color:red}red text%
420 420
421 421 h1. Another title
422 422
423 423 h2. An "Internet link":http://www.redmine.org/ inside subtitle
424 424
425 425 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
426 426
427 427 RAW
428 428
429 429 expected = '<ul class="toc">' +
430 430 '<li class="heading1"><a href="#Title">Title</a></li>' +
431 431 '<li class="heading2"><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
432 432 '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
433 433 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
434 434 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
435 435 '<li class="heading2"><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
436 436 '<li class="heading2"><a href="#Project-Name">Project Name</a></li>' +
437 437 '</ul>'
438 438
439 439 assert textilizable(raw).gsub("\n", "").include?(expected)
440 440 end
441 441
442 442 def test_blockquote
443 443 # orig raw text
444 444 raw = <<-RAW
445 445 John said:
446 446 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
447 447 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
448 448 > * Donec odio lorem,
449 449 > * sagittis ac,
450 450 > * malesuada in,
451 451 > * adipiscing eu, dolor.
452 452 >
453 453 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
454 454 > Proin a tellus. Nam vel neque.
455 455
456 456 He's right.
457 457 RAW
458 458
459 459 # expected html
460 460 expected = <<-EXPECTED
461 461 <p>John said:</p>
462 462 <blockquote>
463 463 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
464 464 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
465 465 <ul>
466 466 <li>Donec odio lorem,</li>
467 467 <li>sagittis ac,</li>
468 468 <li>malesuada in,</li>
469 469 <li>adipiscing eu, dolor.</li>
470 470 </ul>
471 471 <blockquote>
472 472 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
473 473 </blockquote>
474 474 <p>Proin a tellus. Nam vel neque.</p>
475 475 </blockquote>
476 476 <p>He's right.</p>
477 477 EXPECTED
478 478
479 479 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
480 480 end
481 481
482 482 def test_table
483 483 raw = <<-RAW
484 484 This is a table with empty cells:
485 485
486 486 |cell11|cell12||
487 487 |cell21||cell23|
488 488 |cell31|cell32|cell33|
489 489 RAW
490 490
491 491 expected = <<-EXPECTED
492 492 <p>This is a table with empty cells:</p>
493 493
494 494 <table>
495 495 <tr><td>cell11</td><td>cell12</td><td></td></tr>
496 496 <tr><td>cell21</td><td></td><td>cell23</td></tr>
497 497 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
498 498 </table>
499 499 EXPECTED
500 500
501 501 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
502 502 end
503 503
504 504 def test_table_with_line_breaks
505 505 raw = <<-RAW
506 506 This is a table with line breaks:
507 507
508 508 |cell11
509 509 continued|cell12||
510 510 |-cell21-||cell23
511 511 cell23 line2
512 512 cell23 *line3*|
513 513 |cell31|cell32
514 514 cell32 line2|cell33|
515 515
516 516 RAW
517 517
518 518 expected = <<-EXPECTED
519 519 <p>This is a table with line breaks:</p>
520 520
521 521 <table>
522 522 <tr>
523 523 <td>cell11<br />continued</td>
524 524 <td>cell12</td>
525 525 <td></td>
526 526 </tr>
527 527 <tr>
528 528 <td><del>cell21</del></td>
529 529 <td></td>
530 530 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
531 531 </tr>
532 532 <tr>
533 533 <td>cell31</td>
534 534 <td>cell32<br/>cell32 line2</td>
535 535 <td>cell33</td>
536 536 </tr>
537 537 </table>
538 538 EXPECTED
539 539
540 540 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
541 541 end
542 542
543 543 def test_textile_should_not_mangle_brackets
544 544 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
545 545 end
546 546
547 547 def test_default_formatter
548 548 Setting.text_formatting = 'unknown'
549 549 text = 'a *link*: http://www.example.net/'
550 550 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
551 551 Setting.text_formatting = 'textile'
552 552 end
553 553
554 554 def test_due_date_distance_in_words
555 555 to_test = { Date.today => 'Due in 0 days',
556 556 Date.today + 1 => 'Due in 1 day',
557 557 Date.today + 100 => 'Due in about 3 months',
558 558 Date.today + 20000 => 'Due in over 54 years',
559 559 Date.today - 1 => '1 day late',
560 560 Date.today - 100 => 'about 3 months late',
561 561 Date.today - 20000 => 'over 54 years late',
562 562 }
563 563 to_test.each do |date, expected|
564 564 assert_equal expected, due_date_distance_in_words(date)
565 565 end
566 566 end
567 567
568 568 def test_avatar
569 569 # turn on avatars
570 570 Setting.gravatar_enabled = '1'
571 571 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
572 572 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
573 573 assert_nil avatar('jsmith')
574 574 assert_nil avatar(nil)
575 575
576 576 # turn off avatars
577 577 Setting.gravatar_enabled = '0'
578 578 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
579 579 end
580 580
581 581 def test_link_to_user
582 582 user = User.find(2)
583 583 t = link_to_user(user)
584 584 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
585 585 end
586 586
587 587 def test_link_to_user_should_not_link_to_locked_user
588 588 user = User.find(5)
589 589 assert user.locked?
590 590 t = link_to_user(user)
591 591 assert_equal user.name, t
592 592 end
593 593
594 594 def test_link_to_user_should_not_link_to_anonymous
595 595 user = User.anonymous
596 596 assert user.anonymous?
597 597 t = link_to_user(user)
598 598 assert_equal ::I18n.t(:label_user_anonymous), t
599 599 end
600
601 def test_link_to_project
602 project = Project.find(1)
603 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
604 link_to_project(project)
605 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
606 link_to_project(project, :action => 'settings')
607 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
608 link_to_project(project, {:only_path => false, :jump => 'blah'})
609 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
610 link_to_project(project, {:action => 'settings'}, :class => "project")
611 end
600 612 end
General Comments 0
You need to be logged in to leave comments. Login now