##// END OF EJS Templates
Render TOC as nested lists (#1857)....
Jean-Philippe Lang -
r4263:7f9d2b080416
parent child
Show More
@@ -1,873 +1,885
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 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
108 108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 109 end
110 110
111 111 # Generates a link to a project if active
112 112 # Examples:
113 113 #
114 114 # link_to_project(project) # => link to the specified project overview
115 115 # link_to_project(project, :action=>'settings') # => link to project settings
116 116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 118 #
119 119 def link_to_project(project, options={}, html_options = nil)
120 120 if project.active?
121 121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 122 link_to(h(project), url, html_options)
123 123 else
124 124 h(project)
125 125 end
126 126 end
127 127
128 128 def toggle_link(name, id, options={})
129 129 onclick = "Element.toggle('#{id}'); "
130 130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 131 onclick << "return false;"
132 132 link_to(name, "#", :onclick => onclick)
133 133 end
134 134
135 135 def image_to_function(name, function, html_options = {})
136 136 html_options.symbolize_keys!
137 137 tag(:input, html_options.merge({
138 138 :type => "image", :src => image_path(name),
139 139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 140 }))
141 141 end
142 142
143 143 def prompt_to_remote(name, text, param, url, html_options = {})
144 144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 145 link_to name, {}, html_options
146 146 end
147 147
148 148 def format_activity_title(text)
149 149 h(truncate_single_line(text, :length => 100))
150 150 end
151 151
152 152 def format_activity_day(date)
153 153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 154 end
155 155
156 156 def format_activity_description(text)
157 157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 158 end
159 159
160 160 def format_version_name(version)
161 161 if version.project == @project
162 162 h(version)
163 163 else
164 164 h("#{version.project} - #{version}")
165 165 end
166 166 end
167 167
168 168 def due_date_distance_in_words(date)
169 169 if date
170 170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 171 end
172 172 end
173 173
174 174 def render_page_hierarchy(pages, node=nil)
175 175 content = ''
176 176 if pages[node]
177 177 content << "<ul class=\"pages-hierarchy\">\n"
178 178 pages[node].each do |page|
179 179 content << "<li>"
180 180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 183 content << "</li>\n"
184 184 end
185 185 content << "</ul>\n"
186 186 end
187 187 content
188 188 end
189 189
190 190 # Renders flash messages
191 191 def render_flash_messages
192 192 s = ''
193 193 flash.each do |k,v|
194 194 s << content_tag('div', v, :class => "flash #{k}")
195 195 end
196 196 s
197 197 end
198 198
199 199 # Renders tabs and their content
200 200 def render_tabs(tabs)
201 201 if tabs.any?
202 202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 203 else
204 204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 205 end
206 206 end
207 207
208 208 # Renders the project quick-jump box
209 209 def render_project_jump_box
210 210 # Retrieve them now to avoid a COUNT query
211 211 projects = User.current.projects.all
212 212 if projects.any?
213 213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 215 '<option value="" disabled="disabled">---</option>'
216 216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 218 end
219 219 s << '</select>'
220 220 s
221 221 end
222 222 end
223 223
224 224 def project_tree_options_for_select(projects, options = {})
225 225 s = ''
226 226 project_tree(projects) do |project, level|
227 227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 228 tag_options = {:value => project.id}
229 229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 230 tag_options[:selected] = 'selected'
231 231 else
232 232 tag_options[:selected] = nil
233 233 end
234 234 tag_options.merge!(yield(project)) if block_given?
235 235 s << content_tag('option', name_prefix + h(project), tag_options)
236 236 end
237 237 s
238 238 end
239 239
240 240 # Yields the given block for each project with its level in the tree
241 241 #
242 242 # Wrapper for Project#project_tree
243 243 def project_tree(projects, &block)
244 244 Project.project_tree(projects, &block)
245 245 end
246 246
247 247 def project_nested_ul(projects, &block)
248 248 s = ''
249 249 if projects.any?
250 250 ancestors = []
251 251 projects.sort_by(&:lft).each do |project|
252 252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 253 s << "<ul>\n"
254 254 else
255 255 ancestors.pop
256 256 s << "</li>"
257 257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 258 ancestors.pop
259 259 s << "</ul></li>\n"
260 260 end
261 261 end
262 262 s << "<li>"
263 263 s << yield(project).to_s
264 264 ancestors << project
265 265 end
266 266 s << ("</li></ul>\n" * ancestors.size)
267 267 end
268 268 s
269 269 end
270 270
271 271 def principals_check_box_tags(name, principals)
272 272 s = ''
273 273 principals.sort.each do |principal|
274 274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 275 end
276 276 s
277 277 end
278 278
279 279 # Truncates and returns the string as a single line
280 280 def truncate_single_line(string, *args)
281 281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 282 end
283 283
284 284 # Truncates at line break after 250 characters or options[:length]
285 285 def truncate_lines(string, options={})
286 286 length = options[:length] || 250
287 287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 288 "#{$1}..."
289 289 else
290 290 string
291 291 end
292 292 end
293 293
294 294 def html_hours(text)
295 295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 296 end
297 297
298 298 def authoring(created, author, options={})
299 299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 300 end
301 301
302 302 def time_tag(time)
303 303 text = distance_of_time_in_words(Time.now, time)
304 304 if @project
305 305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 306 else
307 307 content_tag('acronym', text, :title => format_time(time))
308 308 end
309 309 end
310 310
311 311 def syntax_highlight(name, content)
312 312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 313 end
314 314
315 315 def to_path_param(path)
316 316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 317 end
318 318
319 319 def pagination_links_full(paginator, count=nil, options={})
320 320 page_param = options.delete(:page_param) || :page
321 321 per_page_links = options.delete(:per_page_links)
322 322 url_param = params.dup
323 323 # don't reuse query params if filters are present
324 324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325 325
326 326 html = ''
327 327 if paginator.current.previous
328 328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 329 end
330 330
331 331 html << (pagination_links_each(paginator, options) do |n|
332 332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 333 end || '')
334 334
335 335 if paginator.current.next
336 336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 337 end
338 338
339 339 unless count.nil?
340 340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 342 html << " | #{links}"
343 343 end
344 344 end
345 345
346 346 html
347 347 end
348 348
349 349 def per_page_links(selected=nil)
350 350 url_param = params.dup
351 351 url_param.clear if url_param.has_key?(:set_filter)
352 352
353 353 links = Setting.per_page_options_array.collect do |n|
354 354 n == selected ? n : link_to_remote(n, {:update => "content",
355 355 :url => params.dup.merge(:per_page => n),
356 356 :method => :get},
357 357 {:href => url_for(url_param.merge(:per_page => n))})
358 358 end
359 359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 360 end
361 361
362 362 def reorder_links(name, url)
363 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)) +
364 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)) +
365 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)) +
366 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))
367 367 end
368 368
369 369 def breadcrumb(*args)
370 370 elements = args.flatten
371 371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 372 end
373 373
374 374 def other_formats_links(&block)
375 375 concat('<p class="other-formats">' + l(:label_export_to))
376 376 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 377 concat('</p>')
378 378 end
379 379
380 380 def page_header_title
381 381 if @project.nil? || @project.new_record?
382 382 h(Setting.app_title)
383 383 else
384 384 b = []
385 385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 386 if ancestors.any?
387 387 root = ancestors.shift
388 388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 389 if ancestors.size > 2
390 390 b << '&#8230;'
391 391 ancestors = ancestors[-2, 2]
392 392 end
393 393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 394 end
395 395 b << h(@project)
396 396 b.join(' &#187; ')
397 397 end
398 398 end
399 399
400 400 def html_title(*args)
401 401 if args.empty?
402 402 title = []
403 403 title << @project.name if @project
404 404 title += @html_title if @html_title
405 405 title << Setting.app_title
406 406 title.select {|t| !t.blank? }.join(' - ')
407 407 else
408 408 @html_title ||= []
409 409 @html_title += args
410 410 end
411 411 end
412 412
413 413 # Returns the theme, controller name, and action as css classes for the
414 414 # HTML body.
415 415 def body_css_classes
416 416 css = []
417 417 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 418 css << 'theme-' + theme.name
419 419 end
420 420
421 421 css << 'controller-' + params[:controller]
422 422 css << 'action-' + params[:action]
423 423 css.join(' ')
424 424 end
425 425
426 426 def accesskey(s)
427 427 Redmine::AccessKeys.key_for s
428 428 end
429 429
430 430 # Formats text according to system settings.
431 431 # 2 ways to call this method:
432 432 # * with a String: textilizable(text, options)
433 433 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 434 def textilizable(*args)
435 435 options = args.last.is_a?(Hash) ? args.pop : {}
436 436 case args.size
437 437 when 1
438 438 obj = options[:object]
439 439 text = args.shift
440 440 when 2
441 441 obj = args.shift
442 442 attr = args.shift
443 443 text = obj.send(attr).to_s
444 444 else
445 445 raise ArgumentError, 'invalid arguments to textilizable'
446 446 end
447 447 return '' if text.blank?
448 448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 449 only_path = options.delete(:only_path) == false ? false : true
450 450
451 451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452 452
453 453 parse_non_pre_blocks(text) do |text|
454 454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
455 455 send method_name, text, project, obj, attr, only_path, options
456 456 end
457 457 end
458 458 end
459 459
460 460 def parse_non_pre_blocks(text)
461 461 s = StringScanner.new(text)
462 462 tags = []
463 463 parsed = ''
464 464 while !s.eos?
465 465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
466 466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
467 467 if tags.empty?
468 468 yield text
469 469 end
470 470 parsed << text
471 471 if tag
472 472 if closing
473 473 if tags.last == tag.downcase
474 474 tags.pop
475 475 end
476 476 else
477 477 tags << tag.downcase
478 478 end
479 479 parsed << full_tag
480 480 end
481 481 end
482 482 # Close any non closing tags
483 483 while tag = tags.pop
484 484 parsed << "</#{tag}>"
485 485 end
486 486 parsed
487 487 end
488 488
489 489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
490 490 # when using an image link, try to use an attachment, if possible
491 491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
492 492 attachments = nil
493 493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
494 494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
495 495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
496 496 # search for the picture in attachments
497 497 if found = attachments.detect { |att| att.filename.downcase == filename }
498 498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
499 499 desc = found.description.to_s.gsub('"', '')
500 500 if !desc.blank? && alttext.blank?
501 501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
502 502 end
503 503 "src=\"#{image_url}\"#{alt}"
504 504 else
505 505 m
506 506 end
507 507 end
508 508 end
509 509 end
510 510
511 511 # Wiki links
512 512 #
513 513 # Examples:
514 514 # [[mypage]]
515 515 # [[mypage|mytext]]
516 516 # wiki links can refer other project wikis, using project name or identifier:
517 517 # [[project:]] -> wiki starting page
518 518 # [[project:|mytext]]
519 519 # [[project:mypage]]
520 520 # [[project:mypage|mytext]]
521 521 def parse_wiki_links(text, project, obj, attr, only_path, options)
522 522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
523 523 link_project = project
524 524 esc, all, page, title = $1, $2, $3, $5
525 525 if esc.nil?
526 526 if page =~ /^([^\:]+)\:(.*)$/
527 527 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
528 528 page = $2
529 529 title ||= $1 if page.blank?
530 530 end
531 531
532 532 if link_project && link_project.wiki
533 533 # extract anchor
534 534 anchor = nil
535 535 if page =~ /^(.+?)\#(.+)$/
536 536 page, anchor = $1, $2
537 537 end
538 538 # check if page exists
539 539 wiki_page = link_project.wiki.find_page(page)
540 540 url = case options[:wiki_links]
541 541 when :local; "#{title}.html"
542 542 when :anchor; "##{title}" # used for single-file wiki export
543 543 else
544 544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
545 545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
546 546 end
547 547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
548 548 else
549 549 # project or wiki doesn't exist
550 550 all
551 551 end
552 552 else
553 553 all
554 554 end
555 555 end
556 556 end
557 557
558 558 # Redmine links
559 559 #
560 560 # Examples:
561 561 # Issues:
562 562 # #52 -> Link to issue #52
563 563 # Changesets:
564 564 # r52 -> Link to revision 52
565 565 # commit:a85130f -> Link to scmid starting with a85130f
566 566 # Documents:
567 567 # document#17 -> Link to document with id 17
568 568 # document:Greetings -> Link to the document with title "Greetings"
569 569 # document:"Some document" -> Link to the document with title "Some document"
570 570 # Versions:
571 571 # version#3 -> Link to version with id 3
572 572 # version:1.0.0 -> Link to version named "1.0.0"
573 573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
574 574 # Attachments:
575 575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
576 576 # Source files:
577 577 # source:some/file -> Link to the file located at /some/file in the project's repository
578 578 # source:some/file@52 -> Link to the file's revision 52
579 579 # source:some/file#L120 -> Link to line 120 of the file
580 580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
581 581 # export:some/file -> Force the download of the file
582 582 # Forum messages:
583 583 # message#1218 -> Link to message with id 1218
584 584 def parse_redmine_links(text, project, obj, attr, only_path, options)
585 585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
586 586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
587 587 link = nil
588 588 if esc.nil?
589 589 if prefix.nil? && sep == 'r'
590 590 if project && (changeset = project.changesets.find_by_revision(identifier))
591 591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
592 592 :class => 'changeset',
593 593 :title => truncate_single_line(changeset.comments, :length => 100))
594 594 end
595 595 elsif sep == '#'
596 596 oid = identifier.to_i
597 597 case prefix
598 598 when nil
599 599 if issue = Issue.visible.find_by_id(oid, :include => :status)
600 600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
601 601 :class => issue.css_classes,
602 602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
603 603 end
604 604 when 'document'
605 605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
606 606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
607 607 :class => 'document'
608 608 end
609 609 when 'version'
610 610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
611 611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
612 612 :class => 'version'
613 613 end
614 614 when 'message'
615 615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
616 616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
617 617 :controller => 'messages',
618 618 :action => 'show',
619 619 :board_id => message.board,
620 620 :id => message.root,
621 621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
622 622 :class => 'message'
623 623 end
624 624 when 'project'
625 625 if p = Project.visible.find_by_id(oid)
626 626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
627 627 end
628 628 end
629 629 elsif sep == ':'
630 630 # removes the double quotes if any
631 631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
632 632 case prefix
633 633 when 'document'
634 634 if project && document = project.documents.find_by_title(name)
635 635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 636 :class => 'document'
637 637 end
638 638 when 'version'
639 639 if project && version = project.versions.find_by_name(name)
640 640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 641 :class => 'version'
642 642 end
643 643 when 'commit'
644 644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
645 645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
646 646 :class => 'changeset',
647 647 :title => truncate_single_line(changeset.comments, :length => 100)
648 648 end
649 649 when 'source', 'export'
650 650 if project && project.repository
651 651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
652 652 path, rev, anchor = $1, $3, $5
653 653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
654 654 :path => to_path_param(path),
655 655 :rev => rev,
656 656 :anchor => anchor,
657 657 :format => (prefix == 'export' ? 'raw' : nil)},
658 658 :class => (prefix == 'export' ? 'source download' : 'source')
659 659 end
660 660 when 'attachment'
661 661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
662 662 if attachments && attachment = attachments.detect {|a| a.filename == name }
663 663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
664 664 :class => 'attachment'
665 665 end
666 666 when 'project'
667 667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
668 668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
669 669 end
670 670 end
671 671 end
672 672 end
673 673 leading + (link || "#{prefix}#{sep}#{identifier}")
674 674 end
675 675 end
676 676
677 677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
678 678 HEADING_RE = /<h(1|2|3)( [^>]+)?>(.+?)<\/h(1|2|3)>/i unless const_defined?(:HEADING_RE)
679 679
680 680 # Headings and TOC
681 681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
682 682 def parse_headings(text, project, obj, attr, only_path, options)
683 683 headings = []
684 684 text.gsub!(HEADING_RE) do
685 level, attrs, content = $1, $2, $3
685 level, attrs, content = $1.to_i, $2, $3
686 686 item = strip_tags(content).strip
687 687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
688 688 headings << [level, anchor, item]
689 689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
690 690 end unless options[:headings] == false
691 691
692 692 text.gsub!(TOC_RE) do
693 693 if headings.empty?
694 694 ''
695 695 else
696 696 div_class = 'toc'
697 697 div_class << ' right' if $1 == '>'
698 698 div_class << ' left' if $1 == '<'
699 out = "<ul class=\"#{div_class}\">"
699 out = "<ul class=\"#{div_class}\"><li>"
700 root = headings.map(&:first).min
701 current = root
702 started = false
700 703 headings.each do |level, anchor, item|
701 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{item}</a></li>\n"
704 if level > current
705 out << '<ul><li>' * (level - current)
706 elsif level < current
707 out << "</li></ul>\n" * (current - level) + "</li><li>"
708 elsif started
709 out << '</li><li>'
710 end
711 out << "<a href=\"##{anchor}\">#{item}</a>"
712 current = level
713 started = true
702 714 end
703 out << '</ul>'
704 out
715 out << '</li></ul>' * (current - root)
716 out << '</li></ul>'
705 717 end
706 718 end
707 719 end
708 720
709 721 # Same as Rails' simple_format helper without using paragraphs
710 722 def simple_format_without_paragraph(text)
711 723 text.to_s.
712 724 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
713 725 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
714 726 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
715 727 end
716 728
717 729 def lang_options_for_select(blank=true)
718 730 (blank ? [["(auto)", ""]] : []) +
719 731 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
720 732 end
721 733
722 734 def label_tag_for(name, option_tags = nil, options = {})
723 735 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
724 736 content_tag("label", label_text)
725 737 end
726 738
727 739 def labelled_tabular_form_for(name, object, options, &proc)
728 740 options[:html] ||= {}
729 741 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
730 742 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
731 743 end
732 744
733 745 def back_url_hidden_field_tag
734 746 back_url = params[:back_url] || request.env['HTTP_REFERER']
735 747 back_url = CGI.unescape(back_url.to_s)
736 748 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
737 749 end
738 750
739 751 def check_all_links(form_name)
740 752 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
741 753 " | " +
742 754 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
743 755 end
744 756
745 757 def progress_bar(pcts, options={})
746 758 pcts = [pcts, pcts] unless pcts.is_a?(Array)
747 759 pcts = pcts.collect(&:round)
748 760 pcts[1] = pcts[1] - pcts[0]
749 761 pcts << (100 - pcts[1] - pcts[0])
750 762 width = options[:width] || '100px;'
751 763 legend = options[:legend] || ''
752 764 content_tag('table',
753 765 content_tag('tr',
754 766 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
755 767 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
756 768 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
757 769 ), :class => 'progress', :style => "width: #{width};") +
758 770 content_tag('p', legend, :class => 'pourcent')
759 771 end
760 772
761 773 def checked_image(checked=true)
762 774 if checked
763 775 image_tag 'toggle_check.png'
764 776 end
765 777 end
766 778
767 779 def context_menu(url)
768 780 unless @context_menu_included
769 781 content_for :header_tags do
770 782 javascript_include_tag('context_menu') +
771 783 stylesheet_link_tag('context_menu')
772 784 end
773 785 if l(:direction) == 'rtl'
774 786 content_for :header_tags do
775 787 stylesheet_link_tag('context_menu_rtl')
776 788 end
777 789 end
778 790 @context_menu_included = true
779 791 end
780 792 javascript_tag "new ContextMenu('#{ url_for(url) }')"
781 793 end
782 794
783 795 def context_menu_link(name, url, options={})
784 796 options[:class] ||= ''
785 797 if options.delete(:selected)
786 798 options[:class] << ' icon-checked disabled'
787 799 options[:disabled] = true
788 800 end
789 801 if options.delete(:disabled)
790 802 options.delete(:method)
791 803 options.delete(:confirm)
792 804 options.delete(:onclick)
793 805 options[:class] << ' disabled'
794 806 url = '#'
795 807 end
796 808 link_to name, url, options
797 809 end
798 810
799 811 def calendar_for(field_id)
800 812 include_calendar_headers_tags
801 813 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
802 814 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
803 815 end
804 816
805 817 def include_calendar_headers_tags
806 818 unless @calendar_headers_tags_included
807 819 @calendar_headers_tags_included = true
808 820 content_for :header_tags do
809 821 start_of_week = case Setting.start_of_week.to_i
810 822 when 1
811 823 'Calendar._FD = 1;' # Monday
812 824 when 7
813 825 'Calendar._FD = 0;' # Sunday
814 826 else
815 827 '' # use language
816 828 end
817 829
818 830 javascript_include_tag('calendar/calendar') +
819 831 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
820 832 javascript_tag(start_of_week) +
821 833 javascript_include_tag('calendar/calendar-setup') +
822 834 stylesheet_link_tag('calendar')
823 835 end
824 836 end
825 837 end
826 838
827 839 def content_for(name, content = nil, &block)
828 840 @has_content ||= {}
829 841 @has_content[name] = true
830 842 super(name, content, &block)
831 843 end
832 844
833 845 def has_content?(name)
834 846 (@has_content && @has_content[name]) || false
835 847 end
836 848
837 849 # Returns the avatar image tag for the given +user+ if avatars are enabled
838 850 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
839 851 def avatar(user, options = { })
840 852 if Setting.gravatar_enabled?
841 853 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
842 854 email = nil
843 855 if user.respond_to?(:mail)
844 856 email = user.mail
845 857 elsif user.to_s =~ %r{<(.+?)>}
846 858 email = $1
847 859 end
848 860 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
849 861 else
850 862 ''
851 863 end
852 864 end
853 865
854 866 def favicon
855 867 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
856 868 end
857 869
858 870 private
859 871
860 872 def wiki_helper
861 873 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
862 874 extend helper
863 875 return self
864 876 end
865 877
866 878 def link_to_remote_content_update(text, url_params)
867 879 link_to_remote(text,
868 880 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
869 881 {:href => url_for(:params => url_params)}
870 882 )
871 883 end
872 884
873 885 end
@@ -1,943 +1,944
1 1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 2
3 3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 4 h1 {margin:0; padding:0; font-size: 24px;}
5 5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8 8
9 9 /***** Layout *****/
10 10 #wrapper {background: white;}
11 11
12 12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 13 #top-menu ul {margin: 0; padding: 0;}
14 14 #top-menu li {
15 15 float:left;
16 16 list-style-type:none;
17 17 margin: 0px 0px 0px 0px;
18 18 padding: 0px 0px 0px 0px;
19 19 white-space:nowrap;
20 20 }
21 21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23 23
24 24 #account {float:right;}
25 25
26 26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 27 #header a {color:#f8f8f8;}
28 28 #header h1 a.ancestor { font-size: 80%; }
29 29 #quick-search {float:right;}
30 30
31 31 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
32 32 #main-menu ul {margin: 0; padding: 0;}
33 33 #main-menu li {
34 34 float:left;
35 35 list-style-type:none;
36 36 margin: 0px 2px 0px 0px;
37 37 padding: 0px 0px 0px 0px;
38 38 white-space:nowrap;
39 39 }
40 40 #main-menu li a {
41 41 display: block;
42 42 color: #fff;
43 43 text-decoration: none;
44 44 font-weight: bold;
45 45 margin: 0;
46 46 padding: 4px 10px 4px 10px;
47 47 }
48 48 #main-menu li a:hover {background:#759FCF; color:#fff;}
49 49 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
50 50
51 51 #admin-menu ul {margin: 0; padding: 0;}
52 52 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
53 53
54 54 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
55 55 #admin-menu a.projects { background-image: url(../images/projects.png); }
56 56 #admin-menu a.users { background-image: url(../images/user.png); }
57 57 #admin-menu a.groups { background-image: url(../images/group.png); }
58 58 #admin-menu a.roles { background-image: url(../images/database_key.png); }
59 59 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
60 60 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
61 61 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
62 62 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
63 63 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
64 64 #admin-menu a.settings { background-image: url(../images/changeset.png); }
65 65 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
66 66 #admin-menu a.info { background-image: url(../images/help.png); }
67 67 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
68 68
69 69 #main {background-color:#EEEEEE;}
70 70
71 71 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
72 72 * html #sidebar{ width: 22%; }
73 73 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
74 74 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
75 75 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
76 76 #sidebar .contextual { margin-right: 1em; }
77 77
78 78 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
79 79 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
80 80 html>body #content { min-height: 600px; }
81 81 * html body #content { height: 600px; } /* IE */
82 82
83 83 #main.nosidebar #sidebar{ display: none; }
84 84 #main.nosidebar #content{ width: auto; border-right: 0; }
85 85
86 86 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
87 87
88 88 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
89 89 #login-form table td {padding: 6px;}
90 90 #login-form label {font-weight: bold;}
91 91 #login-form input#username, #login-form input#password { width: 300px; }
92 92
93 93 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
94 94
95 95 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
96 96
97 97 /***** Links *****/
98 98 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
99 99 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
100 100 a img{ border: 0; }
101 101
102 102 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
103 103
104 104 /***** Tables *****/
105 105 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
106 106 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
107 107 table.list td { vertical-align: top; }
108 108 table.list td.id { width: 2%; text-align: center;}
109 109 table.list td.checkbox { width: 15px; padding: 0px;}
110 110 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
111 111 table.list td.buttons a { padding-right: 0.6em; }
112 112 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
113 113
114 114 tr.project td.name a { white-space:nowrap; }
115 115
116 116 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
117 117 tr.project.idnt-1 td.name {padding-left: 0.5em;}
118 118 tr.project.idnt-2 td.name {padding-left: 2em;}
119 119 tr.project.idnt-3 td.name {padding-left: 3.5em;}
120 120 tr.project.idnt-4 td.name {padding-left: 5em;}
121 121 tr.project.idnt-5 td.name {padding-left: 6.5em;}
122 122 tr.project.idnt-6 td.name {padding-left: 8em;}
123 123 tr.project.idnt-7 td.name {padding-left: 9.5em;}
124 124 tr.project.idnt-8 td.name {padding-left: 11em;}
125 125 tr.project.idnt-9 td.name {padding-left: 12.5em;}
126 126
127 127 tr.issue { text-align: center; white-space: nowrap; }
128 128 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
129 129 tr.issue td.subject { text-align: left; }
130 130 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
131 131
132 132 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
133 133 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
134 134 tr.issue.idnt-2 td.subject {padding-left: 2em;}
135 135 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
136 136 tr.issue.idnt-4 td.subject {padding-left: 5em;}
137 137 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
138 138 tr.issue.idnt-6 td.subject {padding-left: 8em;}
139 139 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
140 140 tr.issue.idnt-8 td.subject {padding-left: 11em;}
141 141 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
142 142
143 143 tr.entry { border: 1px solid #f8f8f8; }
144 144 tr.entry td { white-space: nowrap; }
145 145 tr.entry td.filename { width: 30%; }
146 146 tr.entry td.size { text-align: right; font-size: 90%; }
147 147 tr.entry td.revision, tr.entry td.author { text-align: center; }
148 148 tr.entry td.age { text-align: right; }
149 149 tr.entry.file td.filename a { margin-left: 16px; }
150 150
151 151 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
152 152 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
153 153
154 154 tr.changeset td.author { text-align: center; width: 15%; }
155 155 tr.changeset td.committed_on { text-align: center; width: 15%; }
156 156
157 157 table.files tr.file td { text-align: center; }
158 158 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
159 159 table.files tr.file td.digest { font-size: 80%; }
160 160
161 161 table.members td.roles, table.memberships td.roles { width: 45%; }
162 162
163 163 tr.message { height: 2.6em; }
164 164 tr.message td.subject { padding-left: 20px; }
165 165 tr.message td.created_on { white-space: nowrap; }
166 166 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
167 167 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
168 168 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
169 169
170 170 tr.version.closed, tr.version.closed a { color: #999; }
171 171 tr.version td.name { padding-left: 20px; }
172 172 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
173 173 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; }
174 174
175 175 tr.user td { width:13%; }
176 176 tr.user td.email { width:18%; }
177 177 tr.user td { white-space: nowrap; }
178 178 tr.user.locked, tr.user.registered { color: #aaa; }
179 179 tr.user.locked a, tr.user.registered a { color: #aaa; }
180 180
181 181 tr.time-entry { text-align: center; white-space: nowrap; }
182 182 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
183 183 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
184 184 td.hours .hours-dec { font-size: 0.9em; }
185 185
186 186 table.plugins td { vertical-align: middle; }
187 187 table.plugins td.configure { text-align: right; padding-right: 1em; }
188 188 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
189 189 table.plugins span.description { display: block; font-size: 0.9em; }
190 190 table.plugins span.url { display: block; font-size: 0.9em; }
191 191
192 192 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
193 193 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
194 194
195 195 table.list tbody tr:hover { background-color:#ffffdd; }
196 196 table.list tbody tr.group:hover { background-color:inherit; }
197 197 table td {padding:2px;}
198 198 table p {margin:0;}
199 199 .odd {background-color:#f6f7f8;}
200 200 .even {background-color: #fff;}
201 201
202 202 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
203 203 a.sort.asc { background-image: url(../images/sort_asc.png); }
204 204 a.sort.desc { background-image: url(../images/sort_desc.png); }
205 205
206 206 table.attributes { width: 100% }
207 207 table.attributes th { vertical-align: top; text-align: left; }
208 208 table.attributes td { vertical-align: top; }
209 209
210 210 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
211 211
212 212 td.center {text-align:center;}
213 213
214 214 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
215 215
216 216 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
217 217 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
218 218 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
219 219 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
220 220
221 221 #watchers ul {margin: 0; padding: 0;}
222 222 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
223 223 #watchers select {width: 95%; display: block;}
224 224 #watchers a.delete {opacity: 0.4;}
225 225 #watchers a.delete:hover {opacity: 1;}
226 226 #watchers img.gravatar {vertical-align: middle;margin: 0 4px 2px 0;}
227 227
228 228 .highlight { background-color: #FCFD8D;}
229 229 .highlight.token-1 { background-color: #faa;}
230 230 .highlight.token-2 { background-color: #afa;}
231 231 .highlight.token-3 { background-color: #aaf;}
232 232
233 233 .box{
234 234 padding:6px;
235 235 margin-bottom: 10px;
236 236 background-color:#f6f6f6;
237 237 color:#505050;
238 238 line-height:1.5em;
239 239 border: 1px solid #e4e4e4;
240 240 }
241 241
242 242 div.square {
243 243 border: 1px solid #999;
244 244 float: left;
245 245 margin: .3em .4em 0 .4em;
246 246 overflow: hidden;
247 247 width: .6em; height: .6em;
248 248 }
249 249 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
250 250 .contextual input, .contextual select {font-size:0.9em;}
251 251 .message .contextual { margin-top: 0; }
252 252
253 253 .splitcontentleft{float:left; width:49%;}
254 254 .splitcontentright{float:right; width:49%;}
255 255 form {display: inline;}
256 256 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
257 257 fieldset {border: 1px solid #e4e4e4; margin:0;}
258 258 legend {color: #484848;}
259 259 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
260 260 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
261 261 blockquote blockquote { margin-left: 0;}
262 262 acronym { border-bottom: 1px dotted; cursor: help; }
263 263 textarea.wiki-edit { width: 99%; }
264 264 li p {margin-top: 0;}
265 265 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
266 266 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
267 267 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
268 268 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
269 269
270 270 div.issue div.subject div div { padding-left: 16px; }
271 271 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
272 272 div.issue div.subject>div>p { margin-top: 0.5em; }
273 273 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
274 274
275 275 #issue_tree table.issues { border: 0; }
276 276 #issue_tree td.checkbox {display:none;}
277 277
278 278 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
279 279 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
280 280 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
281 281
282 282 fieldset#date-range p { margin: 2px 0 2px 0; }
283 283 fieldset#filters table { border-collapse: collapse; }
284 284 fieldset#filters table td { padding: 0; vertical-align: middle; }
285 285 fieldset#filters tr.filter { height: 2em; }
286 286 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
287 287 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
288 288
289 289 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
290 290 div#issue-changesets div.changeset { padding: 4px;}
291 291 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
292 292 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
293 293
294 294 div#activity dl, #search-results { margin-left: 2em; }
295 295 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
296 296 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
297 297 div#activity dt.me .time { border-bottom: 1px solid #999; }
298 298 div#activity dt .time { color: #777; font-size: 80%; }
299 299 div#activity dd .description, #search-results dd .description { font-style: italic; }
300 300 div#activity span.project:after, #search-results span.project:after { content: " -"; }
301 301 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
302 302
303 303 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
304 304
305 305 div#search-results-counts {float:right;}
306 306 div#search-results-counts ul { margin-top: 0.5em; }
307 307 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
308 308
309 309 dt.issue { background-image: url(../images/ticket.png); }
310 310 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
311 311 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
312 312 dt.issue-note { background-image: url(../images/ticket_note.png); }
313 313 dt.changeset { background-image: url(../images/changeset.png); }
314 314 dt.news { background-image: url(../images/news.png); }
315 315 dt.message { background-image: url(../images/message.png); }
316 316 dt.reply { background-image: url(../images/comments.png); }
317 317 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
318 318 dt.attachment { background-image: url(../images/attachment.png); }
319 319 dt.document { background-image: url(../images/document.png); }
320 320 dt.project { background-image: url(../images/projects.png); }
321 321 dt.time-entry { background-image: url(../images/time.png); }
322 322
323 323 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
324 324
325 325 div#roadmap .related-issues { margin-bottom: 1em; }
326 326 div#roadmap .related-issues td.checkbox { display: none; }
327 327 div#roadmap .wiki h1:first-child { display: none; }
328 328 div#roadmap .wiki h1 { font-size: 120%; }
329 329 div#roadmap .wiki h2 { font-size: 110%; }
330 330
331 331 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
332 332 div#version-summary fieldset { margin-bottom: 1em; }
333 333 div#version-summary .total-hours { text-align: right; }
334 334
335 335 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
336 336 table#time-report tbody tr { font-style: italic; color: #777; }
337 337 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
338 338 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
339 339 table#time-report .hours-dec { font-size: 0.9em; }
340 340
341 341 form .attributes { margin-bottom: 8px; }
342 342 form .attributes p { padding-top: 1px; padding-bottom: 2px; }
343 343 form .attributes select { min-width: 50%; }
344 344
345 345 ul.projects { margin: 0; padding-left: 1em; }
346 346 ul.projects.root { margin: 0; padding: 0; }
347 347 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
348 348 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
349 349 ul.projects li.child { list-style-type:none; margin-top: 1em;}
350 350 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
351 351 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
352 352
353 353 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
354 354 #tracker_project_ids li { list-style-type:none; }
355 355
356 356 ul.properties {padding:0; font-size: 0.9em; color: #777;}
357 357 ul.properties li {list-style-type:none;}
358 358 ul.properties li span {font-style:italic;}
359 359
360 360 .total-hours { font-size: 110%; font-weight: bold; }
361 361 .total-hours span.hours-int { font-size: 120%; }
362 362
363 363 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
364 364 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
365 365
366 366 #workflow_copy_form select { width: 200px; }
367 367
368 368 .pagination {font-size: 90%}
369 369 p.pagination {margin-top:8px;}
370 370
371 371 /***** Tabular forms ******/
372 372 .tabular p{
373 373 margin: 0;
374 374 padding: 5px 0 8px 0;
375 375 padding-left: 180px; /*width of left column containing the label elements*/
376 376 height: 1%;
377 377 clear:left;
378 378 }
379 379
380 380 html>body .tabular p {overflow:hidden;}
381 381
382 382 .tabular label{
383 383 font-weight: bold;
384 384 float: left;
385 385 text-align: right;
386 386 margin-left: -180px; /*width of left column*/
387 387 width: 175px; /*width of labels. Should be smaller than left column to create some right
388 388 margin*/
389 389 }
390 390
391 391 .tabular label.floating{
392 392 font-weight: normal;
393 393 margin-left: 0px;
394 394 text-align: left;
395 395 width: 270px;
396 396 }
397 397
398 398 .tabular label.block{
399 399 font-weight: normal;
400 400 margin-left: 0px !important;
401 401 text-align: left;
402 402 float: none;
403 403 display: block;
404 404 width: auto;
405 405 }
406 406
407 407 .tabular label.inline{
408 408 float:none;
409 409 margin-left: 5px !important;
410 410 width: auto;
411 411 }
412 412
413 413 input#time_entry_comments { width: 90%;}
414 414
415 415 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
416 416
417 417 .tabular.settings p{ padding-left: 300px; }
418 418 .tabular.settings label{ margin-left: -300px; width: 295px; }
419 419 .tabular.settings textarea { width: 99%; }
420 420
421 421 fieldset.settings label { display: block; }
422 422 .parent { padding-left: 20px; }
423 423
424 424 .required {color: #bb0000;}
425 425 .summary {font-style: italic;}
426 426
427 427 #attachments_fields input[type=text] {margin-left: 8px; }
428 428
429 429 div.attachments { margin-top: 12px; }
430 430 div.attachments p { margin:4px 0 2px 0; }
431 431 div.attachments img { vertical-align: middle; }
432 432 div.attachments span.author { font-size: 0.9em; color: #888; }
433 433
434 434 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
435 435 .other-formats span + span:before { content: "| "; }
436 436
437 437 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
438 438
439 439 /* Project members tab */
440 440 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
441 441 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
442 442 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
443 443 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
444 444 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
445 445 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
446 446
447 447 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
448 448
449 449 input#principal_search, input#user_search {width:100%}
450 450
451 451 * html div#tab-content-members fieldset div { height: 450px; }
452 452
453 453 /***** Flash & error messages ****/
454 454 #errorExplanation, div.flash, .nodata, .warning {
455 455 padding: 4px 4px 4px 30px;
456 456 margin-bottom: 12px;
457 457 font-size: 1.1em;
458 458 border: 2px solid;
459 459 }
460 460
461 461 div.flash {margin-top: 8px;}
462 462
463 463 div.flash.error, #errorExplanation {
464 464 background: url(../images/exclamation.png) 8px 50% no-repeat;
465 465 background-color: #ffe3e3;
466 466 border-color: #dd0000;
467 467 color: #880000;
468 468 }
469 469
470 470 div.flash.notice {
471 471 background: url(../images/true.png) 8px 5px no-repeat;
472 472 background-color: #dfffdf;
473 473 border-color: #9fcf9f;
474 474 color: #005f00;
475 475 }
476 476
477 477 div.flash.warning {
478 478 background: url(../images/warning.png) 8px 5px no-repeat;
479 479 background-color: #FFEBC1;
480 480 border-color: #FDBF3B;
481 481 color: #A6750C;
482 482 text-align: left;
483 483 }
484 484
485 485 .nodata, .warning {
486 486 text-align: center;
487 487 background-color: #FFEBC1;
488 488 border-color: #FDBF3B;
489 489 color: #A6750C;
490 490 }
491 491
492 492 #errorExplanation ul { font-size: 0.9em;}
493 493 #errorExplanation h2, #errorExplanation p { display: none; }
494 494
495 495 /***** Ajax indicator ******/
496 496 #ajax-indicator {
497 497 position: absolute; /* fixed not supported by IE */
498 498 background-color:#eee;
499 499 border: 1px solid #bbb;
500 500 top:35%;
501 501 left:40%;
502 502 width:20%;
503 503 font-weight:bold;
504 504 text-align:center;
505 505 padding:0.6em;
506 506 z-index:100;
507 507 filter:alpha(opacity=50);
508 508 opacity: 0.5;
509 509 }
510 510
511 511 html>body #ajax-indicator { position: fixed; }
512 512
513 513 #ajax-indicator span {
514 514 background-position: 0% 40%;
515 515 background-repeat: no-repeat;
516 516 background-image: url(../images/loading.gif);
517 517 padding-left: 26px;
518 518 vertical-align: bottom;
519 519 }
520 520
521 521 /***** Calendar *****/
522 522 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
523 523 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
524 524 table.cal thead th.week-number {width: auto;}
525 525 table.cal tbody tr {height: 100px;}
526 526 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
527 527 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
528 528 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
529 529 table.cal td.odd p.day-num {color: #bbb;}
530 530 table.cal td.today {background:#ffffdd;}
531 531 table.cal td.today p.day-num {font-weight: bold;}
532 532 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
533 533 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
534 534 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
535 535 p.cal.legend span {display:block;}
536 536
537 537 /***** Tooltips ******/
538 538 .tooltip{position:relative;z-index:24;}
539 539 .tooltip:hover{z-index:25;color:#000;}
540 540 .tooltip span.tip{display: none; text-align:left;}
541 541
542 542 div.tooltip:hover span.tip{
543 543 display:block;
544 544 position:absolute;
545 545 top:12px; left:24px; width:270px;
546 546 border:1px solid #555;
547 547 background-color:#fff;
548 548 padding: 4px;
549 549 font-size: 0.8em;
550 550 color:#505050;
551 551 }
552 552
553 553 /***** Progress bar *****/
554 554 table.progress {
555 555 border: 1px solid #D7D7D7;
556 556 border-collapse: collapse;
557 557 border-spacing: 0pt;
558 558 empty-cells: show;
559 559 text-align: center;
560 560 float:left;
561 561 margin: 1px 6px 1px 0px;
562 562 }
563 563
564 564 table.progress td { height: 0.9em; }
565 565 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
566 566 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
567 567 table.progress td.open { background: #FFF none repeat scroll 0%; }
568 568 p.pourcent {font-size: 80%;}
569 569 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
570 570
571 571 /***** Tabs *****/
572 572 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
573 573 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
574 574 #content .tabs ul li {
575 575 float:left;
576 576 list-style-type:none;
577 577 white-space:nowrap;
578 578 margin-right:8px;
579 579 background:#fff;
580 580 position:relative;
581 581 margin-bottom:-1px;
582 582 }
583 583 #content .tabs ul li a{
584 584 display:block;
585 585 font-size: 0.9em;
586 586 text-decoration:none;
587 587 line-height:1.3em;
588 588 padding:4px 6px 4px 6px;
589 589 border: 1px solid #ccc;
590 590 border-bottom: 1px solid #bbbbbb;
591 591 background-color: #eeeeee;
592 592 color:#777;
593 593 font-weight:bold;
594 594 }
595 595
596 596 #content .tabs ul li a:hover {
597 597 background-color: #ffffdd;
598 598 text-decoration:none;
599 599 }
600 600
601 601 #content .tabs ul li a.selected {
602 602 background-color: #fff;
603 603 border: 1px solid #bbbbbb;
604 604 border-bottom: 1px solid #fff;
605 605 }
606 606
607 607 #content .tabs ul li a.selected:hover {
608 608 background-color: #fff;
609 609 }
610 610
611 611 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
612 612
613 613 button.tab-left, button.tab-right {
614 614 font-size: 0.9em;
615 615 cursor: pointer;
616 616 height:24px;
617 617 border: 1px solid #ccc;
618 618 border-bottom: 1px solid #bbbbbb;
619 619 position:absolute;
620 620 padding:4px;
621 621 width: 20px;
622 622 bottom: -1px;
623 623 }
624 624
625 625 button.tab-left {
626 626 right: 20px;
627 627 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
628 628 }
629 629
630 630 button.tab-right {
631 631 right: 0;
632 632 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
633 633 }
634 634
635 635 /***** Auto-complete *****/
636 636 div.autocomplete {
637 637 position:absolute;
638 638 width:400px;
639 639 margin:0;
640 640 padding:0;
641 641 }
642 642 div.autocomplete ul {
643 643 list-style-type:none;
644 644 margin:0;
645 645 padding:0;
646 646 }
647 647 div.autocomplete ul li {
648 648 list-style-type:none;
649 649 display:block;
650 650 margin:-1px 0 0 0;
651 651 padding:2px;
652 652 cursor:pointer;
653 653 font-size: 90%;
654 654 border: 1px solid #ccc;
655 655 border-left: 1px solid #ccc;
656 656 border-right: 1px solid #ccc;
657 657 background-color:white;
658 658 }
659 659 div.autocomplete ul li.selected { background-color: #ffb;}
660 660 div.autocomplete ul li span.informal {
661 661 font-size: 80%;
662 662 color: #aaa;
663 663 }
664 664
665 665 #parent_issue_candidates ul li {width: 500px;}
666 666
667 667 /***** Diff *****/
668 668 .diff_out { background: #fcc; }
669 669 .diff_in { background: #cfc; }
670 670
671 671 /***** Wiki *****/
672 672 div.wiki table {
673 673 border: 1px solid #505050;
674 674 border-collapse: collapse;
675 675 margin-bottom: 1em;
676 676 }
677 677
678 678 div.wiki table, div.wiki td, div.wiki th {
679 679 border: 1px solid #bbb;
680 680 padding: 4px;
681 681 }
682 682
683 683 div.wiki .external {
684 684 background-position: 0% 60%;
685 685 background-repeat: no-repeat;
686 686 padding-left: 12px;
687 687 background-image: url(../images/external.png);
688 688 }
689 689
690 690 div.wiki a.new {
691 691 color: #b73535;
692 692 }
693 693
694 694 div.wiki pre {
695 695 margin: 1em 1em 1em 1.6em;
696 696 padding: 2px 2px 2px 0;
697 697 background-color: #fafafa;
698 698 border: 1px solid #dadada;
699 699 width:auto;
700 700 overflow-x: auto;
701 701 overflow-y: hidden;
702 702 }
703 703
704 704 div.wiki ul.toc {
705 705 background-color: #ffffdd;
706 706 border: 1px solid #e4e4e4;
707 707 padding: 4px;
708 708 line-height: 1.2em;
709 709 margin-bottom: 12px;
710 710 margin-right: 12px;
711 711 margin-left: 0;
712 712 display: table
713 713 }
714 714 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
715 715
716 716 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
717 717 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
718 div.wiki ul.toc li { list-style-type:none;}
719 div.wiki ul.toc li.heading2 { margin-left: 6px; }
720 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
718 div.wiki ul.toc ul { margin: 0; padding: 0; }
719 div.wiki ul.toc li { list-style-type:none; margin: 0;}
720 div.wiki ul.toc li li { margin-left: 1.5em; }
721 div.wiki ul.toc li li li { font-size: 0.8em; }
721 722
722 723 div.wiki ul.toc a {
723 724 font-size: 0.9em;
724 725 font-weight: normal;
725 726 text-decoration: none;
726 727 color: #606060;
727 728 }
728 729 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
729 730
730 731 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
731 732 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
732 733 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
733 734
734 735 div.wiki img { vertical-align: middle; }
735 736
736 737 /***** My page layout *****/
737 738 .block-receiver {
738 739 border:1px dashed #c0c0c0;
739 740 margin-bottom: 20px;
740 741 padding: 15px 0 15px 0;
741 742 }
742 743
743 744 .mypage-box {
744 745 margin:0 0 20px 0;
745 746 color:#505050;
746 747 line-height:1.5em;
747 748 }
748 749
749 750 .handle {
750 751 cursor: move;
751 752 }
752 753
753 754 a.close-icon {
754 755 display:block;
755 756 margin-top:3px;
756 757 overflow:hidden;
757 758 width:12px;
758 759 height:12px;
759 760 background-repeat: no-repeat;
760 761 cursor:pointer;
761 762 background-image:url('../images/close.png');
762 763 }
763 764
764 765 a.close-icon:hover {
765 766 background-image:url('../images/close_hl.png');
766 767 }
767 768
768 769 /***** Gantt chart *****/
769 770 .gantt_hdr {
770 771 position:absolute;
771 772 top:0;
772 773 height:16px;
773 774 border-top: 1px solid #c0c0c0;
774 775 border-bottom: 1px solid #c0c0c0;
775 776 border-right: 1px solid #c0c0c0;
776 777 text-align: center;
777 778 overflow: hidden;
778 779 }
779 780
780 781 .task {
781 782 position: absolute;
782 783 height:8px;
783 784 font-size:0.8em;
784 785 color:#888;
785 786 padding:0;
786 787 margin:0;
787 788 line-height:0.8em;
788 789 white-space:nowrap;
789 790 }
790 791
791 792 .task.label {width:100%;}
792 793
793 794 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
794 795 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
795 796 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
796 797
797 798 .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
798 799 .task_late.parent, .task_done.parent { height: 3px;}
799 800 .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;}
800 801 .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;}
801 802
802 803 .milestone { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; }
803 804 .milestone_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
804 805 .milestone_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
805 806 .milestone_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
806 807 .project-line { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; }
807 808 .project_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
808 809 .project_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
809 810 .project_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
810 811
811 812 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
812 813 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
813 814
814 815 /***** Icons *****/
815 816 .icon {
816 817 background-position: 0% 50%;
817 818 background-repeat: no-repeat;
818 819 padding-left: 20px;
819 820 padding-top: 2px;
820 821 padding-bottom: 3px;
821 822 }
822 823
823 824 .icon-add { background-image: url(../images/add.png); }
824 825 .icon-edit { background-image: url(../images/edit.png); }
825 826 .icon-copy { background-image: url(../images/copy.png); }
826 827 .icon-duplicate { background-image: url(../images/duplicate.png); }
827 828 .icon-del { background-image: url(../images/delete.png); }
828 829 .icon-move { background-image: url(../images/move.png); }
829 830 .icon-save { background-image: url(../images/save.png); }
830 831 .icon-cancel { background-image: url(../images/cancel.png); }
831 832 .icon-multiple { background-image: url(../images/table_multiple.png); }
832 833 .icon-folder { background-image: url(../images/folder.png); }
833 834 .open .icon-folder { background-image: url(../images/folder_open.png); }
834 835 .icon-package { background-image: url(../images/package.png); }
835 836 .icon-home { background-image: url(../images/home.png); }
836 837 .icon-user { background-image: url(../images/user.png); }
837 838 .icon-projects { background-image: url(../images/projects.png); }
838 839 .icon-help { background-image: url(../images/help.png); }
839 840 .icon-attachment { background-image: url(../images/attachment.png); }
840 841 .icon-history { background-image: url(../images/history.png); }
841 842 .icon-time { background-image: url(../images/time.png); }
842 843 .icon-time-add { background-image: url(../images/time_add.png); }
843 844 .icon-stats { background-image: url(../images/stats.png); }
844 845 .icon-warning { background-image: url(../images/warning.png); }
845 846 .icon-fav { background-image: url(../images/fav.png); }
846 847 .icon-fav-off { background-image: url(../images/fav_off.png); }
847 848 .icon-reload { background-image: url(../images/reload.png); }
848 849 .icon-lock { background-image: url(../images/locked.png); }
849 850 .icon-unlock { background-image: url(../images/unlock.png); }
850 851 .icon-checked { background-image: url(../images/true.png); }
851 852 .icon-details { background-image: url(../images/zoom_in.png); }
852 853 .icon-report { background-image: url(../images/report.png); }
853 854 .icon-comment { background-image: url(../images/comment.png); }
854 855 .icon-summary { background-image: url(../images/lightning.png); }
855 856 .icon-server-authentication { background-image: url(../images/server_key.png); }
856 857 .icon-issue { background-image: url(../images/ticket.png); }
857 858 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
858 859 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
859 860
860 861 .icon-file { background-image: url(../images/files/default.png); }
861 862 .icon-file.text-plain { background-image: url(../images/files/text.png); }
862 863 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
863 864 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
864 865 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
865 866 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
866 867 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
867 868 .icon-file.image-gif { background-image: url(../images/files/image.png); }
868 869 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
869 870 .icon-file.image-png { background-image: url(../images/files/image.png); }
870 871 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
871 872 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
872 873 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
873 874 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
874 875
875 876 img.gravatar {
876 877 padding: 2px;
877 878 border: solid 1px #d5d5d5;
878 879 background: #fff;
879 880 }
880 881
881 882 div.issue img.gravatar {
882 883 float: right;
883 884 margin: 0 0 0 1em;
884 885 padding: 5px;
885 886 }
886 887
887 888 div.issue table img.gravatar {
888 889 height: 14px;
889 890 width: 14px;
890 891 padding: 2px;
891 892 float: left;
892 893 margin: 0 0.5em 0 0;
893 894 }
894 895
895 896 h2 img.gravatar {
896 897 padding: 3px;
897 898 margin: -2px 4px -4px 0;
898 899 vertical-align: top;
899 900 }
900 901
901 902 h4 img.gravatar {
902 903 padding: 3px;
903 904 margin: -6px 0 -4px 0;
904 905 vertical-align: top;
905 906 }
906 907
907 908 td.username img.gravatar {
908 909 float: left;
909 910 margin: 0 1em 0 0;
910 911 }
911 912
912 913 #activity dt img.gravatar {
913 914 float: left;
914 915 margin: 0 1em 1em 0;
915 916 }
916 917
917 918 /* Used on 12px Gravatar img tags without the icon background */
918 919 .icon-gravatar {
919 920 float: left;
920 921 margin-right: 4px;
921 922 }
922 923
923 924 #activity dt,
924 925 .journal {
925 926 clear: left;
926 927 }
927 928
928 929 .journal-link {
929 930 float: right;
930 931 }
931 932
932 933 h2 img { vertical-align:middle; }
933 934
934 935 .hascontextmenu { cursor: context-menu; }
935 936
936 937 /***** Media print specific styles *****/
937 938 @media print {
938 939 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
939 940 #main { background: #fff; }
940 941 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
941 942 #wiki_add_attachment { display:none; }
942 943 .hide-when-print { display: none; }
943 944 }
@@ -1,639 +1,652
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 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 < 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'), :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_attachment_links
229 229 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
230 230 to_test = {
231 231 'attachment:error281.txt' => attachment_link
232 232 }
233 233 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
234 234 end
235 235
236 236 def test_wiki_links
237 237 to_test = {
238 238 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
239 239 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
240 240 # link with anchor
241 241 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
242 242 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
243 243 # page that doesn't exist
244 244 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
245 245 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
246 246 # link to another project wiki
247 247 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
248 248 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
249 249 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
250 250 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
251 251 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
252 252 # striked through link
253 253 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
254 254 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
255 255 # escaping
256 256 '![[Another page|Page]]' => '[[Another page|Page]]',
257 257 # project does not exist
258 258 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
259 259 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
260 260 }
261 261 @project = Project.find(1)
262 262 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
263 263 end
264 264
265 265 def test_html_tags
266 266 to_test = {
267 267 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
268 268 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
269 269 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
270 270 # do not escape pre/code tags
271 271 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
272 272 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
273 273 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
274 274 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
275 275 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
276 276 # remove attributes except class
277 277 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
278 278 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
279 279 }
280 280 to_test.each { |text, result| assert_equal result, textilizable(text) }
281 281 end
282 282
283 283 def test_allowed_html_tags
284 284 to_test = {
285 285 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
286 286 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
287 287 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
288 288 }
289 289 to_test.each { |text, result| assert_equal result, textilizable(text) }
290 290 end
291 291
292 292 def test_pre_tags
293 293 raw = <<-RAW
294 294 Before
295 295
296 296 <pre>
297 297 <prepared-statement-cache-size>32</prepared-statement-cache-size>
298 298 </pre>
299 299
300 300 After
301 301 RAW
302 302
303 303 expected = <<-EXPECTED
304 304 <p>Before</p>
305 305 <pre>
306 306 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
307 307 </pre>
308 308 <p>After</p>
309 309 EXPECTED
310 310
311 311 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
312 312 end
313 313
314 314 def test_pre_content_should_not_parse_wiki_and_redmine_links
315 315 raw = <<-RAW
316 316 [[CookBook documentation]]
317 317
318 318 #1
319 319
320 320 <pre>
321 321 [[CookBook documentation]]
322 322
323 323 #1
324 324 </pre>
325 325 RAW
326 326
327 327 expected = <<-EXPECTED
328 328 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
329 329 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
330 330 <pre>
331 331 [[CookBook documentation]]
332 332
333 333 #1
334 334 </pre>
335 335 EXPECTED
336 336
337 337 @project = Project.find(1)
338 338 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
339 339 end
340 340
341 341 def test_non_closing_pre_blocks_should_be_closed
342 342 raw = <<-RAW
343 343 <pre><code>
344 344 RAW
345 345
346 346 expected = <<-EXPECTED
347 347 <pre><code>
348 348 </code></pre>
349 349 EXPECTED
350 350
351 351 @project = Project.find(1)
352 352 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
353 353 end
354 354
355 355 def test_syntax_highlight
356 356 raw = <<-RAW
357 357 <pre><code class="ruby">
358 358 # Some ruby code here
359 359 </code></pre>
360 360 RAW
361 361
362 362 expected = <<-EXPECTED
363 363 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
364 364 </code></pre>
365 365 EXPECTED
366 366
367 367 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
368 368 end
369 369
370 370 def test_wiki_links_in_tables
371 371 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
372 372 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
373 373 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
374 374 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
375 375 }
376 376 @project = Project.find(1)
377 377 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
378 378 end
379 379
380 380 def test_text_formatting
381 381 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
382 382 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
383 383 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
384 384 '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>',
385 385 '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',
386 386 }
387 387 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
388 388 end
389 389
390 390 def test_wiki_horizontal_rule
391 391 assert_equal '<hr />', textilizable('---')
392 392 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
393 393 end
394 394
395 395 def test_footnotes
396 396 raw = <<-RAW
397 397 This is some text[1].
398 398
399 399 fn1. This is the foot note
400 400 RAW
401 401
402 402 expected = <<-EXPECTED
403 403 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
404 404 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
405 405 EXPECTED
406 406
407 407 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
408 408 end
409 409
410 410 def test_table_of_content
411 411 raw = <<-RAW
412 412 {{toc}}
413 413
414 414 h1. Title
415 415
416 416 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
417 417
418 418 h2. Subtitle with a [[Wiki]] link
419 419
420 420 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
421 421
422 422 h2. Subtitle with [[Wiki|another Wiki]] link
423 423
424 424 h2. Subtitle with %{color:red}red text%
425 425
426 h2. Subtitle with *some* _modifiers_
426 h3. Subtitle with *some* _modifiers_
427 427
428 428 h1. Another title
429 429
430 h2. An "Internet link":http://www.redmine.org/ inside subtitle
430 h3. An "Internet link":http://www.redmine.org/ inside subtitle
431 431
432 432 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
433 433
434 434 RAW
435 435
436 expected = '<ul class="toc">' +
437 '<li class="heading1"><a href="#Title">Title</a></li>' +
438 '<li class="heading2"><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
439 '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
440 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
441 '<li class="heading2"><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
442 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
443 '<li class="heading2"><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
444 '<li class="heading2"><a href="#Project-Name">Project Name</a></li>' +
436 expected = '<ul class="toc">' +
437 '<li><a href="#Title">Title</a>' +
438 '<ul>' +
439 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
440 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
441 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
442 '<ul>' +
443 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
444 '</ul>' +
445 '</li>' +
446 '</ul>' +
447 '</li>' +
448 '<li><a href="#Another-title">Another title</a>' +
449 '<ul>' +
450 '<li>' +
451 '<ul>' +
452 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
453 '</ul>' +
454 '</li>' +
455 '<li><a href="#Project-Name">Project Name</a></li>' +
456 '</ul>' +
457 '</li>' +
445 458 '</ul>'
446 459
447 460 @project = Project.find(1)
448 461 assert textilizable(raw).gsub("\n", "").include?(expected)
449 462 end
450 463
451 464 def test_table_of_content_should_contain_included_page_headings
452 465 raw = <<-RAW
453 466 {{toc}}
454 467
455 468 h1. Included
456 469
457 470 {{include(Child_1)}}
458 471 RAW
459 472
460 473 expected = '<ul class="toc">' +
461 '<li class="heading1"><a href="#Included">Included</a></li>' +
462 '<li class="heading1"><a href="#Child-page-1">Child page 1</a></li>' +
474 '<li><a href="#Included">Included</a></li>' +
475 '<li><a href="#Child-page-1">Child page 1</a></li>' +
463 476 '</ul>'
464 477
465 478 @project = Project.find(1)
466 479 assert textilizable(raw).gsub("\n", "").include?(expected)
467 480 end
468 481
469 482 def test_blockquote
470 483 # orig raw text
471 484 raw = <<-RAW
472 485 John said:
473 486 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
474 487 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
475 488 > * Donec odio lorem,
476 489 > * sagittis ac,
477 490 > * malesuada in,
478 491 > * adipiscing eu, dolor.
479 492 >
480 493 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
481 494 > Proin a tellus. Nam vel neque.
482 495
483 496 He's right.
484 497 RAW
485 498
486 499 # expected html
487 500 expected = <<-EXPECTED
488 501 <p>John said:</p>
489 502 <blockquote>
490 503 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
491 504 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
492 505 <ul>
493 506 <li>Donec odio lorem,</li>
494 507 <li>sagittis ac,</li>
495 508 <li>malesuada in,</li>
496 509 <li>adipiscing eu, dolor.</li>
497 510 </ul>
498 511 <blockquote>
499 512 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
500 513 </blockquote>
501 514 <p>Proin a tellus. Nam vel neque.</p>
502 515 </blockquote>
503 516 <p>He's right.</p>
504 517 EXPECTED
505 518
506 519 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
507 520 end
508 521
509 522 def test_table
510 523 raw = <<-RAW
511 524 This is a table with empty cells:
512 525
513 526 |cell11|cell12||
514 527 |cell21||cell23|
515 528 |cell31|cell32|cell33|
516 529 RAW
517 530
518 531 expected = <<-EXPECTED
519 532 <p>This is a table with empty cells:</p>
520 533
521 534 <table>
522 535 <tr><td>cell11</td><td>cell12</td><td></td></tr>
523 536 <tr><td>cell21</td><td></td><td>cell23</td></tr>
524 537 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
525 538 </table>
526 539 EXPECTED
527 540
528 541 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
529 542 end
530 543
531 544 def test_table_with_line_breaks
532 545 raw = <<-RAW
533 546 This is a table with line breaks:
534 547
535 548 |cell11
536 549 continued|cell12||
537 550 |-cell21-||cell23
538 551 cell23 line2
539 552 cell23 *line3*|
540 553 |cell31|cell32
541 554 cell32 line2|cell33|
542 555
543 556 RAW
544 557
545 558 expected = <<-EXPECTED
546 559 <p>This is a table with line breaks:</p>
547 560
548 561 <table>
549 562 <tr>
550 563 <td>cell11<br />continued</td>
551 564 <td>cell12</td>
552 565 <td></td>
553 566 </tr>
554 567 <tr>
555 568 <td><del>cell21</del></td>
556 569 <td></td>
557 570 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
558 571 </tr>
559 572 <tr>
560 573 <td>cell31</td>
561 574 <td>cell32<br/>cell32 line2</td>
562 575 <td>cell33</td>
563 576 </tr>
564 577 </table>
565 578 EXPECTED
566 579
567 580 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
568 581 end
569 582
570 583 def test_textile_should_not_mangle_brackets
571 584 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
572 585 end
573 586
574 587 def test_default_formatter
575 588 Setting.text_formatting = 'unknown'
576 589 text = 'a *link*: http://www.example.net/'
577 590 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
578 591 Setting.text_formatting = 'textile'
579 592 end
580 593
581 594 def test_due_date_distance_in_words
582 595 to_test = { Date.today => 'Due in 0 days',
583 596 Date.today + 1 => 'Due in 1 day',
584 597 Date.today + 100 => 'Due in about 3 months',
585 598 Date.today + 20000 => 'Due in over 54 years',
586 599 Date.today - 1 => '1 day late',
587 600 Date.today - 100 => 'about 3 months late',
588 601 Date.today - 20000 => 'over 54 years late',
589 602 }
590 603 to_test.each do |date, expected|
591 604 assert_equal expected, due_date_distance_in_words(date)
592 605 end
593 606 end
594 607
595 608 def test_avatar
596 609 # turn on avatars
597 610 Setting.gravatar_enabled = '1'
598 611 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
599 612 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
600 613 assert_nil avatar('jsmith')
601 614 assert_nil avatar(nil)
602 615
603 616 # turn off avatars
604 617 Setting.gravatar_enabled = '0'
605 618 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
606 619 end
607 620
608 621 def test_link_to_user
609 622 user = User.find(2)
610 623 t = link_to_user(user)
611 624 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
612 625 end
613 626
614 627 def test_link_to_user_should_not_link_to_locked_user
615 628 user = User.find(5)
616 629 assert user.locked?
617 630 t = link_to_user(user)
618 631 assert_equal user.name, t
619 632 end
620 633
621 634 def test_link_to_user_should_not_link_to_anonymous
622 635 user = User.anonymous
623 636 assert user.anonymous?
624 637 t = link_to_user(user)
625 638 assert_equal ::I18n.t(:label_user_anonymous), t
626 639 end
627 640
628 641 def test_link_to_project
629 642 project = Project.find(1)
630 643 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
631 644 link_to_project(project)
632 645 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
633 646 link_to_project(project, :action => 'settings')
634 647 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
635 648 link_to_project(project, {:only_path => false, :jump => 'blah'})
636 649 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
637 650 link_to_project(project, {:action => 'settings'}, :class => "project")
638 651 end
639 652 end
General Comments 0
You need to be logged in to leave comments. Login now