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