##// END OF EJS Templates
Adds links to locked users when current user is admin....
Jean-Philippe Lang -
r10462:7729178d9d50
parent child
Show More
@@ -1,1275 +1,1275
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Displays a link to user's account page if active
46 # Displays a link to user's account page if active
47 def link_to_user(user, options={})
47 def link_to_user(user, options={})
48 if user.is_a?(User)
48 if user.is_a?(User)
49 name = h(user.name(options[:format]))
49 name = h(user.name(options[:format]))
50 if user.active?
50 if user.active? || (User.current.admin? && user.logged?)
51 link_to name, :controller => 'users', :action => 'show', :id => user
51 link_to name, {:controller => 'users', :action => 'show', :id => user}, :class => user.css_classes
52 else
52 else
53 name
53 name
54 end
54 end
55 else
55 else
56 h(user.to_s)
56 h(user.to_s)
57 end
57 end
58 end
58 end
59
59
60 # Displays a link to +issue+ with its subject.
60 # Displays a link to +issue+ with its subject.
61 # Examples:
61 # Examples:
62 #
62 #
63 # link_to_issue(issue) # => Defect #6: This is the subject
63 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :subject => false) # => Defect #6
65 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
67 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 #
68 #
69 def link_to_issue(issue, options={})
69 def link_to_issue(issue, options={})
70 title = nil
70 title = nil
71 subject = nil
71 subject = nil
72 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
72 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 if options[:subject] == false
73 if options[:subject] == false
74 title = truncate(issue.subject, :length => 60)
74 title = truncate(issue.subject, :length => 60)
75 else
75 else
76 subject = issue.subject
76 subject = issue.subject
77 if options[:truncate]
77 if options[:truncate]
78 subject = truncate(subject, :length => options[:truncate])
78 subject = truncate(subject, :length => options[:truncate])
79 end
79 end
80 end
80 end
81 s = link_to text, {:controller => "issues", :action => "show", :id => issue},
81 s = link_to text, {:controller => "issues", :action => "show", :id => issue},
82 :class => issue.css_classes,
82 :class => issue.css_classes,
83 :title => title
83 :title => title
84 s << h(": #{subject}") if subject
84 s << h(": #{subject}") if subject
85 s = h("#{issue.project} - ") + s if options[:project]
85 s = h("#{issue.project} - ") + s if options[:project]
86 s
86 s
87 end
87 end
88
88
89 # Generates a link to an attachment.
89 # Generates a link to an attachment.
90 # Options:
90 # Options:
91 # * :text - Link text (default to attachment filename)
91 # * :text - Link text (default to attachment filename)
92 # * :download - Force download (default: false)
92 # * :download - Force download (default: false)
93 def link_to_attachment(attachment, options={})
93 def link_to_attachment(attachment, options={})
94 text = options.delete(:text) || attachment.filename
94 text = options.delete(:text) || attachment.filename
95 action = options.delete(:download) ? 'download' : 'show'
95 action = options.delete(:download) ? 'download' : 'show'
96 opt_only_path = {}
96 opt_only_path = {}
97 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
97 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
98 options.delete(:only_path)
98 options.delete(:only_path)
99 link_to(h(text),
99 link_to(h(text),
100 {:controller => 'attachments', :action => action,
100 {:controller => 'attachments', :action => action,
101 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
101 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
102 options)
102 options)
103 end
103 end
104
104
105 # Generates a link to a SCM revision
105 # Generates a link to a SCM revision
106 # Options:
106 # Options:
107 # * :text - Link text (default to the formatted revision)
107 # * :text - Link text (default to the formatted revision)
108 def link_to_revision(revision, repository, options={})
108 def link_to_revision(revision, repository, options={})
109 if repository.is_a?(Project)
109 if repository.is_a?(Project)
110 repository = repository.repository
110 repository = repository.repository
111 end
111 end
112 text = options.delete(:text) || format_revision(revision)
112 text = options.delete(:text) || format_revision(revision)
113 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
114 link_to(
114 link_to(
115 h(text),
115 h(text),
116 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
117 :title => l(:label_revision_id, format_revision(revision))
117 :title => l(:label_revision_id, format_revision(revision))
118 )
118 )
119 end
119 end
120
120
121 # Generates a link to a message
121 # Generates a link to a message
122 def link_to_message(message, options={}, html_options = nil)
122 def link_to_message(message, options={}, html_options = nil)
123 link_to(
123 link_to(
124 h(truncate(message.subject, :length => 60)),
124 h(truncate(message.subject, :length => 60)),
125 { :controller => 'messages', :action => 'show',
125 { :controller => 'messages', :action => 'show',
126 :board_id => message.board_id,
126 :board_id => message.board_id,
127 :id => (message.parent_id || message.id),
127 :id => (message.parent_id || message.id),
128 :r => (message.parent_id && message.id),
128 :r => (message.parent_id && message.id),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
130 }.merge(options),
130 }.merge(options),
131 html_options
131 html_options
132 )
132 )
133 end
133 end
134
134
135 # Generates a link to a project if active
135 # Generates a link to a project if active
136 # Examples:
136 # Examples:
137 #
137 #
138 # link_to_project(project) # => link to the specified project overview
138 # link_to_project(project) # => link to the specified project overview
139 # link_to_project(project, :action=>'settings') # => link to project settings
139 # link_to_project(project, :action=>'settings') # => link to project settings
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 #
142 #
143 def link_to_project(project, options={}, html_options = nil)
143 def link_to_project(project, options={}, html_options = nil)
144 if project.archived?
144 if project.archived?
145 h(project)
145 h(project)
146 else
146 else
147 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
148 link_to(h(project), url, html_options)
148 link_to(h(project), url, html_options)
149 end
149 end
150 end
150 end
151
151
152 def thumbnail_tag(attachment)
152 def thumbnail_tag(attachment)
153 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
153 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
154 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
154 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
155 :title => attachment.filename
155 :title => attachment.filename
156 end
156 end
157
157
158 def toggle_link(name, id, options={})
158 def toggle_link(name, id, options={})
159 onclick = "$('##{id}').toggle(); "
159 onclick = "$('##{id}').toggle(); "
160 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
160 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
161 onclick << "return false;"
161 onclick << "return false;"
162 link_to(name, "#", :onclick => onclick)
162 link_to(name, "#", :onclick => onclick)
163 end
163 end
164
164
165 def image_to_function(name, function, html_options = {})
165 def image_to_function(name, function, html_options = {})
166 html_options.symbolize_keys!
166 html_options.symbolize_keys!
167 tag(:input, html_options.merge({
167 tag(:input, html_options.merge({
168 :type => "image", :src => image_path(name),
168 :type => "image", :src => image_path(name),
169 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
169 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
170 }))
170 }))
171 end
171 end
172
172
173 def format_activity_title(text)
173 def format_activity_title(text)
174 h(truncate_single_line(text, :length => 100))
174 h(truncate_single_line(text, :length => 100))
175 end
175 end
176
176
177 def format_activity_day(date)
177 def format_activity_day(date)
178 date == User.current.today ? l(:label_today).titleize : format_date(date)
178 date == User.current.today ? l(:label_today).titleize : format_date(date)
179 end
179 end
180
180
181 def format_activity_description(text)
181 def format_activity_description(text)
182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
183 ).gsub(/[\r\n]+/, "<br />").html_safe
183 ).gsub(/[\r\n]+/, "<br />").html_safe
184 end
184 end
185
185
186 def format_version_name(version)
186 def format_version_name(version)
187 if version.project == @project
187 if version.project == @project
188 h(version)
188 h(version)
189 else
189 else
190 h("#{version.project} - #{version}")
190 h("#{version.project} - #{version}")
191 end
191 end
192 end
192 end
193
193
194 def due_date_distance_in_words(date)
194 def due_date_distance_in_words(date)
195 if date
195 if date
196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
197 end
197 end
198 end
198 end
199
199
200 # Renders a tree of projects as a nested set of unordered lists
200 # Renders a tree of projects as a nested set of unordered lists
201 # The given collection may be a subset of the whole project tree
201 # The given collection may be a subset of the whole project tree
202 # (eg. some intermediate nodes are private and can not be seen)
202 # (eg. some intermediate nodes are private and can not be seen)
203 def render_project_nested_lists(projects)
203 def render_project_nested_lists(projects)
204 s = ''
204 s = ''
205 if projects.any?
205 if projects.any?
206 ancestors = []
206 ancestors = []
207 original_project = @project
207 original_project = @project
208 projects.sort_by(&:lft).each do |project|
208 projects.sort_by(&:lft).each do |project|
209 # set the project environment to please macros.
209 # set the project environment to please macros.
210 @project = project
210 @project = project
211 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
211 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
212 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
212 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
213 else
213 else
214 ancestors.pop
214 ancestors.pop
215 s << "</li>"
215 s << "</li>"
216 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
216 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
217 ancestors.pop
217 ancestors.pop
218 s << "</ul></li>\n"
218 s << "</ul></li>\n"
219 end
219 end
220 end
220 end
221 classes = (ancestors.empty? ? 'root' : 'child')
221 classes = (ancestors.empty? ? 'root' : 'child')
222 s << "<li class='#{classes}'><div class='#{classes}'>"
222 s << "<li class='#{classes}'><div class='#{classes}'>"
223 s << h(block_given? ? yield(project) : project.name)
223 s << h(block_given? ? yield(project) : project.name)
224 s << "</div>\n"
224 s << "</div>\n"
225 ancestors << project
225 ancestors << project
226 end
226 end
227 s << ("</li></ul>\n" * ancestors.size)
227 s << ("</li></ul>\n" * ancestors.size)
228 @project = original_project
228 @project = original_project
229 end
229 end
230 s.html_safe
230 s.html_safe
231 end
231 end
232
232
233 def render_page_hierarchy(pages, node=nil, options={})
233 def render_page_hierarchy(pages, node=nil, options={})
234 content = ''
234 content = ''
235 if pages[node]
235 if pages[node]
236 content << "<ul class=\"pages-hierarchy\">\n"
236 content << "<ul class=\"pages-hierarchy\">\n"
237 pages[node].each do |page|
237 pages[node].each do |page|
238 content << "<li>"
238 content << "<li>"
239 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
239 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
240 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
240 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
241 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
241 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
242 content << "</li>\n"
242 content << "</li>\n"
243 end
243 end
244 content << "</ul>\n"
244 content << "</ul>\n"
245 end
245 end
246 content.html_safe
246 content.html_safe
247 end
247 end
248
248
249 # Renders flash messages
249 # Renders flash messages
250 def render_flash_messages
250 def render_flash_messages
251 s = ''
251 s = ''
252 flash.each do |k,v|
252 flash.each do |k,v|
253 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
253 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
254 end
254 end
255 s.html_safe
255 s.html_safe
256 end
256 end
257
257
258 # Renders tabs and their content
258 # Renders tabs and their content
259 def render_tabs(tabs)
259 def render_tabs(tabs)
260 if tabs.any?
260 if tabs.any?
261 render :partial => 'common/tabs', :locals => {:tabs => tabs}
261 render :partial => 'common/tabs', :locals => {:tabs => tabs}
262 else
262 else
263 content_tag 'p', l(:label_no_data), :class => "nodata"
263 content_tag 'p', l(:label_no_data), :class => "nodata"
264 end
264 end
265 end
265 end
266
266
267 # Renders the project quick-jump box
267 # Renders the project quick-jump box
268 def render_project_jump_box
268 def render_project_jump_box
269 return unless User.current.logged?
269 return unless User.current.logged?
270 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
270 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
271 if projects.any?
271 if projects.any?
272 options =
272 options =
273 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
273 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
274 '<option value="" disabled="disabled">---</option>').html_safe
274 '<option value="" disabled="disabled">---</option>').html_safe
275
275
276 options << project_tree_options_for_select(projects, :selected => @project) do |p|
276 options << project_tree_options_for_select(projects, :selected => @project) do |p|
277 { :value => project_path(:id => p, :jump => current_menu_item) }
277 { :value => project_path(:id => p, :jump => current_menu_item) }
278 end
278 end
279
279
280 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
280 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
281 end
281 end
282 end
282 end
283
283
284 def project_tree_options_for_select(projects, options = {})
284 def project_tree_options_for_select(projects, options = {})
285 s = ''
285 s = ''
286 project_tree(projects) do |project, level|
286 project_tree(projects) do |project, level|
287 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
287 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
288 tag_options = {:value => project.id}
288 tag_options = {:value => project.id}
289 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
289 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
290 tag_options[:selected] = 'selected'
290 tag_options[:selected] = 'selected'
291 else
291 else
292 tag_options[:selected] = nil
292 tag_options[:selected] = nil
293 end
293 end
294 tag_options.merge!(yield(project)) if block_given?
294 tag_options.merge!(yield(project)) if block_given?
295 s << content_tag('option', name_prefix + h(project), tag_options)
295 s << content_tag('option', name_prefix + h(project), tag_options)
296 end
296 end
297 s.html_safe
297 s.html_safe
298 end
298 end
299
299
300 # Yields the given block for each project with its level in the tree
300 # Yields the given block for each project with its level in the tree
301 #
301 #
302 # Wrapper for Project#project_tree
302 # Wrapper for Project#project_tree
303 def project_tree(projects, &block)
303 def project_tree(projects, &block)
304 Project.project_tree(projects, &block)
304 Project.project_tree(projects, &block)
305 end
305 end
306
306
307 def principals_check_box_tags(name, principals)
307 def principals_check_box_tags(name, principals)
308 s = ''
308 s = ''
309 principals.sort.each do |principal|
309 principals.sort.each do |principal|
310 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
310 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
311 end
311 end
312 s.html_safe
312 s.html_safe
313 end
313 end
314
314
315 # Returns a string for users/groups option tags
315 # Returns a string for users/groups option tags
316 def principals_options_for_select(collection, selected=nil)
316 def principals_options_for_select(collection, selected=nil)
317 s = ''
317 s = ''
318 if collection.include?(User.current)
318 if collection.include?(User.current)
319 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
319 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
320 end
320 end
321 groups = ''
321 groups = ''
322 collection.sort.each do |element|
322 collection.sort.each do |element|
323 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
323 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
324 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
324 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
325 end
325 end
326 unless groups.empty?
326 unless groups.empty?
327 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
327 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
328 end
328 end
329 s.html_safe
329 s.html_safe
330 end
330 end
331
331
332 # Truncates and returns the string as a single line
332 # Truncates and returns the string as a single line
333 def truncate_single_line(string, *args)
333 def truncate_single_line(string, *args)
334 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
334 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
335 end
335 end
336
336
337 # Truncates at line break after 250 characters or options[:length]
337 # Truncates at line break after 250 characters or options[:length]
338 def truncate_lines(string, options={})
338 def truncate_lines(string, options={})
339 length = options[:length] || 250
339 length = options[:length] || 250
340 if string.to_s =~ /\A(.{#{length}}.*?)$/m
340 if string.to_s =~ /\A(.{#{length}}.*?)$/m
341 "#{$1}..."
341 "#{$1}..."
342 else
342 else
343 string
343 string
344 end
344 end
345 end
345 end
346
346
347 def anchor(text)
347 def anchor(text)
348 text.to_s.gsub(' ', '_')
348 text.to_s.gsub(' ', '_')
349 end
349 end
350
350
351 def html_hours(text)
351 def html_hours(text)
352 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
352 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
353 end
353 end
354
354
355 def authoring(created, author, options={})
355 def authoring(created, author, options={})
356 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
356 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
357 end
357 end
358
358
359 def time_tag(time)
359 def time_tag(time)
360 text = distance_of_time_in_words(Time.now, time)
360 text = distance_of_time_in_words(Time.now, time)
361 if @project
361 if @project
362 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
362 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
363 else
363 else
364 content_tag('acronym', text, :title => format_time(time))
364 content_tag('acronym', text, :title => format_time(time))
365 end
365 end
366 end
366 end
367
367
368 def syntax_highlight_lines(name, content)
368 def syntax_highlight_lines(name, content)
369 lines = []
369 lines = []
370 syntax_highlight(name, content).each_line { |line| lines << line }
370 syntax_highlight(name, content).each_line { |line| lines << line }
371 lines
371 lines
372 end
372 end
373
373
374 def syntax_highlight(name, content)
374 def syntax_highlight(name, content)
375 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
375 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
376 end
376 end
377
377
378 def to_path_param(path)
378 def to_path_param(path)
379 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
379 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
380 str.blank? ? nil : str
380 str.blank? ? nil : str
381 end
381 end
382
382
383 def pagination_links_full(paginator, count=nil, options={})
383 def pagination_links_full(paginator, count=nil, options={})
384 page_param = options.delete(:page_param) || :page
384 page_param = options.delete(:page_param) || :page
385 per_page_links = options.delete(:per_page_links)
385 per_page_links = options.delete(:per_page_links)
386 url_param = params.dup
386 url_param = params.dup
387
387
388 html = ''
388 html = ''
389 if paginator.current.previous
389 if paginator.current.previous
390 # \xc2\xab(utf-8) = &#171;
390 # \xc2\xab(utf-8) = &#171;
391 html << link_to_content_update(
391 html << link_to_content_update(
392 "\xc2\xab " + l(:label_previous),
392 "\xc2\xab " + l(:label_previous),
393 url_param.merge(page_param => paginator.current.previous)) + ' '
393 url_param.merge(page_param => paginator.current.previous)) + ' '
394 end
394 end
395
395
396 html << (pagination_links_each(paginator, options) do |n|
396 html << (pagination_links_each(paginator, options) do |n|
397 link_to_content_update(n.to_s, url_param.merge(page_param => n))
397 link_to_content_update(n.to_s, url_param.merge(page_param => n))
398 end || '')
398 end || '')
399
399
400 if paginator.current.next
400 if paginator.current.next
401 # \xc2\xbb(utf-8) = &#187;
401 # \xc2\xbb(utf-8) = &#187;
402 html << ' ' + link_to_content_update(
402 html << ' ' + link_to_content_update(
403 (l(:label_next) + " \xc2\xbb"),
403 (l(:label_next) + " \xc2\xbb"),
404 url_param.merge(page_param => paginator.current.next))
404 url_param.merge(page_param => paginator.current.next))
405 end
405 end
406
406
407 unless count.nil?
407 unless count.nil?
408 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
408 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
409 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
409 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
410 html << " | #{links}"
410 html << " | #{links}"
411 end
411 end
412 end
412 end
413
413
414 html.html_safe
414 html.html_safe
415 end
415 end
416
416
417 def per_page_links(selected=nil, item_count=nil)
417 def per_page_links(selected=nil, item_count=nil)
418 values = Setting.per_page_options_array
418 values = Setting.per_page_options_array
419 if item_count && values.any?
419 if item_count && values.any?
420 if item_count > values.first
420 if item_count > values.first
421 max = values.detect {|value| value >= item_count} || item_count
421 max = values.detect {|value| value >= item_count} || item_count
422 else
422 else
423 max = item_count
423 max = item_count
424 end
424 end
425 values = values.select {|value| value <= max || value == selected}
425 values = values.select {|value| value <= max || value == selected}
426 end
426 end
427 if values.empty? || (values.size == 1 && values.first == selected)
427 if values.empty? || (values.size == 1 && values.first == selected)
428 return nil
428 return nil
429 end
429 end
430 links = values.collect do |n|
430 links = values.collect do |n|
431 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
431 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
432 end
432 end
433 l(:label_display_per_page, links.join(', '))
433 l(:label_display_per_page, links.join(', '))
434 end
434 end
435
435
436 def reorder_links(name, url, method = :post)
436 def reorder_links(name, url, method = :post)
437 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
437 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
438 url.merge({"#{name}[move_to]" => 'highest'}),
438 url.merge({"#{name}[move_to]" => 'highest'}),
439 :method => method, :title => l(:label_sort_highest)) +
439 :method => method, :title => l(:label_sort_highest)) +
440 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
440 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
441 url.merge({"#{name}[move_to]" => 'higher'}),
441 url.merge({"#{name}[move_to]" => 'higher'}),
442 :method => method, :title => l(:label_sort_higher)) +
442 :method => method, :title => l(:label_sort_higher)) +
443 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
443 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
444 url.merge({"#{name}[move_to]" => 'lower'}),
444 url.merge({"#{name}[move_to]" => 'lower'}),
445 :method => method, :title => l(:label_sort_lower)) +
445 :method => method, :title => l(:label_sort_lower)) +
446 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
446 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
447 url.merge({"#{name}[move_to]" => 'lowest'}),
447 url.merge({"#{name}[move_to]" => 'lowest'}),
448 :method => method, :title => l(:label_sort_lowest))
448 :method => method, :title => l(:label_sort_lowest))
449 end
449 end
450
450
451 def breadcrumb(*args)
451 def breadcrumb(*args)
452 elements = args.flatten
452 elements = args.flatten
453 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
453 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
454 end
454 end
455
455
456 def other_formats_links(&block)
456 def other_formats_links(&block)
457 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
457 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
458 yield Redmine::Views::OtherFormatsBuilder.new(self)
458 yield Redmine::Views::OtherFormatsBuilder.new(self)
459 concat('</p>'.html_safe)
459 concat('</p>'.html_safe)
460 end
460 end
461
461
462 def page_header_title
462 def page_header_title
463 if @project.nil? || @project.new_record?
463 if @project.nil? || @project.new_record?
464 h(Setting.app_title)
464 h(Setting.app_title)
465 else
465 else
466 b = []
466 b = []
467 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
467 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
468 if ancestors.any?
468 if ancestors.any?
469 root = ancestors.shift
469 root = ancestors.shift
470 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
470 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
471 if ancestors.size > 2
471 if ancestors.size > 2
472 b << "\xe2\x80\xa6"
472 b << "\xe2\x80\xa6"
473 ancestors = ancestors[-2, 2]
473 ancestors = ancestors[-2, 2]
474 end
474 end
475 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
475 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
476 end
476 end
477 b << h(@project)
477 b << h(@project)
478 b.join(" \xc2\xbb ").html_safe
478 b.join(" \xc2\xbb ").html_safe
479 end
479 end
480 end
480 end
481
481
482 def html_title(*args)
482 def html_title(*args)
483 if args.empty?
483 if args.empty?
484 title = @html_title || []
484 title = @html_title || []
485 title << @project.name if @project
485 title << @project.name if @project
486 title << Setting.app_title unless Setting.app_title == title.last
486 title << Setting.app_title unless Setting.app_title == title.last
487 title.select {|t| !t.blank? }.join(' - ')
487 title.select {|t| !t.blank? }.join(' - ')
488 else
488 else
489 @html_title ||= []
489 @html_title ||= []
490 @html_title += args
490 @html_title += args
491 end
491 end
492 end
492 end
493
493
494 # Returns the theme, controller name, and action as css classes for the
494 # Returns the theme, controller name, and action as css classes for the
495 # HTML body.
495 # HTML body.
496 def body_css_classes
496 def body_css_classes
497 css = []
497 css = []
498 if theme = Redmine::Themes.theme(Setting.ui_theme)
498 if theme = Redmine::Themes.theme(Setting.ui_theme)
499 css << 'theme-' + theme.name
499 css << 'theme-' + theme.name
500 end
500 end
501
501
502 css << 'controller-' + controller_name
502 css << 'controller-' + controller_name
503 css << 'action-' + action_name
503 css << 'action-' + action_name
504 css.join(' ')
504 css.join(' ')
505 end
505 end
506
506
507 def accesskey(s)
507 def accesskey(s)
508 Redmine::AccessKeys.key_for s
508 Redmine::AccessKeys.key_for s
509 end
509 end
510
510
511 # Formats text according to system settings.
511 # Formats text according to system settings.
512 # 2 ways to call this method:
512 # 2 ways to call this method:
513 # * with a String: textilizable(text, options)
513 # * with a String: textilizable(text, options)
514 # * with an object and one of its attribute: textilizable(issue, :description, options)
514 # * with an object and one of its attribute: textilizable(issue, :description, options)
515 def textilizable(*args)
515 def textilizable(*args)
516 options = args.last.is_a?(Hash) ? args.pop : {}
516 options = args.last.is_a?(Hash) ? args.pop : {}
517 case args.size
517 case args.size
518 when 1
518 when 1
519 obj = options[:object]
519 obj = options[:object]
520 text = args.shift
520 text = args.shift
521 when 2
521 when 2
522 obj = args.shift
522 obj = args.shift
523 attr = args.shift
523 attr = args.shift
524 text = obj.send(attr).to_s
524 text = obj.send(attr).to_s
525 else
525 else
526 raise ArgumentError, 'invalid arguments to textilizable'
526 raise ArgumentError, 'invalid arguments to textilizable'
527 end
527 end
528 return '' if text.blank?
528 return '' if text.blank?
529 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
529 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
530 only_path = options.delete(:only_path) == false ? false : true
530 only_path = options.delete(:only_path) == false ? false : true
531
531
532 text = text.dup
532 text = text.dup
533 macros = catch_macros(text)
533 macros = catch_macros(text)
534 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
534 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
535
535
536 @parsed_headings = []
536 @parsed_headings = []
537 @heading_anchors = {}
537 @heading_anchors = {}
538 @current_section = 0 if options[:edit_section_links]
538 @current_section = 0 if options[:edit_section_links]
539
539
540 parse_sections(text, project, obj, attr, only_path, options)
540 parse_sections(text, project, obj, attr, only_path, options)
541 text = parse_non_pre_blocks(text, obj, macros) do |text|
541 text = parse_non_pre_blocks(text, obj, macros) do |text|
542 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
542 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
543 send method_name, text, project, obj, attr, only_path, options
543 send method_name, text, project, obj, attr, only_path, options
544 end
544 end
545 end
545 end
546 parse_headings(text, project, obj, attr, only_path, options)
546 parse_headings(text, project, obj, attr, only_path, options)
547
547
548 if @parsed_headings.any?
548 if @parsed_headings.any?
549 replace_toc(text, @parsed_headings)
549 replace_toc(text, @parsed_headings)
550 end
550 end
551
551
552 text.html_safe
552 text.html_safe
553 end
553 end
554
554
555 def parse_non_pre_blocks(text, obj, macros)
555 def parse_non_pre_blocks(text, obj, macros)
556 s = StringScanner.new(text)
556 s = StringScanner.new(text)
557 tags = []
557 tags = []
558 parsed = ''
558 parsed = ''
559 while !s.eos?
559 while !s.eos?
560 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
560 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
561 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
561 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
562 if tags.empty?
562 if tags.empty?
563 yield text
563 yield text
564 inject_macros(text, obj, macros) if macros.any?
564 inject_macros(text, obj, macros) if macros.any?
565 else
565 else
566 inject_macros(text, obj, macros, false) if macros.any?
566 inject_macros(text, obj, macros, false) if macros.any?
567 end
567 end
568 parsed << text
568 parsed << text
569 if tag
569 if tag
570 if closing
570 if closing
571 if tags.last == tag.downcase
571 if tags.last == tag.downcase
572 tags.pop
572 tags.pop
573 end
573 end
574 else
574 else
575 tags << tag.downcase
575 tags << tag.downcase
576 end
576 end
577 parsed << full_tag
577 parsed << full_tag
578 end
578 end
579 end
579 end
580 # Close any non closing tags
580 # Close any non closing tags
581 while tag = tags.pop
581 while tag = tags.pop
582 parsed << "</#{tag}>"
582 parsed << "</#{tag}>"
583 end
583 end
584 parsed
584 parsed
585 end
585 end
586
586
587 def parse_inline_attachments(text, project, obj, attr, only_path, options)
587 def parse_inline_attachments(text, project, obj, attr, only_path, options)
588 # when using an image link, try to use an attachment, if possible
588 # when using an image link, try to use an attachment, if possible
589 if options[:attachments] || (obj && obj.respond_to?(:attachments))
589 if options[:attachments] || (obj && obj.respond_to?(:attachments))
590 attachments = options[:attachments] || obj.attachments
590 attachments = options[:attachments] || obj.attachments
591 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
591 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
592 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
592 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
593 # search for the picture in attachments
593 # search for the picture in attachments
594 if found = Attachment.latest_attach(attachments, filename)
594 if found = Attachment.latest_attach(attachments, filename)
595 image_url = url_for :only_path => only_path, :controller => 'attachments',
595 image_url = url_for :only_path => only_path, :controller => 'attachments',
596 :action => 'download', :id => found
596 :action => 'download', :id => found
597 desc = found.description.to_s.gsub('"', '')
597 desc = found.description.to_s.gsub('"', '')
598 if !desc.blank? && alttext.blank?
598 if !desc.blank? && alttext.blank?
599 alt = " title=\"#{desc}\" alt=\"#{desc}\""
599 alt = " title=\"#{desc}\" alt=\"#{desc}\""
600 end
600 end
601 "src=\"#{image_url}\"#{alt}"
601 "src=\"#{image_url}\"#{alt}"
602 else
602 else
603 m
603 m
604 end
604 end
605 end
605 end
606 end
606 end
607 end
607 end
608
608
609 # Wiki links
609 # Wiki links
610 #
610 #
611 # Examples:
611 # Examples:
612 # [[mypage]]
612 # [[mypage]]
613 # [[mypage|mytext]]
613 # [[mypage|mytext]]
614 # wiki links can refer other project wikis, using project name or identifier:
614 # wiki links can refer other project wikis, using project name or identifier:
615 # [[project:]] -> wiki starting page
615 # [[project:]] -> wiki starting page
616 # [[project:|mytext]]
616 # [[project:|mytext]]
617 # [[project:mypage]]
617 # [[project:mypage]]
618 # [[project:mypage|mytext]]
618 # [[project:mypage|mytext]]
619 def parse_wiki_links(text, project, obj, attr, only_path, options)
619 def parse_wiki_links(text, project, obj, attr, only_path, options)
620 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
620 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
621 link_project = project
621 link_project = project
622 esc, all, page, title = $1, $2, $3, $5
622 esc, all, page, title = $1, $2, $3, $5
623 if esc.nil?
623 if esc.nil?
624 if page =~ /^([^\:]+)\:(.*)$/
624 if page =~ /^([^\:]+)\:(.*)$/
625 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
625 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
626 page = $2
626 page = $2
627 title ||= $1 if page.blank?
627 title ||= $1 if page.blank?
628 end
628 end
629
629
630 if link_project && link_project.wiki
630 if link_project && link_project.wiki
631 # extract anchor
631 # extract anchor
632 anchor = nil
632 anchor = nil
633 if page =~ /^(.+?)\#(.+)$/
633 if page =~ /^(.+?)\#(.+)$/
634 page, anchor = $1, $2
634 page, anchor = $1, $2
635 end
635 end
636 anchor = sanitize_anchor_name(anchor) if anchor.present?
636 anchor = sanitize_anchor_name(anchor) if anchor.present?
637 # check if page exists
637 # check if page exists
638 wiki_page = link_project.wiki.find_page(page)
638 wiki_page = link_project.wiki.find_page(page)
639 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
639 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
640 "##{anchor}"
640 "##{anchor}"
641 else
641 else
642 case options[:wiki_links]
642 case options[:wiki_links]
643 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
643 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
644 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
644 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
645 else
645 else
646 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
646 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
647 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
647 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
648 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
648 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
649 :id => wiki_page_id, :anchor => anchor, :parent => parent)
649 :id => wiki_page_id, :anchor => anchor, :parent => parent)
650 end
650 end
651 end
651 end
652 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
652 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
653 else
653 else
654 # project or wiki doesn't exist
654 # project or wiki doesn't exist
655 all
655 all
656 end
656 end
657 else
657 else
658 all
658 all
659 end
659 end
660 end
660 end
661 end
661 end
662
662
663 # Redmine links
663 # Redmine links
664 #
664 #
665 # Examples:
665 # Examples:
666 # Issues:
666 # Issues:
667 # #52 -> Link to issue #52
667 # #52 -> Link to issue #52
668 # Changesets:
668 # Changesets:
669 # r52 -> Link to revision 52
669 # r52 -> Link to revision 52
670 # commit:a85130f -> Link to scmid starting with a85130f
670 # commit:a85130f -> Link to scmid starting with a85130f
671 # Documents:
671 # Documents:
672 # document#17 -> Link to document with id 17
672 # document#17 -> Link to document with id 17
673 # document:Greetings -> Link to the document with title "Greetings"
673 # document:Greetings -> Link to the document with title "Greetings"
674 # document:"Some document" -> Link to the document with title "Some document"
674 # document:"Some document" -> Link to the document with title "Some document"
675 # Versions:
675 # Versions:
676 # version#3 -> Link to version with id 3
676 # version#3 -> Link to version with id 3
677 # version:1.0.0 -> Link to version named "1.0.0"
677 # version:1.0.0 -> Link to version named "1.0.0"
678 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
678 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
679 # Attachments:
679 # Attachments:
680 # attachment:file.zip -> Link to the attachment of the current object named file.zip
680 # attachment:file.zip -> Link to the attachment of the current object named file.zip
681 # Source files:
681 # Source files:
682 # source:some/file -> Link to the file located at /some/file in the project's repository
682 # source:some/file -> Link to the file located at /some/file in the project's repository
683 # source:some/file@52 -> Link to the file's revision 52
683 # source:some/file@52 -> Link to the file's revision 52
684 # source:some/file#L120 -> Link to line 120 of the file
684 # source:some/file#L120 -> Link to line 120 of the file
685 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
685 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
686 # export:some/file -> Force the download of the file
686 # export:some/file -> Force the download of the file
687 # Forum messages:
687 # Forum messages:
688 # message#1218 -> Link to message with id 1218
688 # message#1218 -> Link to message with id 1218
689 #
689 #
690 # Links can refer other objects from other projects, using project identifier:
690 # Links can refer other objects from other projects, using project identifier:
691 # identifier:r52
691 # identifier:r52
692 # identifier:document:"Some document"
692 # identifier:document:"Some document"
693 # identifier:version:1.0.0
693 # identifier:version:1.0.0
694 # identifier:source:some/file
694 # identifier:source:some/file
695 def parse_redmine_links(text, project, obj, attr, only_path, options)
695 def parse_redmine_links(text, project, obj, attr, only_path, options)
696 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
696 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
697 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
697 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
698 link = nil
698 link = nil
699 if project_identifier
699 if project_identifier
700 project = Project.visible.find_by_identifier(project_identifier)
700 project = Project.visible.find_by_identifier(project_identifier)
701 end
701 end
702 if esc.nil?
702 if esc.nil?
703 if prefix.nil? && sep == 'r'
703 if prefix.nil? && sep == 'r'
704 if project
704 if project
705 repository = nil
705 repository = nil
706 if repo_identifier
706 if repo_identifier
707 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
707 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
708 else
708 else
709 repository = project.repository
709 repository = project.repository
710 end
710 end
711 # project.changesets.visible raises an SQL error because of a double join on repositories
711 # project.changesets.visible raises an SQL error because of a double join on repositories
712 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
712 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
713 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
713 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
714 :class => 'changeset',
714 :class => 'changeset',
715 :title => truncate_single_line(changeset.comments, :length => 100))
715 :title => truncate_single_line(changeset.comments, :length => 100))
716 end
716 end
717 end
717 end
718 elsif sep == '#'
718 elsif sep == '#'
719 oid = identifier.to_i
719 oid = identifier.to_i
720 case prefix
720 case prefix
721 when nil
721 when nil
722 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
722 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
723 anchor = comment_id ? "note-#{comment_id}" : nil
723 anchor = comment_id ? "note-#{comment_id}" : nil
724 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
724 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
725 :class => issue.css_classes,
725 :class => issue.css_classes,
726 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
726 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
727 end
727 end
728 when 'document'
728 when 'document'
729 if document = Document.visible.find_by_id(oid)
729 if document = Document.visible.find_by_id(oid)
730 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
730 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
731 :class => 'document'
731 :class => 'document'
732 end
732 end
733 when 'version'
733 when 'version'
734 if version = Version.visible.find_by_id(oid)
734 if version = Version.visible.find_by_id(oid)
735 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
735 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
736 :class => 'version'
736 :class => 'version'
737 end
737 end
738 when 'message'
738 when 'message'
739 if message = Message.visible.find_by_id(oid, :include => :parent)
739 if message = Message.visible.find_by_id(oid, :include => :parent)
740 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
740 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
741 end
741 end
742 when 'forum'
742 when 'forum'
743 if board = Board.visible.find_by_id(oid)
743 if board = Board.visible.find_by_id(oid)
744 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
744 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
745 :class => 'board'
745 :class => 'board'
746 end
746 end
747 when 'news'
747 when 'news'
748 if news = News.visible.find_by_id(oid)
748 if news = News.visible.find_by_id(oid)
749 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
749 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
750 :class => 'news'
750 :class => 'news'
751 end
751 end
752 when 'project'
752 when 'project'
753 if p = Project.visible.find_by_id(oid)
753 if p = Project.visible.find_by_id(oid)
754 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
754 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
755 end
755 end
756 end
756 end
757 elsif sep == ':'
757 elsif sep == ':'
758 # removes the double quotes if any
758 # removes the double quotes if any
759 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
759 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
760 case prefix
760 case prefix
761 when 'document'
761 when 'document'
762 if project && document = project.documents.visible.find_by_title(name)
762 if project && document = project.documents.visible.find_by_title(name)
763 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
763 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
764 :class => 'document'
764 :class => 'document'
765 end
765 end
766 when 'version'
766 when 'version'
767 if project && version = project.versions.visible.find_by_name(name)
767 if project && version = project.versions.visible.find_by_name(name)
768 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
768 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
769 :class => 'version'
769 :class => 'version'
770 end
770 end
771 when 'forum'
771 when 'forum'
772 if project && board = project.boards.visible.find_by_name(name)
772 if project && board = project.boards.visible.find_by_name(name)
773 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
773 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
774 :class => 'board'
774 :class => 'board'
775 end
775 end
776 when 'news'
776 when 'news'
777 if project && news = project.news.visible.find_by_title(name)
777 if project && news = project.news.visible.find_by_title(name)
778 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
778 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
779 :class => 'news'
779 :class => 'news'
780 end
780 end
781 when 'commit', 'source', 'export'
781 when 'commit', 'source', 'export'
782 if project
782 if project
783 repository = nil
783 repository = nil
784 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
784 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
785 repo_prefix, repo_identifier, name = $1, $2, $3
785 repo_prefix, repo_identifier, name = $1, $2, $3
786 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
786 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
787 else
787 else
788 repository = project.repository
788 repository = project.repository
789 end
789 end
790 if prefix == 'commit'
790 if prefix == 'commit'
791 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
791 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
792 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
792 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
793 :class => 'changeset',
793 :class => 'changeset',
794 :title => truncate_single_line(h(changeset.comments), :length => 100)
794 :title => truncate_single_line(h(changeset.comments), :length => 100)
795 end
795 end
796 else
796 else
797 if repository && User.current.allowed_to?(:browse_repository, project)
797 if repository && User.current.allowed_to?(:browse_repository, project)
798 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
798 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
799 path, rev, anchor = $1, $3, $5
799 path, rev, anchor = $1, $3, $5
800 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
800 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
801 :path => to_path_param(path),
801 :path => to_path_param(path),
802 :rev => rev,
802 :rev => rev,
803 :anchor => anchor},
803 :anchor => anchor},
804 :class => (prefix == 'export' ? 'source download' : 'source')
804 :class => (prefix == 'export' ? 'source download' : 'source')
805 end
805 end
806 end
806 end
807 repo_prefix = nil
807 repo_prefix = nil
808 end
808 end
809 when 'attachment'
809 when 'attachment'
810 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
810 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
811 if attachments && attachment = attachments.detect {|a| a.filename == name }
811 if attachments && attachment = attachments.detect {|a| a.filename == name }
812 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
812 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
813 :class => 'attachment'
813 :class => 'attachment'
814 end
814 end
815 when 'project'
815 when 'project'
816 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
816 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
817 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
817 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
818 end
818 end
819 end
819 end
820 end
820 end
821 end
821 end
822 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
822 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
823 end
823 end
824 end
824 end
825
825
826 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
826 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
827
827
828 def parse_sections(text, project, obj, attr, only_path, options)
828 def parse_sections(text, project, obj, attr, only_path, options)
829 return unless options[:edit_section_links]
829 return unless options[:edit_section_links]
830 text.gsub!(HEADING_RE) do
830 text.gsub!(HEADING_RE) do
831 heading = $1
831 heading = $1
832 @current_section += 1
832 @current_section += 1
833 if @current_section > 1
833 if @current_section > 1
834 content_tag('div',
834 content_tag('div',
835 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
835 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
836 :class => 'contextual',
836 :class => 'contextual',
837 :title => l(:button_edit_section)) + heading.html_safe
837 :title => l(:button_edit_section)) + heading.html_safe
838 else
838 else
839 heading
839 heading
840 end
840 end
841 end
841 end
842 end
842 end
843
843
844 # Headings and TOC
844 # Headings and TOC
845 # Adds ids and links to headings unless options[:headings] is set to false
845 # Adds ids and links to headings unless options[:headings] is set to false
846 def parse_headings(text, project, obj, attr, only_path, options)
846 def parse_headings(text, project, obj, attr, only_path, options)
847 return if options[:headings] == false
847 return if options[:headings] == false
848
848
849 text.gsub!(HEADING_RE) do
849 text.gsub!(HEADING_RE) do
850 level, attrs, content = $2.to_i, $3, $4
850 level, attrs, content = $2.to_i, $3, $4
851 item = strip_tags(content).strip
851 item = strip_tags(content).strip
852 anchor = sanitize_anchor_name(item)
852 anchor = sanitize_anchor_name(item)
853 # used for single-file wiki export
853 # used for single-file wiki export
854 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
854 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
855 @heading_anchors[anchor] ||= 0
855 @heading_anchors[anchor] ||= 0
856 idx = (@heading_anchors[anchor] += 1)
856 idx = (@heading_anchors[anchor] += 1)
857 if idx > 1
857 if idx > 1
858 anchor = "#{anchor}-#{idx}"
858 anchor = "#{anchor}-#{idx}"
859 end
859 end
860 @parsed_headings << [level, anchor, item]
860 @parsed_headings << [level, anchor, item]
861 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
861 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
862 end
862 end
863 end
863 end
864
864
865 MACROS_RE = /(
865 MACROS_RE = /(
866 (!)? # escaping
866 (!)? # escaping
867 (
867 (
868 \{\{ # opening tag
868 \{\{ # opening tag
869 ([\w]+) # macro name
869 ([\w]+) # macro name
870 (\(([^\n\r]*?)\))? # optional arguments
870 (\(([^\n\r]*?)\))? # optional arguments
871 ([\n\r].*?[\n\r])? # optional block of text
871 ([\n\r].*?[\n\r])? # optional block of text
872 \}\} # closing tag
872 \}\} # closing tag
873 )
873 )
874 )/mx unless const_defined?(:MACROS_RE)
874 )/mx unless const_defined?(:MACROS_RE)
875
875
876 MACRO_SUB_RE = /(
876 MACRO_SUB_RE = /(
877 \{\{
877 \{\{
878 macro\((\d+)\)
878 macro\((\d+)\)
879 \}\}
879 \}\}
880 )/x unless const_defined?(:MACRO_SUB_RE)
880 )/x unless const_defined?(:MACRO_SUB_RE)
881
881
882 # Extracts macros from text
882 # Extracts macros from text
883 def catch_macros(text)
883 def catch_macros(text)
884 macros = {}
884 macros = {}
885 text.gsub!(MACROS_RE) do
885 text.gsub!(MACROS_RE) do
886 all, macro = $1, $4.downcase
886 all, macro = $1, $4.downcase
887 if macro_exists?(macro) || all =~ MACRO_SUB_RE
887 if macro_exists?(macro) || all =~ MACRO_SUB_RE
888 index = macros.size
888 index = macros.size
889 macros[index] = all
889 macros[index] = all
890 "{{macro(#{index})}}"
890 "{{macro(#{index})}}"
891 else
891 else
892 all
892 all
893 end
893 end
894 end
894 end
895 macros
895 macros
896 end
896 end
897
897
898 # Executes and replaces macros in text
898 # Executes and replaces macros in text
899 def inject_macros(text, obj, macros, execute=true)
899 def inject_macros(text, obj, macros, execute=true)
900 text.gsub!(MACRO_SUB_RE) do
900 text.gsub!(MACRO_SUB_RE) do
901 all, index = $1, $2.to_i
901 all, index = $1, $2.to_i
902 orig = macros.delete(index)
902 orig = macros.delete(index)
903 if execute && orig && orig =~ MACROS_RE
903 if execute && orig && orig =~ MACROS_RE
904 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
904 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
905 if esc.nil?
905 if esc.nil?
906 h(exec_macro(macro, obj, args, block) || all)
906 h(exec_macro(macro, obj, args, block) || all)
907 else
907 else
908 h(all)
908 h(all)
909 end
909 end
910 elsif orig
910 elsif orig
911 h(orig)
911 h(orig)
912 else
912 else
913 h(all)
913 h(all)
914 end
914 end
915 end
915 end
916 end
916 end
917
917
918 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
918 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
919
919
920 # Renders the TOC with given headings
920 # Renders the TOC with given headings
921 def replace_toc(text, headings)
921 def replace_toc(text, headings)
922 text.gsub!(TOC_RE) do
922 text.gsub!(TOC_RE) do
923 # Keep only the 4 first levels
923 # Keep only the 4 first levels
924 headings = headings.select{|level, anchor, item| level <= 4}
924 headings = headings.select{|level, anchor, item| level <= 4}
925 if headings.empty?
925 if headings.empty?
926 ''
926 ''
927 else
927 else
928 div_class = 'toc'
928 div_class = 'toc'
929 div_class << ' right' if $1 == '>'
929 div_class << ' right' if $1 == '>'
930 div_class << ' left' if $1 == '<'
930 div_class << ' left' if $1 == '<'
931 out = "<ul class=\"#{div_class}\"><li>"
931 out = "<ul class=\"#{div_class}\"><li>"
932 root = headings.map(&:first).min
932 root = headings.map(&:first).min
933 current = root
933 current = root
934 started = false
934 started = false
935 headings.each do |level, anchor, item|
935 headings.each do |level, anchor, item|
936 if level > current
936 if level > current
937 out << '<ul><li>' * (level - current)
937 out << '<ul><li>' * (level - current)
938 elsif level < current
938 elsif level < current
939 out << "</li></ul>\n" * (current - level) + "</li><li>"
939 out << "</li></ul>\n" * (current - level) + "</li><li>"
940 elsif started
940 elsif started
941 out << '</li><li>'
941 out << '</li><li>'
942 end
942 end
943 out << "<a href=\"##{anchor}\">#{item}</a>"
943 out << "<a href=\"##{anchor}\">#{item}</a>"
944 current = level
944 current = level
945 started = true
945 started = true
946 end
946 end
947 out << '</li></ul>' * (current - root)
947 out << '</li></ul>' * (current - root)
948 out << '</li></ul>'
948 out << '</li></ul>'
949 end
949 end
950 end
950 end
951 end
951 end
952
952
953 # Same as Rails' simple_format helper without using paragraphs
953 # Same as Rails' simple_format helper without using paragraphs
954 def simple_format_without_paragraph(text)
954 def simple_format_without_paragraph(text)
955 text.to_s.
955 text.to_s.
956 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
956 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
957 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
957 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
958 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
958 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
959 html_safe
959 html_safe
960 end
960 end
961
961
962 def lang_options_for_select(blank=true)
962 def lang_options_for_select(blank=true)
963 (blank ? [["(auto)", ""]] : []) +
963 (blank ? [["(auto)", ""]] : []) +
964 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
964 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
965 end
965 end
966
966
967 def label_tag_for(name, option_tags = nil, options = {})
967 def label_tag_for(name, option_tags = nil, options = {})
968 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
968 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
969 content_tag("label", label_text)
969 content_tag("label", label_text)
970 end
970 end
971
971
972 def labelled_form_for(*args, &proc)
972 def labelled_form_for(*args, &proc)
973 args << {} unless args.last.is_a?(Hash)
973 args << {} unless args.last.is_a?(Hash)
974 options = args.last
974 options = args.last
975 if args.first.is_a?(Symbol)
975 if args.first.is_a?(Symbol)
976 options.merge!(:as => args.shift)
976 options.merge!(:as => args.shift)
977 end
977 end
978 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
978 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
979 form_for(*args, &proc)
979 form_for(*args, &proc)
980 end
980 end
981
981
982 def labelled_fields_for(*args, &proc)
982 def labelled_fields_for(*args, &proc)
983 args << {} unless args.last.is_a?(Hash)
983 args << {} unless args.last.is_a?(Hash)
984 options = args.last
984 options = args.last
985 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
985 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
986 fields_for(*args, &proc)
986 fields_for(*args, &proc)
987 end
987 end
988
988
989 def labelled_remote_form_for(*args, &proc)
989 def labelled_remote_form_for(*args, &proc)
990 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
990 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
991 args << {} unless args.last.is_a?(Hash)
991 args << {} unless args.last.is_a?(Hash)
992 options = args.last
992 options = args.last
993 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
993 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
994 form_for(*args, &proc)
994 form_for(*args, &proc)
995 end
995 end
996
996
997 def error_messages_for(*objects)
997 def error_messages_for(*objects)
998 html = ""
998 html = ""
999 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
999 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1000 errors = objects.map {|o| o.errors.full_messages}.flatten
1000 errors = objects.map {|o| o.errors.full_messages}.flatten
1001 if errors.any?
1001 if errors.any?
1002 html << "<div id='errorExplanation'><ul>\n"
1002 html << "<div id='errorExplanation'><ul>\n"
1003 errors.each do |error|
1003 errors.each do |error|
1004 html << "<li>#{h error}</li>\n"
1004 html << "<li>#{h error}</li>\n"
1005 end
1005 end
1006 html << "</ul></div>\n"
1006 html << "</ul></div>\n"
1007 end
1007 end
1008 html.html_safe
1008 html.html_safe
1009 end
1009 end
1010
1010
1011 def delete_link(url, options={})
1011 def delete_link(url, options={})
1012 options = {
1012 options = {
1013 :method => :delete,
1013 :method => :delete,
1014 :data => {:confirm => l(:text_are_you_sure)},
1014 :data => {:confirm => l(:text_are_you_sure)},
1015 :class => 'icon icon-del'
1015 :class => 'icon icon-del'
1016 }.merge(options)
1016 }.merge(options)
1017
1017
1018 link_to l(:button_delete), url, options
1018 link_to l(:button_delete), url, options
1019 end
1019 end
1020
1020
1021 def preview_link(url, form, target='preview', options={})
1021 def preview_link(url, form, target='preview', options={})
1022 content_tag 'a', l(:label_preview), {
1022 content_tag 'a', l(:label_preview), {
1023 :href => "#",
1023 :href => "#",
1024 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1024 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1025 :accesskey => accesskey(:preview)
1025 :accesskey => accesskey(:preview)
1026 }.merge(options)
1026 }.merge(options)
1027 end
1027 end
1028
1028
1029 def link_to_function(name, function, html_options={})
1029 def link_to_function(name, function, html_options={})
1030 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1030 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1031 end
1031 end
1032
1032
1033 # Helper to render JSON in views
1033 # Helper to render JSON in views
1034 def raw_json(arg)
1034 def raw_json(arg)
1035 arg.to_json.to_s.gsub('/', '\/').html_safe
1035 arg.to_json.to_s.gsub('/', '\/').html_safe
1036 end
1036 end
1037
1037
1038 def back_url
1038 def back_url
1039 url = params[:back_url]
1039 url = params[:back_url]
1040 if url.nil? && referer = request.env['HTTP_REFERER']
1040 if url.nil? && referer = request.env['HTTP_REFERER']
1041 url = CGI.unescape(referer.to_s)
1041 url = CGI.unescape(referer.to_s)
1042 end
1042 end
1043 url
1043 url
1044 end
1044 end
1045
1045
1046 def back_url_hidden_field_tag
1046 def back_url_hidden_field_tag
1047 url = back_url
1047 url = back_url
1048 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1048 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1049 end
1049 end
1050
1050
1051 def check_all_links(form_name)
1051 def check_all_links(form_name)
1052 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1052 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1053 " | ".html_safe +
1053 " | ".html_safe +
1054 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1054 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1055 end
1055 end
1056
1056
1057 def progress_bar(pcts, options={})
1057 def progress_bar(pcts, options={})
1058 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1058 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1059 pcts = pcts.collect(&:round)
1059 pcts = pcts.collect(&:round)
1060 pcts[1] = pcts[1] - pcts[0]
1060 pcts[1] = pcts[1] - pcts[0]
1061 pcts << (100 - pcts[1] - pcts[0])
1061 pcts << (100 - pcts[1] - pcts[0])
1062 width = options[:width] || '100px;'
1062 width = options[:width] || '100px;'
1063 legend = options[:legend] || ''
1063 legend = options[:legend] || ''
1064 content_tag('table',
1064 content_tag('table',
1065 content_tag('tr',
1065 content_tag('tr',
1066 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1066 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1067 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1067 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1068 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1068 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1069 ), :class => 'progress', :style => "width: #{width};").html_safe +
1069 ), :class => 'progress', :style => "width: #{width};").html_safe +
1070 content_tag('p', legend, :class => 'pourcent').html_safe
1070 content_tag('p', legend, :class => 'pourcent').html_safe
1071 end
1071 end
1072
1072
1073 def checked_image(checked=true)
1073 def checked_image(checked=true)
1074 if checked
1074 if checked
1075 image_tag 'toggle_check.png'
1075 image_tag 'toggle_check.png'
1076 end
1076 end
1077 end
1077 end
1078
1078
1079 def context_menu(url)
1079 def context_menu(url)
1080 unless @context_menu_included
1080 unless @context_menu_included
1081 content_for :header_tags do
1081 content_for :header_tags do
1082 javascript_include_tag('context_menu') +
1082 javascript_include_tag('context_menu') +
1083 stylesheet_link_tag('context_menu')
1083 stylesheet_link_tag('context_menu')
1084 end
1084 end
1085 if l(:direction) == 'rtl'
1085 if l(:direction) == 'rtl'
1086 content_for :header_tags do
1086 content_for :header_tags do
1087 stylesheet_link_tag('context_menu_rtl')
1087 stylesheet_link_tag('context_menu_rtl')
1088 end
1088 end
1089 end
1089 end
1090 @context_menu_included = true
1090 @context_menu_included = true
1091 end
1091 end
1092 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1092 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1093 end
1093 end
1094
1094
1095 def calendar_for(field_id)
1095 def calendar_for(field_id)
1096 include_calendar_headers_tags
1096 include_calendar_headers_tags
1097 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1097 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1098 end
1098 end
1099
1099
1100 def include_calendar_headers_tags
1100 def include_calendar_headers_tags
1101 unless @calendar_headers_tags_included
1101 unless @calendar_headers_tags_included
1102 @calendar_headers_tags_included = true
1102 @calendar_headers_tags_included = true
1103 content_for :header_tags do
1103 content_for :header_tags do
1104 start_of_week = Setting.start_of_week
1104 start_of_week = Setting.start_of_week
1105 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1105 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1106 # Redmine uses 1..7 (monday..sunday) in settings and locales
1106 # Redmine uses 1..7 (monday..sunday) in settings and locales
1107 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1107 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1108 start_of_week = start_of_week.to_i % 7
1108 start_of_week = start_of_week.to_i % 7
1109
1109
1110 tags = javascript_tag(
1110 tags = javascript_tag(
1111 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1111 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1112 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1112 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1113 path_to_image('/images/calendar.png') +
1113 path_to_image('/images/calendar.png') +
1114 "', showButtonPanel: true};")
1114 "', showButtonPanel: true};")
1115 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1115 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1116 unless jquery_locale == 'en'
1116 unless jquery_locale == 'en'
1117 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1117 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1118 end
1118 end
1119 tags
1119 tags
1120 end
1120 end
1121 end
1121 end
1122 end
1122 end
1123
1123
1124 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1124 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1125 # Examples:
1125 # Examples:
1126 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1126 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1127 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1127 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1128 #
1128 #
1129 def stylesheet_link_tag(*sources)
1129 def stylesheet_link_tag(*sources)
1130 options = sources.last.is_a?(Hash) ? sources.pop : {}
1130 options = sources.last.is_a?(Hash) ? sources.pop : {}
1131 plugin = options.delete(:plugin)
1131 plugin = options.delete(:plugin)
1132 sources = sources.map do |source|
1132 sources = sources.map do |source|
1133 if plugin
1133 if plugin
1134 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1134 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1135 elsif current_theme && current_theme.stylesheets.include?(source)
1135 elsif current_theme && current_theme.stylesheets.include?(source)
1136 current_theme.stylesheet_path(source)
1136 current_theme.stylesheet_path(source)
1137 else
1137 else
1138 source
1138 source
1139 end
1139 end
1140 end
1140 end
1141 super sources, options
1141 super sources, options
1142 end
1142 end
1143
1143
1144 # Overrides Rails' image_tag with themes and plugins support.
1144 # Overrides Rails' image_tag with themes and plugins support.
1145 # Examples:
1145 # Examples:
1146 # image_tag('image.png') # => picks image.png from the current theme or defaults
1146 # image_tag('image.png') # => picks image.png from the current theme or defaults
1147 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1147 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1148 #
1148 #
1149 def image_tag(source, options={})
1149 def image_tag(source, options={})
1150 if plugin = options.delete(:plugin)
1150 if plugin = options.delete(:plugin)
1151 source = "/plugin_assets/#{plugin}/images/#{source}"
1151 source = "/plugin_assets/#{plugin}/images/#{source}"
1152 elsif current_theme && current_theme.images.include?(source)
1152 elsif current_theme && current_theme.images.include?(source)
1153 source = current_theme.image_path(source)
1153 source = current_theme.image_path(source)
1154 end
1154 end
1155 super source, options
1155 super source, options
1156 end
1156 end
1157
1157
1158 # Overrides Rails' javascript_include_tag with plugins support
1158 # Overrides Rails' javascript_include_tag with plugins support
1159 # Examples:
1159 # Examples:
1160 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1160 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1161 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1161 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1162 #
1162 #
1163 def javascript_include_tag(*sources)
1163 def javascript_include_tag(*sources)
1164 options = sources.last.is_a?(Hash) ? sources.pop : {}
1164 options = sources.last.is_a?(Hash) ? sources.pop : {}
1165 if plugin = options.delete(:plugin)
1165 if plugin = options.delete(:plugin)
1166 sources = sources.map do |source|
1166 sources = sources.map do |source|
1167 if plugin
1167 if plugin
1168 "/plugin_assets/#{plugin}/javascripts/#{source}"
1168 "/plugin_assets/#{plugin}/javascripts/#{source}"
1169 else
1169 else
1170 source
1170 source
1171 end
1171 end
1172 end
1172 end
1173 end
1173 end
1174 super sources, options
1174 super sources, options
1175 end
1175 end
1176
1176
1177 def content_for(name, content = nil, &block)
1177 def content_for(name, content = nil, &block)
1178 @has_content ||= {}
1178 @has_content ||= {}
1179 @has_content[name] = true
1179 @has_content[name] = true
1180 super(name, content, &block)
1180 super(name, content, &block)
1181 end
1181 end
1182
1182
1183 def has_content?(name)
1183 def has_content?(name)
1184 (@has_content && @has_content[name]) || false
1184 (@has_content && @has_content[name]) || false
1185 end
1185 end
1186
1186
1187 def sidebar_content?
1187 def sidebar_content?
1188 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1188 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1189 end
1189 end
1190
1190
1191 def view_layouts_base_sidebar_hook_response
1191 def view_layouts_base_sidebar_hook_response
1192 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1192 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1193 end
1193 end
1194
1194
1195 def email_delivery_enabled?
1195 def email_delivery_enabled?
1196 !!ActionMailer::Base.perform_deliveries
1196 !!ActionMailer::Base.perform_deliveries
1197 end
1197 end
1198
1198
1199 # Returns the avatar image tag for the given +user+ if avatars are enabled
1199 # Returns the avatar image tag for the given +user+ if avatars are enabled
1200 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1200 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1201 def avatar(user, options = { })
1201 def avatar(user, options = { })
1202 if Setting.gravatar_enabled?
1202 if Setting.gravatar_enabled?
1203 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1203 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1204 email = nil
1204 email = nil
1205 if user.respond_to?(:mail)
1205 if user.respond_to?(:mail)
1206 email = user.mail
1206 email = user.mail
1207 elsif user.to_s =~ %r{<(.+?)>}
1207 elsif user.to_s =~ %r{<(.+?)>}
1208 email = $1
1208 email = $1
1209 end
1209 end
1210 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1210 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1211 else
1211 else
1212 ''
1212 ''
1213 end
1213 end
1214 end
1214 end
1215
1215
1216 def sanitize_anchor_name(anchor)
1216 def sanitize_anchor_name(anchor)
1217 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1217 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1218 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1218 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1219 else
1219 else
1220 # TODO: remove when ruby1.8 is no longer supported
1220 # TODO: remove when ruby1.8 is no longer supported
1221 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1221 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1222 end
1222 end
1223 end
1223 end
1224
1224
1225 # Returns the javascript tags that are included in the html layout head
1225 # Returns the javascript tags that are included in the html layout head
1226 def javascript_heads
1226 def javascript_heads
1227 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1227 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1228 unless User.current.pref.warn_on_leaving_unsaved == '0'
1228 unless User.current.pref.warn_on_leaving_unsaved == '0'
1229 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1229 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1230 end
1230 end
1231 tags
1231 tags
1232 end
1232 end
1233
1233
1234 def favicon
1234 def favicon
1235 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1235 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1236 end
1236 end
1237
1237
1238 def robot_exclusion_tag
1238 def robot_exclusion_tag
1239 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1239 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1240 end
1240 end
1241
1241
1242 # Returns true if arg is expected in the API response
1242 # Returns true if arg is expected in the API response
1243 def include_in_api_response?(arg)
1243 def include_in_api_response?(arg)
1244 unless @included_in_api_response
1244 unless @included_in_api_response
1245 param = params[:include]
1245 param = params[:include]
1246 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1246 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1247 @included_in_api_response.collect!(&:strip)
1247 @included_in_api_response.collect!(&:strip)
1248 end
1248 end
1249 @included_in_api_response.include?(arg.to_s)
1249 @included_in_api_response.include?(arg.to_s)
1250 end
1250 end
1251
1251
1252 # Returns options or nil if nometa param or X-Redmine-Nometa header
1252 # Returns options or nil if nometa param or X-Redmine-Nometa header
1253 # was set in the request
1253 # was set in the request
1254 def api_meta(options)
1254 def api_meta(options)
1255 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1255 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1256 # compatibility mode for activeresource clients that raise
1256 # compatibility mode for activeresource clients that raise
1257 # an error when unserializing an array with attributes
1257 # an error when unserializing an array with attributes
1258 nil
1258 nil
1259 else
1259 else
1260 options
1260 options
1261 end
1261 end
1262 end
1262 end
1263
1263
1264 private
1264 private
1265
1265
1266 def wiki_helper
1266 def wiki_helper
1267 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1267 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1268 extend helper
1268 extend helper
1269 return self
1269 return self
1270 end
1270 end
1271
1271
1272 def link_to_content_update(text, url_params = {}, html_options = {})
1272 def link_to_content_update(text, url_params = {}, html_options = {})
1273 link_to(text, url_params, html_options)
1273 link_to(text, url_params, html_options)
1274 end
1274 end
1275 end
1275 end
@@ -1,696 +1,707
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Account statuses
23 # Account statuses
24 STATUS_ANONYMOUS = 0
24 STATUS_ANONYMOUS = 0
25 STATUS_ACTIVE = 1
25 STATUS_ACTIVE = 1
26 STATUS_REGISTERED = 2
26 STATUS_REGISTERED = 2
27 STATUS_LOCKED = 3
27 STATUS_LOCKED = 3
28
28
29 # Different ways of displaying/sorting users
29 # Different ways of displaying/sorting users
30 USER_FORMATS = {
30 USER_FORMATS = {
31 :firstname_lastname => {
31 :firstname_lastname => {
32 :string => '#{firstname} #{lastname}',
32 :string => '#{firstname} #{lastname}',
33 :order => %w(firstname lastname id),
33 :order => %w(firstname lastname id),
34 :setting_order => 1
34 :setting_order => 1
35 },
35 },
36 :firstname => {
36 :firstname => {
37 :string => '#{firstname}',
37 :string => '#{firstname}',
38 :order => %w(firstname id),
38 :order => %w(firstname id),
39 :setting_order => 2
39 :setting_order => 2
40 },
40 },
41 :lastname_firstname => {
41 :lastname_firstname => {
42 :string => '#{lastname} #{firstname}',
42 :string => '#{lastname} #{firstname}',
43 :order => %w(lastname firstname id),
43 :order => %w(lastname firstname id),
44 :setting_order => 3
44 :setting_order => 3
45 },
45 },
46 :lastname_coma_firstname => {
46 :lastname_coma_firstname => {
47 :string => '#{lastname}, #{firstname}',
47 :string => '#{lastname}, #{firstname}',
48 :order => %w(lastname firstname id),
48 :order => %w(lastname firstname id),
49 :setting_order => 4
49 :setting_order => 4
50 },
50 },
51 :lastname => {
51 :lastname => {
52 :string => '#{lastname}',
52 :string => '#{lastname}',
53 :order => %w(lastname id),
53 :order => %w(lastname id),
54 :setting_order => 5
54 :setting_order => 5
55 },
55 },
56 :username => {
56 :username => {
57 :string => '#{login}',
57 :string => '#{login}',
58 :order => %w(login id),
58 :order => %w(login id),
59 :setting_order => 6
59 :setting_order => 6
60 },
60 },
61 }
61 }
62
62
63 MAIL_NOTIFICATION_OPTIONS = [
63 MAIL_NOTIFICATION_OPTIONS = [
64 ['all', :label_user_mail_option_all],
64 ['all', :label_user_mail_option_all],
65 ['selected', :label_user_mail_option_selected],
65 ['selected', :label_user_mail_option_selected],
66 ['only_my_events', :label_user_mail_option_only_my_events],
66 ['only_my_events', :label_user_mail_option_only_my_events],
67 ['only_assigned', :label_user_mail_option_only_assigned],
67 ['only_assigned', :label_user_mail_option_only_assigned],
68 ['only_owner', :label_user_mail_option_only_owner],
68 ['only_owner', :label_user_mail_option_only_owner],
69 ['none', :label_user_mail_option_none]
69 ['none', :label_user_mail_option_none]
70 ]
70 ]
71
71
72 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
73 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 :after_remove => Proc.new {|user, group| group.user_removed(user)}
74 has_many :changesets, :dependent => :nullify
74 has_many :changesets, :dependent => :nullify
75 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
76 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
77 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
78 belongs_to :auth_source
78 belongs_to :auth_source
79
79
80 scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
80 scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
81 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
81 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
82
82
83 acts_as_customizable
83 acts_as_customizable
84
84
85 attr_accessor :password, :password_confirmation
85 attr_accessor :password, :password_confirmation
86 attr_accessor :last_before_login_on
86 attr_accessor :last_before_login_on
87 # Prevents unauthorized assignments
87 # Prevents unauthorized assignments
88 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
89
89
90 LOGIN_LENGTH_LIMIT = 60
90 LOGIN_LENGTH_LIMIT = 60
91 MAIL_LENGTH_LIMIT = 60
91 MAIL_LENGTH_LIMIT = 60
92
92
93 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
94 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
95 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
95 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
96 # Login must contain lettres, numbers, underscores only
96 # Login must contain lettres, numbers, underscores only
97 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
97 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
98 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
99 validates_length_of :firstname, :lastname, :maximum => 30
99 validates_length_of :firstname, :lastname, :maximum => 30
100 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
100 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
101 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
102 validates_confirmation_of :password, :allow_nil => true
102 validates_confirmation_of :password, :allow_nil => true
103 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
104 validate :validate_password_length
104 validate :validate_password_length
105
105
106 before_create :set_mail_notification
106 before_create :set_mail_notification
107 before_save :update_hashed_password
107 before_save :update_hashed_password
108 before_destroy :remove_references_before_destroy
108 before_destroy :remove_references_before_destroy
109
109
110 scope :in_group, lambda {|group|
110 scope :in_group, lambda {|group|
111 group_id = group.is_a?(Group) ? group.id : group.to_i
111 group_id = group.is_a?(Group) ? group.id : group.to_i
112 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
112 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
113 }
113 }
114 scope :not_in_group, lambda {|group|
114 scope :not_in_group, lambda {|group|
115 group_id = group.is_a?(Group) ? group.id : group.to_i
115 group_id = group.is_a?(Group) ? group.id : group.to_i
116 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
116 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
117 }
117 }
118
118
119 def set_mail_notification
119 def set_mail_notification
120 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
120 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
121 true
121 true
122 end
122 end
123
123
124 def update_hashed_password
124 def update_hashed_password
125 # update hashed_password if password was set
125 # update hashed_password if password was set
126 if self.password && self.auth_source_id.blank?
126 if self.password && self.auth_source_id.blank?
127 salt_password(password)
127 salt_password(password)
128 end
128 end
129 end
129 end
130
130
131 def reload(*args)
131 def reload(*args)
132 @name = nil
132 @name = nil
133 @projects_by_role = nil
133 @projects_by_role = nil
134 super
134 super
135 end
135 end
136
136
137 def mail=(arg)
137 def mail=(arg)
138 write_attribute(:mail, arg.to_s.strip)
138 write_attribute(:mail, arg.to_s.strip)
139 end
139 end
140
140
141 def identity_url=(url)
141 def identity_url=(url)
142 if url.blank?
142 if url.blank?
143 write_attribute(:identity_url, '')
143 write_attribute(:identity_url, '')
144 else
144 else
145 begin
145 begin
146 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
146 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
147 rescue OpenIdAuthentication::InvalidOpenId
147 rescue OpenIdAuthentication::InvalidOpenId
148 # Invlaid url, don't save
148 # Invlaid url, don't save
149 end
149 end
150 end
150 end
151 self.read_attribute(:identity_url)
151 self.read_attribute(:identity_url)
152 end
152 end
153
153
154 # Returns the user that matches provided login and password, or nil
154 # Returns the user that matches provided login and password, or nil
155 def self.try_to_login(login, password)
155 def self.try_to_login(login, password)
156 login = login.to_s
156 login = login.to_s
157 password = password.to_s
157 password = password.to_s
158
158
159 # Make sure no one can sign in with an empty password
159 # Make sure no one can sign in with an empty password
160 return nil if password.empty?
160 return nil if password.empty?
161 user = find_by_login(login)
161 user = find_by_login(login)
162 if user
162 if user
163 # user is already in local database
163 # user is already in local database
164 return nil if !user.active?
164 return nil if !user.active?
165 if user.auth_source
165 if user.auth_source
166 # user has an external authentication method
166 # user has an external authentication method
167 return nil unless user.auth_source.authenticate(login, password)
167 return nil unless user.auth_source.authenticate(login, password)
168 else
168 else
169 # authentication with local password
169 # authentication with local password
170 return nil unless user.check_password?(password)
170 return nil unless user.check_password?(password)
171 end
171 end
172 else
172 else
173 # user is not yet registered, try to authenticate with available sources
173 # user is not yet registered, try to authenticate with available sources
174 attrs = AuthSource.authenticate(login, password)
174 attrs = AuthSource.authenticate(login, password)
175 if attrs
175 if attrs
176 user = new(attrs)
176 user = new(attrs)
177 user.login = login
177 user.login = login
178 user.language = Setting.default_language
178 user.language = Setting.default_language
179 if user.save
179 if user.save
180 user.reload
180 user.reload
181 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
181 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
182 end
182 end
183 end
183 end
184 end
184 end
185 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
185 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
186 user
186 user
187 rescue => text
187 rescue => text
188 raise text
188 raise text
189 end
189 end
190
190
191 # Returns the user who matches the given autologin +key+ or nil
191 # Returns the user who matches the given autologin +key+ or nil
192 def self.try_to_autologin(key)
192 def self.try_to_autologin(key)
193 tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
193 tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
194 # Make sure there's only 1 token that matches the key
194 # Make sure there's only 1 token that matches the key
195 if tokens.size == 1
195 if tokens.size == 1
196 token = tokens.first
196 token = tokens.first
197 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
197 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
198 token.user.update_attribute(:last_login_on, Time.now)
198 token.user.update_attribute(:last_login_on, Time.now)
199 token.user
199 token.user
200 end
200 end
201 end
201 end
202 end
202 end
203
203
204 def self.name_formatter(formatter = nil)
204 def self.name_formatter(formatter = nil)
205 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
205 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
206 end
206 end
207
207
208 # Returns an array of fields names than can be used to make an order statement for users
208 # Returns an array of fields names than can be used to make an order statement for users
209 # according to how user names are displayed
209 # according to how user names are displayed
210 # Examples:
210 # Examples:
211 #
211 #
212 # User.fields_for_order_statement => ['users.login', 'users.id']
212 # User.fields_for_order_statement => ['users.login', 'users.id']
213 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
213 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
214 def self.fields_for_order_statement(table=nil)
214 def self.fields_for_order_statement(table=nil)
215 table ||= table_name
215 table ||= table_name
216 name_formatter[:order].map {|field| "#{table}.#{field}"}
216 name_formatter[:order].map {|field| "#{table}.#{field}"}
217 end
217 end
218
218
219 # Return user's full name for display
219 # Return user's full name for display
220 def name(formatter = nil)
220 def name(formatter = nil)
221 f = self.class.name_formatter(formatter)
221 f = self.class.name_formatter(formatter)
222 if formatter
222 if formatter
223 eval('"' + f[:string] + '"')
223 eval('"' + f[:string] + '"')
224 else
224 else
225 @name ||= eval('"' + f[:string] + '"')
225 @name ||= eval('"' + f[:string] + '"')
226 end
226 end
227 end
227 end
228
228
229 def active?
229 def active?
230 self.status == STATUS_ACTIVE
230 self.status == STATUS_ACTIVE
231 end
231 end
232
232
233 def registered?
233 def registered?
234 self.status == STATUS_REGISTERED
234 self.status == STATUS_REGISTERED
235 end
235 end
236
236
237 def locked?
237 def locked?
238 self.status == STATUS_LOCKED
238 self.status == STATUS_LOCKED
239 end
239 end
240
240
241 def activate
241 def activate
242 self.status = STATUS_ACTIVE
242 self.status = STATUS_ACTIVE
243 end
243 end
244
244
245 def register
245 def register
246 self.status = STATUS_REGISTERED
246 self.status = STATUS_REGISTERED
247 end
247 end
248
248
249 def lock
249 def lock
250 self.status = STATUS_LOCKED
250 self.status = STATUS_LOCKED
251 end
251 end
252
252
253 def activate!
253 def activate!
254 update_attribute(:status, STATUS_ACTIVE)
254 update_attribute(:status, STATUS_ACTIVE)
255 end
255 end
256
256
257 def register!
257 def register!
258 update_attribute(:status, STATUS_REGISTERED)
258 update_attribute(:status, STATUS_REGISTERED)
259 end
259 end
260
260
261 def lock!
261 def lock!
262 update_attribute(:status, STATUS_LOCKED)
262 update_attribute(:status, STATUS_LOCKED)
263 end
263 end
264
264
265 # Returns true if +clear_password+ is the correct user's password, otherwise false
265 # Returns true if +clear_password+ is the correct user's password, otherwise false
266 def check_password?(clear_password)
266 def check_password?(clear_password)
267 if auth_source_id.present?
267 if auth_source_id.present?
268 auth_source.authenticate(self.login, clear_password)
268 auth_source.authenticate(self.login, clear_password)
269 else
269 else
270 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
270 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
271 end
271 end
272 end
272 end
273
273
274 # Generates a random salt and computes hashed_password for +clear_password+
274 # Generates a random salt and computes hashed_password for +clear_password+
275 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
275 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
276 def salt_password(clear_password)
276 def salt_password(clear_password)
277 self.salt = User.generate_salt
277 self.salt = User.generate_salt
278 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
278 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
279 end
279 end
280
280
281 # Does the backend storage allow this user to change their password?
281 # Does the backend storage allow this user to change their password?
282 def change_password_allowed?
282 def change_password_allowed?
283 return true if auth_source.nil?
283 return true if auth_source.nil?
284 return auth_source.allow_password_changes?
284 return auth_source.allow_password_changes?
285 end
285 end
286
286
287 # Generate and set a random password. Useful for automated user creation
287 # Generate and set a random password. Useful for automated user creation
288 # Based on Token#generate_token_value
288 # Based on Token#generate_token_value
289 #
289 #
290 def random_password
290 def random_password
291 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
291 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
292 password = ''
292 password = ''
293 40.times { |i| password << chars[rand(chars.size-1)] }
293 40.times { |i| password << chars[rand(chars.size-1)] }
294 self.password = password
294 self.password = password
295 self.password_confirmation = password
295 self.password_confirmation = password
296 self
296 self
297 end
297 end
298
298
299 def pref
299 def pref
300 self.preference ||= UserPreference.new(:user => self)
300 self.preference ||= UserPreference.new(:user => self)
301 end
301 end
302
302
303 def time_zone
303 def time_zone
304 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
304 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
305 end
305 end
306
306
307 def wants_comments_in_reverse_order?
307 def wants_comments_in_reverse_order?
308 self.pref[:comments_sorting] == 'desc'
308 self.pref[:comments_sorting] == 'desc'
309 end
309 end
310
310
311 # Return user's RSS key (a 40 chars long string), used to access feeds
311 # Return user's RSS key (a 40 chars long string), used to access feeds
312 def rss_key
312 def rss_key
313 if rss_token.nil?
313 if rss_token.nil?
314 create_rss_token(:action => 'feeds')
314 create_rss_token(:action => 'feeds')
315 end
315 end
316 rss_token.value
316 rss_token.value
317 end
317 end
318
318
319 # Return user's API key (a 40 chars long string), used to access the API
319 # Return user's API key (a 40 chars long string), used to access the API
320 def api_key
320 def api_key
321 if api_token.nil?
321 if api_token.nil?
322 create_api_token(:action => 'api')
322 create_api_token(:action => 'api')
323 end
323 end
324 api_token.value
324 api_token.value
325 end
325 end
326
326
327 # Return an array of project ids for which the user has explicitly turned mail notifications on
327 # Return an array of project ids for which the user has explicitly turned mail notifications on
328 def notified_projects_ids
328 def notified_projects_ids
329 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
329 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
330 end
330 end
331
331
332 def notified_project_ids=(ids)
332 def notified_project_ids=(ids)
333 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
333 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
334 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
334 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
335 @notified_projects_ids = nil
335 @notified_projects_ids = nil
336 notified_projects_ids
336 notified_projects_ids
337 end
337 end
338
338
339 def valid_notification_options
339 def valid_notification_options
340 self.class.valid_notification_options(self)
340 self.class.valid_notification_options(self)
341 end
341 end
342
342
343 # Only users that belong to more than 1 project can select projects for which they are notified
343 # Only users that belong to more than 1 project can select projects for which they are notified
344 def self.valid_notification_options(user=nil)
344 def self.valid_notification_options(user=nil)
345 # Note that @user.membership.size would fail since AR ignores
345 # Note that @user.membership.size would fail since AR ignores
346 # :include association option when doing a count
346 # :include association option when doing a count
347 if user.nil? || user.memberships.length < 1
347 if user.nil? || user.memberships.length < 1
348 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
348 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
349 else
349 else
350 MAIL_NOTIFICATION_OPTIONS
350 MAIL_NOTIFICATION_OPTIONS
351 end
351 end
352 end
352 end
353
353
354 # Find a user account by matching the exact login and then a case-insensitive
354 # Find a user account by matching the exact login and then a case-insensitive
355 # version. Exact matches will be given priority.
355 # version. Exact matches will be given priority.
356 def self.find_by_login(login)
356 def self.find_by_login(login)
357 # First look for an exact match
357 # First look for an exact match
358 user = all(:conditions => {:login => login}).detect {|u| u.login == login}
358 user = all(:conditions => {:login => login}).detect {|u| u.login == login}
359 unless user
359 unless user
360 # Fail over to case-insensitive if none was found
360 # Fail over to case-insensitive if none was found
361 user = first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
361 user = first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
362 end
362 end
363 user
363 user
364 end
364 end
365
365
366 def self.find_by_rss_key(key)
366 def self.find_by_rss_key(key)
367 token = Token.find_by_action_and_value('feeds', key.to_s)
367 token = Token.find_by_action_and_value('feeds', key.to_s)
368 token && token.user.active? ? token.user : nil
368 token && token.user.active? ? token.user : nil
369 end
369 end
370
370
371 def self.find_by_api_key(key)
371 def self.find_by_api_key(key)
372 token = Token.find_by_action_and_value('api', key.to_s)
372 token = Token.find_by_action_and_value('api', key.to_s)
373 token && token.user.active? ? token.user : nil
373 token && token.user.active? ? token.user : nil
374 end
374 end
375
375
376 # Makes find_by_mail case-insensitive
376 # Makes find_by_mail case-insensitive
377 def self.find_by_mail(mail)
377 def self.find_by_mail(mail)
378 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
378 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
379 end
379 end
380
380
381 # Returns true if the default admin account can no longer be used
381 # Returns true if the default admin account can no longer be used
382 def self.default_admin_account_changed?
382 def self.default_admin_account_changed?
383 !User.active.find_by_login("admin").try(:check_password?, "admin")
383 !User.active.find_by_login("admin").try(:check_password?, "admin")
384 end
384 end
385
385
386 def to_s
386 def to_s
387 name
387 name
388 end
388 end
389
389
390 CSS_CLASS_BY_STATUS = {
391 STATUS_ANONYMOUS => 'anon',
392 STATUS_ACTIVE => 'active',
393 STATUS_REGISTERED => 'registered',
394 STATUS_LOCKED => 'locked'
395 }
396
397 def css_classes
398 "user #{CSS_CLASS_BY_STATUS[status]}"
399 end
400
390 # Returns the current day according to user's time zone
401 # Returns the current day according to user's time zone
391 def today
402 def today
392 if time_zone.nil?
403 if time_zone.nil?
393 Date.today
404 Date.today
394 else
405 else
395 Time.now.in_time_zone(time_zone).to_date
406 Time.now.in_time_zone(time_zone).to_date
396 end
407 end
397 end
408 end
398
409
399 # Returns the day of +time+ according to user's time zone
410 # Returns the day of +time+ according to user's time zone
400 def time_to_date(time)
411 def time_to_date(time)
401 if time_zone.nil?
412 if time_zone.nil?
402 time.to_date
413 time.to_date
403 else
414 else
404 time.in_time_zone(time_zone).to_date
415 time.in_time_zone(time_zone).to_date
405 end
416 end
406 end
417 end
407
418
408 def logged?
419 def logged?
409 true
420 true
410 end
421 end
411
422
412 def anonymous?
423 def anonymous?
413 !logged?
424 !logged?
414 end
425 end
415
426
416 # Return user's roles for project
427 # Return user's roles for project
417 def roles_for_project(project)
428 def roles_for_project(project)
418 roles = []
429 roles = []
419 # No role on archived projects
430 # No role on archived projects
420 return roles if project.nil? || project.archived?
431 return roles if project.nil? || project.archived?
421 if logged?
432 if logged?
422 # Find project membership
433 # Find project membership
423 membership = memberships.detect {|m| m.project_id == project.id}
434 membership = memberships.detect {|m| m.project_id == project.id}
424 if membership
435 if membership
425 roles = membership.roles
436 roles = membership.roles
426 else
437 else
427 @role_non_member ||= Role.non_member
438 @role_non_member ||= Role.non_member
428 roles << @role_non_member
439 roles << @role_non_member
429 end
440 end
430 else
441 else
431 @role_anonymous ||= Role.anonymous
442 @role_anonymous ||= Role.anonymous
432 roles << @role_anonymous
443 roles << @role_anonymous
433 end
444 end
434 roles
445 roles
435 end
446 end
436
447
437 # Return true if the user is a member of project
448 # Return true if the user is a member of project
438 def member_of?(project)
449 def member_of?(project)
439 !roles_for_project(project).detect {|role| role.member?}.nil?
450 !roles_for_project(project).detect {|role| role.member?}.nil?
440 end
451 end
441
452
442 # Returns a hash of user's projects grouped by roles
453 # Returns a hash of user's projects grouped by roles
443 def projects_by_role
454 def projects_by_role
444 return @projects_by_role if @projects_by_role
455 return @projects_by_role if @projects_by_role
445
456
446 @projects_by_role = Hash.new([])
457 @projects_by_role = Hash.new([])
447 memberships.each do |membership|
458 memberships.each do |membership|
448 if membership.project
459 if membership.project
449 membership.roles.each do |role|
460 membership.roles.each do |role|
450 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
461 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
451 @projects_by_role[role] << membership.project
462 @projects_by_role[role] << membership.project
452 end
463 end
453 end
464 end
454 end
465 end
455 @projects_by_role.each do |role, projects|
466 @projects_by_role.each do |role, projects|
456 projects.uniq!
467 projects.uniq!
457 end
468 end
458
469
459 @projects_by_role
470 @projects_by_role
460 end
471 end
461
472
462 # Returns true if user is arg or belongs to arg
473 # Returns true if user is arg or belongs to arg
463 def is_or_belongs_to?(arg)
474 def is_or_belongs_to?(arg)
464 if arg.is_a?(User)
475 if arg.is_a?(User)
465 self == arg
476 self == arg
466 elsif arg.is_a?(Group)
477 elsif arg.is_a?(Group)
467 arg.users.include?(self)
478 arg.users.include?(self)
468 else
479 else
469 false
480 false
470 end
481 end
471 end
482 end
472
483
473 # Return true if the user is allowed to do the specified action on a specific context
484 # Return true if the user is allowed to do the specified action on a specific context
474 # Action can be:
485 # Action can be:
475 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
486 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
476 # * a permission Symbol (eg. :edit_project)
487 # * a permission Symbol (eg. :edit_project)
477 # Context can be:
488 # Context can be:
478 # * a project : returns true if user is allowed to do the specified action on this project
489 # * a project : returns true if user is allowed to do the specified action on this project
479 # * an array of projects : returns true if user is allowed on every project
490 # * an array of projects : returns true if user is allowed on every project
480 # * nil with options[:global] set : check if user has at least one role allowed for this action,
491 # * nil with options[:global] set : check if user has at least one role allowed for this action,
481 # or falls back to Non Member / Anonymous permissions depending if the user is logged
492 # or falls back to Non Member / Anonymous permissions depending if the user is logged
482 def allowed_to?(action, context, options={}, &block)
493 def allowed_to?(action, context, options={}, &block)
483 if context && context.is_a?(Project)
494 if context && context.is_a?(Project)
484 return false unless context.allows_to?(action)
495 return false unless context.allows_to?(action)
485 # Admin users are authorized for anything else
496 # Admin users are authorized for anything else
486 return true if admin?
497 return true if admin?
487
498
488 roles = roles_for_project(context)
499 roles = roles_for_project(context)
489 return false unless roles
500 return false unless roles
490 roles.any? {|role|
501 roles.any? {|role|
491 (context.is_public? || role.member?) &&
502 (context.is_public? || role.member?) &&
492 role.allowed_to?(action) &&
503 role.allowed_to?(action) &&
493 (block_given? ? yield(role, self) : true)
504 (block_given? ? yield(role, self) : true)
494 }
505 }
495 elsif context && context.is_a?(Array)
506 elsif context && context.is_a?(Array)
496 if context.empty?
507 if context.empty?
497 false
508 false
498 else
509 else
499 # Authorize if user is authorized on every element of the array
510 # Authorize if user is authorized on every element of the array
500 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
511 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
501 end
512 end
502 elsif options[:global]
513 elsif options[:global]
503 # Admin users are always authorized
514 # Admin users are always authorized
504 return true if admin?
515 return true if admin?
505
516
506 # authorize if user has at least one role that has this permission
517 # authorize if user has at least one role that has this permission
507 roles = memberships.collect {|m| m.roles}.flatten.uniq
518 roles = memberships.collect {|m| m.roles}.flatten.uniq
508 roles << (self.logged? ? Role.non_member : Role.anonymous)
519 roles << (self.logged? ? Role.non_member : Role.anonymous)
509 roles.any? {|role|
520 roles.any? {|role|
510 role.allowed_to?(action) &&
521 role.allowed_to?(action) &&
511 (block_given? ? yield(role, self) : true)
522 (block_given? ? yield(role, self) : true)
512 }
523 }
513 else
524 else
514 false
525 false
515 end
526 end
516 end
527 end
517
528
518 # Is the user allowed to do the specified action on any project?
529 # Is the user allowed to do the specified action on any project?
519 # See allowed_to? for the actions and valid options.
530 # See allowed_to? for the actions and valid options.
520 def allowed_to_globally?(action, options, &block)
531 def allowed_to_globally?(action, options, &block)
521 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
532 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
522 end
533 end
523
534
524 # Returns true if the user is allowed to delete his own account
535 # Returns true if the user is allowed to delete his own account
525 def own_account_deletable?
536 def own_account_deletable?
526 Setting.unsubscribe? &&
537 Setting.unsubscribe? &&
527 (!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
538 (!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
528 end
539 end
529
540
530 safe_attributes 'login',
541 safe_attributes 'login',
531 'firstname',
542 'firstname',
532 'lastname',
543 'lastname',
533 'mail',
544 'mail',
534 'mail_notification',
545 'mail_notification',
535 'language',
546 'language',
536 'custom_field_values',
547 'custom_field_values',
537 'custom_fields',
548 'custom_fields',
538 'identity_url'
549 'identity_url'
539
550
540 safe_attributes 'status',
551 safe_attributes 'status',
541 'auth_source_id',
552 'auth_source_id',
542 :if => lambda {|user, current_user| current_user.admin?}
553 :if => lambda {|user, current_user| current_user.admin?}
543
554
544 safe_attributes 'group_ids',
555 safe_attributes 'group_ids',
545 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
556 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
546
557
547 # Utility method to help check if a user should be notified about an
558 # Utility method to help check if a user should be notified about an
548 # event.
559 # event.
549 #
560 #
550 # TODO: only supports Issue events currently
561 # TODO: only supports Issue events currently
551 def notify_about?(object)
562 def notify_about?(object)
552 case mail_notification
563 case mail_notification
553 when 'all'
564 when 'all'
554 true
565 true
555 when 'selected'
566 when 'selected'
556 # user receives notifications for created/assigned issues on unselected projects
567 # user receives notifications for created/assigned issues on unselected projects
557 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
568 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
558 true
569 true
559 else
570 else
560 false
571 false
561 end
572 end
562 when 'none'
573 when 'none'
563 false
574 false
564 when 'only_my_events'
575 when 'only_my_events'
565 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
576 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
566 true
577 true
567 else
578 else
568 false
579 false
569 end
580 end
570 when 'only_assigned'
581 when 'only_assigned'
571 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
582 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
572 true
583 true
573 else
584 else
574 false
585 false
575 end
586 end
576 when 'only_owner'
587 when 'only_owner'
577 if object.is_a?(Issue) && object.author == self
588 if object.is_a?(Issue) && object.author == self
578 true
589 true
579 else
590 else
580 false
591 false
581 end
592 end
582 else
593 else
583 false
594 false
584 end
595 end
585 end
596 end
586
597
587 def self.current=(user)
598 def self.current=(user)
588 @current_user = user
599 @current_user = user
589 end
600 end
590
601
591 def self.current
602 def self.current
592 @current_user ||= User.anonymous
603 @current_user ||= User.anonymous
593 end
604 end
594
605
595 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
606 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
596 # one anonymous user per database.
607 # one anonymous user per database.
597 def self.anonymous
608 def self.anonymous
598 anonymous_user = AnonymousUser.find(:first)
609 anonymous_user = AnonymousUser.find(:first)
599 if anonymous_user.nil?
610 if anonymous_user.nil?
600 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
611 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
601 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
612 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
602 end
613 end
603 anonymous_user
614 anonymous_user
604 end
615 end
605
616
606 # Salts all existing unsalted passwords
617 # Salts all existing unsalted passwords
607 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
618 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
608 # This method is used in the SaltPasswords migration and is to be kept as is
619 # This method is used in the SaltPasswords migration and is to be kept as is
609 def self.salt_unsalted_passwords!
620 def self.salt_unsalted_passwords!
610 transaction do
621 transaction do
611 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
622 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
612 next if user.hashed_password.blank?
623 next if user.hashed_password.blank?
613 salt = User.generate_salt
624 salt = User.generate_salt
614 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
625 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
615 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
626 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
616 end
627 end
617 end
628 end
618 end
629 end
619
630
620 protected
631 protected
621
632
622 def validate_password_length
633 def validate_password_length
623 # Password length validation based on setting
634 # Password length validation based on setting
624 if !password.nil? && password.size < Setting.password_min_length.to_i
635 if !password.nil? && password.size < Setting.password_min_length.to_i
625 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
636 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
626 end
637 end
627 end
638 end
628
639
629 private
640 private
630
641
631 # Removes references that are not handled by associations
642 # Removes references that are not handled by associations
632 # Things that are not deleted are reassociated with the anonymous user
643 # Things that are not deleted are reassociated with the anonymous user
633 def remove_references_before_destroy
644 def remove_references_before_destroy
634 return if self.id.nil?
645 return if self.id.nil?
635
646
636 substitute = User.anonymous
647 substitute = User.anonymous
637 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
648 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
638 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
649 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
639 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
650 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
640 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
651 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
641 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
652 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
642 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
653 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
643 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
654 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
644 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
655 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
645 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
656 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
646 # Remove private queries and keep public ones
657 # Remove private queries and keep public ones
647 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
658 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
648 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
659 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
649 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
660 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
650 Token.delete_all ['user_id = ?', id]
661 Token.delete_all ['user_id = ?', id]
651 Watcher.delete_all ['user_id = ?', id]
662 Watcher.delete_all ['user_id = ?', id]
652 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
663 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
653 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
664 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
654 end
665 end
655
666
656 # Return password digest
667 # Return password digest
657 def self.hash_password(clear_password)
668 def self.hash_password(clear_password)
658 Digest::SHA1.hexdigest(clear_password || "")
669 Digest::SHA1.hexdigest(clear_password || "")
659 end
670 end
660
671
661 # Returns a 128bits random salt as a hex string (32 chars long)
672 # Returns a 128bits random salt as a hex string (32 chars long)
662 def self.generate_salt
673 def self.generate_salt
663 Redmine::Utils.random_hex(16)
674 Redmine::Utils.random_hex(16)
664 end
675 end
665
676
666 end
677 end
667
678
668 class AnonymousUser < User
679 class AnonymousUser < User
669 validate :validate_anonymous_uniqueness, :on => :create
680 validate :validate_anonymous_uniqueness, :on => :create
670
681
671 def validate_anonymous_uniqueness
682 def validate_anonymous_uniqueness
672 # There should be only one AnonymousUser in the database
683 # There should be only one AnonymousUser in the database
673 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
684 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
674 end
685 end
675
686
676 def available_custom_fields
687 def available_custom_fields
677 []
688 []
678 end
689 end
679
690
680 # Overrides a few properties
691 # Overrides a few properties
681 def logged?; false end
692 def logged?; false end
682 def admin; false end
693 def admin; false end
683 def name(*args); I18n.t(:label_user_anonymous) end
694 def name(*args); I18n.t(:label_user_anonymous) end
684 def mail; nil end
695 def mail; nil end
685 def time_zone; nil end
696 def time_zone; nil end
686 def rss_key; nil end
697 def rss_key; nil end
687
698
688 def pref
699 def pref
689 UserPreference.new(:user => self)
700 UserPreference.new(:user => self)
690 end
701 end
691
702
692 # Anonymous user can not be destroyed
703 # Anonymous user can not be destroyed
693 def destroy
704 def destroy
694 false
705 false
695 end
706 end
696 end
707 end
@@ -1,58 +1,58
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_user_plural)%></h2>
5 <h2><%=l(:label_user_plural)%></h2>
6
6
7 <%= form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label for='status'><%= l(:field_status) %>:</label>
9 <label for='status'><%= l(:field_status) %>:</label>
10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11
11
12 <% if @groups.present? %>
12 <% if @groups.present? %>
13 <label for='group_id'><%= l(:label_group) %>:</label>
13 <label for='group_id'><%= l(:label_group) %>:</label>
14 <%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %>
14 <%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %>
15 <% end %>
15 <% end %>
16
16
17 <label for='name'><%= l(:label_user) %>:</label>
17 <label for='name'><%= l(:label_user) %>:</label>
18 <%= text_field_tag 'name', params[:name], :size => 30 %>
18 <%= text_field_tag 'name', params[:name], :size => 30 %>
19 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
19 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
20 <%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
20 <%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
21 </fieldset>
21 </fieldset>
22 <% end %>
22 <% end %>
23 &nbsp;
23 &nbsp;
24
24
25 <div class="autoscroll">
25 <div class="autoscroll">
26 <table class="list">
26 <table class="list">
27 <thead><tr>
27 <thead><tr>
28 <%= sort_header_tag('login', :caption => l(:field_login)) %>
28 <%= sort_header_tag('login', :caption => l(:field_login)) %>
29 <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
29 <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
30 <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
30 <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
31 <%= sort_header_tag('mail', :caption => l(:field_mail)) %>
31 <%= sort_header_tag('mail', :caption => l(:field_mail)) %>
32 <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
32 <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
33 <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
33 <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
34 <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
34 <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
35 <th></th>
35 <th></th>
36 </tr></thead>
36 </tr></thead>
37 <tbody>
37 <tbody>
38 <% for user in @users -%>
38 <% for user in @users -%>
39 <tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">
39 <tr class="<%= user.css_classes %> <%= cycle("odd", "even") %>">
40 <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
40 <td class="username"><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %></td>
41 <td class="firstname"><%= h(user.firstname) %></td>
41 <td class="firstname"><%= h(user.firstname) %></td>
42 <td class="lastname"><%= h(user.lastname) %></td>
42 <td class="lastname"><%= h(user.lastname) %></td>
43 <td class="email"><%= mail_to(h(user.mail)) %></td>
43 <td class="email"><%= mail_to(h(user.mail)) %></td>
44 <td align="center"><%= checked_image user.admin? %></td>
44 <td align="center"><%= checked_image user.admin? %></td>
45 <td class="created_on" align="center"><%= format_time(user.created_on) %></td>
45 <td class="created_on" align="center"><%= format_time(user.created_on) %></td>
46 <td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
46 <td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
47 <td class="buttons">
47 <td class="buttons">
48 <%= change_status_link(user) %>
48 <%= change_status_link(user) %>
49 <%= delete_link user_path(user, :back_url => users_path(params)) unless User.current == user %>
49 <%= delete_link user_path(user, :back_url => users_path(params)) unless User.current == user %>
50 </td>
50 </td>
51 </tr>
51 </tr>
52 <% end -%>
52 <% end -%>
53 </tbody>
53 </tbody>
54 </table>
54 </table>
55 </div>
55 </div>
56 <p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p>
56 <p class="pagination"><%= pagination_links_full @user_pages, @user_count %></p>
57
57
58 <% html_title(l(:label_user_plural)) -%>
58 <% html_title(l(:label_user_plural)) -%>
@@ -1,1132 +1,1133
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3
3
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 #content h1, h2, h3, h4 {color: #555;}
5 #content h1, h2, h3, h4 {color: #555;}
6 h2, .wiki h1 {font-size: 20px;}
6 h2, .wiki h1 {font-size: 20px;}
7 h3, .wiki h2 {font-size: 16px;}
7 h3, .wiki h2 {font-size: 16px;}
8 h4, .wiki h3 {font-size: 13px;}
8 h4, .wiki h3 {font-size: 13px;}
9 h4 {border-bottom: 1px dotted #bbb;}
9 h4 {border-bottom: 1px dotted #bbb;}
10
10
11 /***** Layout *****/
11 /***** Layout *****/
12 #wrapper {background: white;}
12 #wrapper {background: white;}
13
13
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 #top-menu ul {margin: 0; padding: 0;}
15 #top-menu ul {margin: 0; padding: 0;}
16 #top-menu li {
16 #top-menu li {
17 float:left;
17 float:left;
18 list-style-type:none;
18 list-style-type:none;
19 margin: 0px 0px 0px 0px;
19 margin: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
21 white-space:nowrap;
21 white-space:nowrap;
22 }
22 }
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25
25
26 #account {float:right;}
26 #account {float:right;}
27
27
28 #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
29 #header a {color:#f8f8f8;}
29 #header a {color:#f8f8f8;}
30 #header h1 a.ancestor { font-size: 80%; }
30 #header h1 a.ancestor { font-size: 80%; }
31 #quick-search {float:right;}
31 #quick-search {float:right;}
32
32
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
34 #main-menu ul {margin: 0; padding: 0;}
34 #main-menu ul {margin: 0; padding: 0;}
35 #main-menu li {
35 #main-menu li {
36 float:left;
36 float:left;
37 list-style-type:none;
37 list-style-type:none;
38 margin: 0px 2px 0px 0px;
38 margin: 0px 2px 0px 0px;
39 padding: 0px 0px 0px 0px;
39 padding: 0px 0px 0px 0px;
40 white-space:nowrap;
40 white-space:nowrap;
41 }
41 }
42 #main-menu li a {
42 #main-menu li a {
43 display: block;
43 display: block;
44 color: #fff;
44 color: #fff;
45 text-decoration: none;
45 text-decoration: none;
46 font-weight: bold;
46 font-weight: bold;
47 margin: 0;
47 margin: 0;
48 padding: 4px 10px 4px 10px;
48 padding: 4px 10px 4px 10px;
49 }
49 }
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
52
52
53 #admin-menu ul {margin: 0; padding: 0;}
53 #admin-menu ul {margin: 0; padding: 0;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
55
55
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
70
70
71 #main {background-color:#EEEEEE;}
71 #main {background-color:#EEEEEE;}
72
72
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
74 * html #sidebar{ width: 22%; }
74 * html #sidebar{ width: 22%; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
78 #sidebar .contextual { margin-right: 1em; }
78 #sidebar .contextual { margin-right: 1em; }
79
79
80 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
81 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
82 html>body #content { min-height: 600px; }
82 html>body #content { min-height: 600px; }
83 * html body #content { height: 600px; } /* IE */
83 * html body #content { height: 600px; } /* IE */
84
84
85 #main.nosidebar #sidebar{ display: none; }
85 #main.nosidebar #sidebar{ display: none; }
86 #main.nosidebar #content{ width: auto; border-right: 0; }
86 #main.nosidebar #content{ width: auto; border-right: 0; }
87
87
88 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
89
89
90 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
91 #login-form table td {padding: 6px;}
91 #login-form table td {padding: 6px;}
92 #login-form label {font-weight: bold;}
92 #login-form label {font-weight: bold;}
93 #login-form input#username, #login-form input#password { width: 300px; }
93 #login-form input#username, #login-form input#password { width: 300px; }
94
94
95 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
95 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
96 div.modal h3.title {display:none;}
96 div.modal h3.title {display:none;}
97 div.modal p.buttons {text-align:right; margin-bottom:0;}
97 div.modal p.buttons {text-align:right; margin-bottom:0;}
98
98
99 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
99 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
100
100
101 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
101 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
102
102
103 /***** Links *****/
103 /***** Links *****/
104 a, a:link, a:visited{ color: #169; text-decoration: none; }
104 a, a:link, a:visited{ color: #169; text-decoration: none; }
105 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
105 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
106 a img{ border: 0; }
106 a img{ border: 0; }
107
107
108 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
108 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
109 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
109 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
110 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
110
111
111 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
112 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
112 #sidebar a.selected:hover {text-decoration:none;}
113 #sidebar a.selected:hover {text-decoration:none;}
113 #admin-menu a {line-height:1.7em;}
114 #admin-menu a {line-height:1.7em;}
114 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
115 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
115
116
116 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
117 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
117 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
118 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
118
119
119 a#toggle-completed-versions {color:#999;}
120 a#toggle-completed-versions {color:#999;}
120 /***** Tables *****/
121 /***** Tables *****/
121 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
122 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
122 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
123 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
123 table.list td { vertical-align: top; padding-right:10px; }
124 table.list td { vertical-align: top; padding-right:10px; }
124 table.list td.id { width: 2%; text-align: center;}
125 table.list td.id { width: 2%; text-align: center;}
125 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
126 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
126 table.list td.checkbox input {padding:0px;}
127 table.list td.checkbox input {padding:0px;}
127 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
128 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
128 table.list td.buttons a { padding-right: 0.6em; }
129 table.list td.buttons a { padding-right: 0.6em; }
129 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
130 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
130
131
131 tr.project td.name a { white-space:nowrap; }
132 tr.project td.name a { white-space:nowrap; }
132 tr.project.closed, tr.project.archived { color: #aaa; }
133 tr.project.closed, tr.project.archived { color: #aaa; }
133 tr.project.closed a, tr.project.archived a { color: #aaa; }
134 tr.project.closed a, tr.project.archived a { color: #aaa; }
134
135
135 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
136 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
136 tr.project.idnt-1 td.name {padding-left: 0.5em;}
137 tr.project.idnt-1 td.name {padding-left: 0.5em;}
137 tr.project.idnt-2 td.name {padding-left: 2em;}
138 tr.project.idnt-2 td.name {padding-left: 2em;}
138 tr.project.idnt-3 td.name {padding-left: 3.5em;}
139 tr.project.idnt-3 td.name {padding-left: 3.5em;}
139 tr.project.idnt-4 td.name {padding-left: 5em;}
140 tr.project.idnt-4 td.name {padding-left: 5em;}
140 tr.project.idnt-5 td.name {padding-left: 6.5em;}
141 tr.project.idnt-5 td.name {padding-left: 6.5em;}
141 tr.project.idnt-6 td.name {padding-left: 8em;}
142 tr.project.idnt-6 td.name {padding-left: 8em;}
142 tr.project.idnt-7 td.name {padding-left: 9.5em;}
143 tr.project.idnt-7 td.name {padding-left: 9.5em;}
143 tr.project.idnt-8 td.name {padding-left: 11em;}
144 tr.project.idnt-8 td.name {padding-left: 11em;}
144 tr.project.idnt-9 td.name {padding-left: 12.5em;}
145 tr.project.idnt-9 td.name {padding-left: 12.5em;}
145
146
146 tr.issue { text-align: center; white-space: nowrap; }
147 tr.issue { text-align: center; white-space: nowrap; }
147 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
148 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
148 tr.issue td.subject, tr.issue td.relations { text-align: left; }
149 tr.issue td.subject, tr.issue td.relations { text-align: left; }
149 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
150 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
150 tr.issue td.relations span {white-space: nowrap;}
151 tr.issue td.relations span {white-space: nowrap;}
151
152
152 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
153 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
153 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
154 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
154 tr.issue.idnt-2 td.subject {padding-left: 2em;}
155 tr.issue.idnt-2 td.subject {padding-left: 2em;}
155 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
156 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
156 tr.issue.idnt-4 td.subject {padding-left: 5em;}
157 tr.issue.idnt-4 td.subject {padding-left: 5em;}
157 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
158 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
158 tr.issue.idnt-6 td.subject {padding-left: 8em;}
159 tr.issue.idnt-6 td.subject {padding-left: 8em;}
159 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
160 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
160 tr.issue.idnt-8 td.subject {padding-left: 11em;}
161 tr.issue.idnt-8 td.subject {padding-left: 11em;}
161 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
162 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
162
163
163 tr.entry { border: 1px solid #f8f8f8; }
164 tr.entry { border: 1px solid #f8f8f8; }
164 tr.entry td { white-space: nowrap; }
165 tr.entry td { white-space: nowrap; }
165 tr.entry td.filename { width: 30%; }
166 tr.entry td.filename { width: 30%; }
166 tr.entry td.filename_no_report { width: 70%; }
167 tr.entry td.filename_no_report { width: 70%; }
167 tr.entry td.size { text-align: right; font-size: 90%; }
168 tr.entry td.size { text-align: right; font-size: 90%; }
168 tr.entry td.revision, tr.entry td.author { text-align: center; }
169 tr.entry td.revision, tr.entry td.author { text-align: center; }
169 tr.entry td.age { text-align: right; }
170 tr.entry td.age { text-align: right; }
170 tr.entry.file td.filename a { margin-left: 16px; }
171 tr.entry.file td.filename a { margin-left: 16px; }
171 tr.entry.file td.filename_no_report a { margin-left: 16px; }
172 tr.entry.file td.filename_no_report a { margin-left: 16px; }
172
173
173 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
174 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
174 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
175 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
175
176
176 tr.changeset { height: 20px }
177 tr.changeset { height: 20px }
177 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
178 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
178 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
179 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
179 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
180 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
180 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
181 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
181
182
182 table.files tr.file td { text-align: center; }
183 table.files tr.file td { text-align: center; }
183 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
184 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
184 table.files tr.file td.digest { font-size: 80%; }
185 table.files tr.file td.digest { font-size: 80%; }
185
186
186 table.members td.roles, table.memberships td.roles { width: 45%; }
187 table.members td.roles, table.memberships td.roles { width: 45%; }
187
188
188 tr.message { height: 2.6em; }
189 tr.message { height: 2.6em; }
189 tr.message td.subject { padding-left: 20px; }
190 tr.message td.subject { padding-left: 20px; }
190 tr.message td.created_on { white-space: nowrap; }
191 tr.message td.created_on { white-space: nowrap; }
191 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
192 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
192 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
193 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
193 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
194 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
194
195
195 tr.version.closed, tr.version.closed a { color: #999; }
196 tr.version.closed, tr.version.closed a { color: #999; }
196 tr.version td.name { padding-left: 20px; }
197 tr.version td.name { padding-left: 20px; }
197 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
198 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
198 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
199 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
199
200
200 tr.user td { width:13%; }
201 tr.user td { width:13%; }
201 tr.user td.email { width:18%; }
202 tr.user td.email { width:18%; }
202 tr.user td { white-space: nowrap; }
203 tr.user td { white-space: nowrap; }
203 tr.user.locked, tr.user.registered { color: #aaa; }
204 tr.user.locked, tr.user.registered { color: #aaa; }
204 tr.user.locked a, tr.user.registered a { color: #aaa; }
205 tr.user.locked a, tr.user.registered a { color: #aaa; }
205
206
206 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
207 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
207
208
208 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
209 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
209
210
210 tr.time-entry { text-align: center; white-space: nowrap; }
211 tr.time-entry { text-align: center; white-space: nowrap; }
211 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
212 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
212 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
213 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
213 td.hours .hours-dec { font-size: 0.9em; }
214 td.hours .hours-dec { font-size: 0.9em; }
214
215
215 table.plugins td { vertical-align: middle; }
216 table.plugins td { vertical-align: middle; }
216 table.plugins td.configure { text-align: right; padding-right: 1em; }
217 table.plugins td.configure { text-align: right; padding-right: 1em; }
217 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
218 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
218 table.plugins span.description { display: block; font-size: 0.9em; }
219 table.plugins span.description { display: block; font-size: 0.9em; }
219 table.plugins span.url { display: block; font-size: 0.9em; }
220 table.plugins span.url { display: block; font-size: 0.9em; }
220
221
221 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
222 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
222 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
223 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
223 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
224 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
224 tr.group:hover a.toggle-all { display:inline;}
225 tr.group:hover a.toggle-all { display:inline;}
225 a.toggle-all:hover {text-decoration:none;}
226 a.toggle-all:hover {text-decoration:none;}
226
227
227 table.list tbody tr:hover { background-color:#ffffdd; }
228 table.list tbody tr:hover { background-color:#ffffdd; }
228 table.list tbody tr.group:hover { background-color:inherit; }
229 table.list tbody tr.group:hover { background-color:inherit; }
229 table td {padding:2px;}
230 table td {padding:2px;}
230 table p {margin:0;}
231 table p {margin:0;}
231 .odd {background-color:#f6f7f8;}
232 .odd {background-color:#f6f7f8;}
232 .even {background-color: #fff;}
233 .even {background-color: #fff;}
233
234
234 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
235 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
235 a.sort.asc { background-image: url(../images/sort_asc.png); }
236 a.sort.asc { background-image: url(../images/sort_asc.png); }
236 a.sort.desc { background-image: url(../images/sort_desc.png); }
237 a.sort.desc { background-image: url(../images/sort_desc.png); }
237
238
238 table.attributes { width: 100% }
239 table.attributes { width: 100% }
239 table.attributes th { vertical-align: top; text-align: left; }
240 table.attributes th { vertical-align: top; text-align: left; }
240 table.attributes td { vertical-align: top; }
241 table.attributes td { vertical-align: top; }
241
242
242 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
243 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
243 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
244 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
244 table.boards td.last-message {font-size:80%;}
245 table.boards td.last-message {font-size:80%;}
245
246
246 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
247 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
247
248
248 table.query-columns {
249 table.query-columns {
249 border-collapse: collapse;
250 border-collapse: collapse;
250 border: 0;
251 border: 0;
251 }
252 }
252
253
253 table.query-columns td.buttons {
254 table.query-columns td.buttons {
254 vertical-align: middle;
255 vertical-align: middle;
255 text-align: center;
256 text-align: center;
256 }
257 }
257
258
258 td.center {text-align:center;}
259 td.center {text-align:center;}
259
260
260 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
261 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
261
262
262 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
263 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
263 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
264 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
264 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
265 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
265 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
266 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
266
267
267 #watchers ul {margin: 0; padding: 0;}
268 #watchers ul {margin: 0; padding: 0;}
268 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
269 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
269 #watchers select {width: 95%; display: block;}
270 #watchers select {width: 95%; display: block;}
270 #watchers a.delete {opacity: 0.4;}
271 #watchers a.delete {opacity: 0.4;}
271 #watchers a.delete:hover {opacity: 1;}
272 #watchers a.delete:hover {opacity: 1;}
272 #watchers img.gravatar {margin: 0 4px 2px 0;}
273 #watchers img.gravatar {margin: 0 4px 2px 0;}
273
274
274 span#watchers_inputs {overflow:auto; display:block;}
275 span#watchers_inputs {overflow:auto; display:block;}
275 span.search_for_watchers {display:block;}
276 span.search_for_watchers {display:block;}
276 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
277 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
277 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
278 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
278
279
279
280
280 .highlight { background-color: #FCFD8D;}
281 .highlight { background-color: #FCFD8D;}
281 .highlight.token-1 { background-color: #faa;}
282 .highlight.token-1 { background-color: #faa;}
282 .highlight.token-2 { background-color: #afa;}
283 .highlight.token-2 { background-color: #afa;}
283 .highlight.token-3 { background-color: #aaf;}
284 .highlight.token-3 { background-color: #aaf;}
284
285
285 .box{
286 .box{
286 padding:6px;
287 padding:6px;
287 margin-bottom: 10px;
288 margin-bottom: 10px;
288 background-color:#f6f6f6;
289 background-color:#f6f6f6;
289 color:#505050;
290 color:#505050;
290 line-height:1.5em;
291 line-height:1.5em;
291 border: 1px solid #e4e4e4;
292 border: 1px solid #e4e4e4;
292 }
293 }
293
294
294 div.square {
295 div.square {
295 border: 1px solid #999;
296 border: 1px solid #999;
296 float: left;
297 float: left;
297 margin: .3em .4em 0 .4em;
298 margin: .3em .4em 0 .4em;
298 overflow: hidden;
299 overflow: hidden;
299 width: .6em; height: .6em;
300 width: .6em; height: .6em;
300 }
301 }
301 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
302 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
302 .contextual input, .contextual select {font-size:0.9em;}
303 .contextual input, .contextual select {font-size:0.9em;}
303 .message .contextual { margin-top: 0; }
304 .message .contextual { margin-top: 0; }
304
305
305 .splitcontent {overflow:auto;}
306 .splitcontent {overflow:auto;}
306 .splitcontentleft{float:left; width:49%;}
307 .splitcontentleft{float:left; width:49%;}
307 .splitcontentright{float:right; width:49%;}
308 .splitcontentright{float:right; width:49%;}
308 form {display: inline;}
309 form {display: inline;}
309 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
310 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
310 fieldset {border: 1px solid #e4e4e4; margin:0;}
311 fieldset {border: 1px solid #e4e4e4; margin:0;}
311 legend {color: #484848;}
312 legend {color: #484848;}
312 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
313 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
313 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
314 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
314 blockquote blockquote { margin-left: 0;}
315 blockquote blockquote { margin-left: 0;}
315 acronym { border-bottom: 1px dotted; cursor: help; }
316 acronym { border-bottom: 1px dotted; cursor: help; }
316 textarea.wiki-edit { width: 99%; }
317 textarea.wiki-edit { width: 99%; }
317 li p {margin-top: 0;}
318 li p {margin-top: 0;}
318 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
319 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
319 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
320 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
320 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
321 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
321 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
322 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
322
323
323 div.issue div.subject div div { padding-left: 16px; }
324 div.issue div.subject div div { padding-left: 16px; }
324 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
325 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
325 div.issue div.subject>div>p { margin-top: 0.5em; }
326 div.issue div.subject>div>p { margin-top: 0.5em; }
326 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
327 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
327 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
328 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
328 div.issue .next-prev-links {color:#999;}
329 div.issue .next-prev-links {color:#999;}
329 div.issue table.attributes th {width:22%;}
330 div.issue table.attributes th {width:22%;}
330 div.issue table.attributes td {width:28%;}
331 div.issue table.attributes td {width:28%;}
331
332
332 #issue_tree table.issues, #relations table.issues { border: 0; }
333 #issue_tree table.issues, #relations table.issues { border: 0; }
333 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
334 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
334 #relations td.buttons {padding:0;}
335 #relations td.buttons {padding:0;}
335
336
336 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
337 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
337 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
338 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
338 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
339 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
339
340
340 fieldset#date-range p { margin: 2px 0 2px 0; }
341 fieldset#date-range p { margin: 2px 0 2px 0; }
341 fieldset#filters table { border-collapse: collapse; }
342 fieldset#filters table { border-collapse: collapse; }
342 fieldset#filters table td { padding: 0; vertical-align: middle; }
343 fieldset#filters table td { padding: 0; vertical-align: middle; }
343 fieldset#filters tr.filter { height: 2.1em; }
344 fieldset#filters tr.filter { height: 2.1em; }
344 fieldset#filters td.field { width:230px; }
345 fieldset#filters td.field { width:230px; }
345 fieldset#filters td.operator { width:180px; }
346 fieldset#filters td.operator { width:180px; }
346 fieldset#filters td.operator select {max-width:170px;}
347 fieldset#filters td.operator select {max-width:170px;}
347 fieldset#filters td.values { white-space:nowrap; }
348 fieldset#filters td.values { white-space:nowrap; }
348 fieldset#filters td.values select {min-width:130px;}
349 fieldset#filters td.values select {min-width:130px;}
349 fieldset#filters td.values input {height:1em;}
350 fieldset#filters td.values input {height:1em;}
350 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
351 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
351
352
352 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
353 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
353 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
354 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
354
355
355 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
356 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
356 div#issue-changesets div.changeset { padding: 4px;}
357 div#issue-changesets div.changeset { padding: 4px;}
357 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
358 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
358 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
359 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
359
360
360 .journal ul.details img {margin:0 0 -3px 4px;}
361 .journal ul.details img {margin:0 0 -3px 4px;}
361 div.journal {overflow:auto;}
362 div.journal {overflow:auto;}
362 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
363 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
363
364
364 div#activity dl, #search-results { margin-left: 2em; }
365 div#activity dl, #search-results { margin-left: 2em; }
365 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
366 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
366 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
367 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
367 div#activity dt.me .time { border-bottom: 1px solid #999; }
368 div#activity dt.me .time { border-bottom: 1px solid #999; }
368 div#activity dt .time { color: #777; font-size: 80%; }
369 div#activity dt .time { color: #777; font-size: 80%; }
369 div#activity dd .description, #search-results dd .description { font-style: italic; }
370 div#activity dd .description, #search-results dd .description { font-style: italic; }
370 div#activity span.project:after, #search-results span.project:after { content: " -"; }
371 div#activity span.project:after, #search-results span.project:after { content: " -"; }
371 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
372 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
372
373
373 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
374 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
374
375
375 div#search-results-counts {float:right;}
376 div#search-results-counts {float:right;}
376 div#search-results-counts ul { margin-top: 0.5em; }
377 div#search-results-counts ul { margin-top: 0.5em; }
377 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
378 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
378
379
379 dt.issue { background-image: url(../images/ticket.png); }
380 dt.issue { background-image: url(../images/ticket.png); }
380 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
381 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
381 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
382 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
382 dt.issue-note { background-image: url(../images/ticket_note.png); }
383 dt.issue-note { background-image: url(../images/ticket_note.png); }
383 dt.changeset { background-image: url(../images/changeset.png); }
384 dt.changeset { background-image: url(../images/changeset.png); }
384 dt.news { background-image: url(../images/news.png); }
385 dt.news { background-image: url(../images/news.png); }
385 dt.message { background-image: url(../images/message.png); }
386 dt.message { background-image: url(../images/message.png); }
386 dt.reply { background-image: url(../images/comments.png); }
387 dt.reply { background-image: url(../images/comments.png); }
387 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
388 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
388 dt.attachment { background-image: url(../images/attachment.png); }
389 dt.attachment { background-image: url(../images/attachment.png); }
389 dt.document { background-image: url(../images/document.png); }
390 dt.document { background-image: url(../images/document.png); }
390 dt.project { background-image: url(../images/projects.png); }
391 dt.project { background-image: url(../images/projects.png); }
391 dt.time-entry { background-image: url(../images/time.png); }
392 dt.time-entry { background-image: url(../images/time.png); }
392
393
393 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
394 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
394
395
395 div#roadmap .related-issues { margin-bottom: 1em; }
396 div#roadmap .related-issues { margin-bottom: 1em; }
396 div#roadmap .related-issues td.checkbox { display: none; }
397 div#roadmap .related-issues td.checkbox { display: none; }
397 div#roadmap .wiki h1:first-child { display: none; }
398 div#roadmap .wiki h1:first-child { display: none; }
398 div#roadmap .wiki h1 { font-size: 120%; }
399 div#roadmap .wiki h1 { font-size: 120%; }
399 div#roadmap .wiki h2 { font-size: 110%; }
400 div#roadmap .wiki h2 { font-size: 110%; }
400 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
401 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
401
402
402 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
403 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
403 div#version-summary fieldset { margin-bottom: 1em; }
404 div#version-summary fieldset { margin-bottom: 1em; }
404 div#version-summary fieldset.time-tracking table { width:100%; }
405 div#version-summary fieldset.time-tracking table { width:100%; }
405 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
406 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
406
407
407 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
408 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
408 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
409 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
409 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
410 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
410 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
411 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
411 table#time-report .hours-dec { font-size: 0.9em; }
412 table#time-report .hours-dec { font-size: 0.9em; }
412
413
413 div.wiki-page .contextual a {opacity: 0.4}
414 div.wiki-page .contextual a {opacity: 0.4}
414 div.wiki-page .contextual a:hover {opacity: 1}
415 div.wiki-page .contextual a:hover {opacity: 1}
415
416
416 form .attributes select { width: 60%; }
417 form .attributes select { width: 60%; }
417 input#issue_subject { width: 99%; }
418 input#issue_subject { width: 99%; }
418 select#issue_done_ratio { width: 95px; }
419 select#issue_done_ratio { width: 95px; }
419
420
420 ul.projects {margin:0; padding-left:1em;}
421 ul.projects {margin:0; padding-left:1em;}
421 ul.projects ul {padding-left:1.6em;}
422 ul.projects ul {padding-left:1.6em;}
422 ul.projects.root {margin:0; padding:0;}
423 ul.projects.root {margin:0; padding:0;}
423 ul.projects li {list-style-type:none;}
424 ul.projects li {list-style-type:none;}
424
425
425 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
426 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
426 #projects-index ul.projects li.root {margin-bottom: 1em;}
427 #projects-index ul.projects li.root {margin-bottom: 1em;}
427 #projects-index ul.projects li.child {margin-top: 1em;}
428 #projects-index ul.projects li.child {margin-top: 1em;}
428 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
429 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
429 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
430 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
430
431
431 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
432 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
432
433
433 #related-issues li img {vertical-align:middle;}
434 #related-issues li img {vertical-align:middle;}
434
435
435 ul.properties {padding:0; font-size: 0.9em; color: #777;}
436 ul.properties {padding:0; font-size: 0.9em; color: #777;}
436 ul.properties li {list-style-type:none;}
437 ul.properties li {list-style-type:none;}
437 ul.properties li span {font-style:italic;}
438 ul.properties li span {font-style:italic;}
438
439
439 .total-hours { font-size: 110%; font-weight: bold; }
440 .total-hours { font-size: 110%; font-weight: bold; }
440 .total-hours span.hours-int { font-size: 120%; }
441 .total-hours span.hours-int { font-size: 120%; }
441
442
442 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
443 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
443 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
444 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
444
445
445 #workflow_copy_form select { width: 200px; }
446 #workflow_copy_form select { width: 200px; }
446 table.transitions td.enabled {background: #bfb;}
447 table.transitions td.enabled {background: #bfb;}
447 table.fields_permissions select {font-size:90%}
448 table.fields_permissions select {font-size:90%}
448 table.fields_permissions td.readonly {background:#ddd;}
449 table.fields_permissions td.readonly {background:#ddd;}
449 table.fields_permissions td.required {background:#d88;}
450 table.fields_permissions td.required {background:#d88;}
450
451
451 textarea#custom_field_possible_values {width: 99%}
452 textarea#custom_field_possible_values {width: 99%}
452 input#content_comments {width: 99%}
453 input#content_comments {width: 99%}
453
454
454 .pagination {font-size: 90%}
455 .pagination {font-size: 90%}
455 p.pagination {margin-top:8px;}
456 p.pagination {margin-top:8px;}
456
457
457 /***** Tabular forms ******/
458 /***** Tabular forms ******/
458 .tabular p{
459 .tabular p{
459 margin: 0;
460 margin: 0;
460 padding: 3px 0 3px 0;
461 padding: 3px 0 3px 0;
461 padding-left: 180px; /* width of left column containing the label elements */
462 padding-left: 180px; /* width of left column containing the label elements */
462 min-height: 1.8em;
463 min-height: 1.8em;
463 clear:left;
464 clear:left;
464 }
465 }
465
466
466 html>body .tabular p {overflow:hidden;}
467 html>body .tabular p {overflow:hidden;}
467
468
468 .tabular label{
469 .tabular label{
469 font-weight: bold;
470 font-weight: bold;
470 float: left;
471 float: left;
471 text-align: right;
472 text-align: right;
472 /* width of left column */
473 /* width of left column */
473 margin-left: -180px;
474 margin-left: -180px;
474 /* width of labels. Should be smaller than left column to create some right margin */
475 /* width of labels. Should be smaller than left column to create some right margin */
475 width: 175px;
476 width: 175px;
476 }
477 }
477
478
478 .tabular label.floating{
479 .tabular label.floating{
479 font-weight: normal;
480 font-weight: normal;
480 margin-left: 0px;
481 margin-left: 0px;
481 text-align: left;
482 text-align: left;
482 width: 270px;
483 width: 270px;
483 }
484 }
484
485
485 .tabular label.block{
486 .tabular label.block{
486 font-weight: normal;
487 font-weight: normal;
487 margin-left: 0px !important;
488 margin-left: 0px !important;
488 text-align: left;
489 text-align: left;
489 float: none;
490 float: none;
490 display: block;
491 display: block;
491 width: auto;
492 width: auto;
492 }
493 }
493
494
494 .tabular label.inline{
495 .tabular label.inline{
495 float:none;
496 float:none;
496 margin-left: 5px !important;
497 margin-left: 5px !important;
497 width: auto;
498 width: auto;
498 }
499 }
499
500
500 label.no-css {
501 label.no-css {
501 font-weight: inherit;
502 font-weight: inherit;
502 float:none;
503 float:none;
503 text-align:left;
504 text-align:left;
504 margin-left:0px;
505 margin-left:0px;
505 width:auto;
506 width:auto;
506 }
507 }
507 input#time_entry_comments { width: 90%;}
508 input#time_entry_comments { width: 90%;}
508
509
509 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
510 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
510
511
511 .tabular.settings p{ padding-left: 300px; }
512 .tabular.settings p{ padding-left: 300px; }
512 .tabular.settings label{ margin-left: -300px; width: 295px; }
513 .tabular.settings label{ margin-left: -300px; width: 295px; }
513 .tabular.settings textarea { width: 99%; }
514 .tabular.settings textarea { width: 99%; }
514
515
515 .settings.enabled_scm table {width:100%}
516 .settings.enabled_scm table {width:100%}
516 .settings.enabled_scm td.scm_name{ font-weight: bold; }
517 .settings.enabled_scm td.scm_name{ font-weight: bold; }
517
518
518 fieldset.settings label { display: block; }
519 fieldset.settings label { display: block; }
519 fieldset#notified_events .parent { padding-left: 20px; }
520 fieldset#notified_events .parent { padding-left: 20px; }
520
521
521 span.required {color: #bb0000;}
522 span.required {color: #bb0000;}
522 .summary {font-style: italic;}
523 .summary {font-style: italic;}
523
524
524 #attachments_fields input.description {margin-left: 8px; width:340px;}
525 #attachments_fields input.description {margin-left: 8px; width:340px;}
525 #attachments_fields span {display:block; white-space:nowrap;}
526 #attachments_fields span {display:block; white-space:nowrap;}
526 #attachments_fields img {vertical-align: middle;}
527 #attachments_fields img {vertical-align: middle;}
527
528
528 div.attachments { margin-top: 12px; }
529 div.attachments { margin-top: 12px; }
529 div.attachments p { margin:4px 0 2px 0; }
530 div.attachments p { margin:4px 0 2px 0; }
530 div.attachments img { vertical-align: middle; }
531 div.attachments img { vertical-align: middle; }
531 div.attachments span.author { font-size: 0.9em; color: #888; }
532 div.attachments span.author { font-size: 0.9em; color: #888; }
532
533
533 div.thumbnails {margin-top:0.6em;}
534 div.thumbnails {margin-top:0.6em;}
534 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
535 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
535 div.thumbnails img {margin: 3px;}
536 div.thumbnails img {margin: 3px;}
536
537
537 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
538 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
538 .other-formats span + span:before { content: "| "; }
539 .other-formats span + span:before { content: "| "; }
539
540
540 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
541 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
541
542
542 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
543 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
543 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
544 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
544
545
545 textarea.text_cf {width:90%;}
546 textarea.text_cf {width:90%;}
546
547
547 /* Project members tab */
548 /* Project members tab */
548 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
549 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
549 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
550 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
550 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
551 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
551 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
552 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
552 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
553 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
553 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
554 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
554
555
555 #users_for_watcher {height: 200px; overflow:auto;}
556 #users_for_watcher {height: 200px; overflow:auto;}
556 #users_for_watcher label {display: block;}
557 #users_for_watcher label {display: block;}
557
558
558 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
559 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
559
560
560 input#principal_search, input#user_search {width:100%}
561 input#principal_search, input#user_search {width:100%}
561 input#principal_search, input#user_search {
562 input#principal_search, input#user_search {
562 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
563 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
563 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
564 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
564 }
565 }
565 input#principal_search.ajax-loading, input#user_search.ajax-loading {
566 input#principal_search.ajax-loading, input#user_search.ajax-loading {
566 background-image: url(../images/loading.gif);
567 background-image: url(../images/loading.gif);
567 }
568 }
568
569
569 * html div#tab-content-members fieldset div { height: 450px; }
570 * html div#tab-content-members fieldset div { height: 450px; }
570
571
571 /***** Flash & error messages ****/
572 /***** Flash & error messages ****/
572 #errorExplanation, div.flash, .nodata, .warning, .conflict {
573 #errorExplanation, div.flash, .nodata, .warning, .conflict {
573 padding: 4px 4px 4px 30px;
574 padding: 4px 4px 4px 30px;
574 margin-bottom: 12px;
575 margin-bottom: 12px;
575 font-size: 1.1em;
576 font-size: 1.1em;
576 border: 2px solid;
577 border: 2px solid;
577 }
578 }
578
579
579 div.flash {margin-top: 8px;}
580 div.flash {margin-top: 8px;}
580
581
581 div.flash.error, #errorExplanation {
582 div.flash.error, #errorExplanation {
582 background: url(../images/exclamation.png) 8px 50% no-repeat;
583 background: url(../images/exclamation.png) 8px 50% no-repeat;
583 background-color: #ffe3e3;
584 background-color: #ffe3e3;
584 border-color: #dd0000;
585 border-color: #dd0000;
585 color: #880000;
586 color: #880000;
586 }
587 }
587
588
588 div.flash.notice {
589 div.flash.notice {
589 background: url(../images/true.png) 8px 5px no-repeat;
590 background: url(../images/true.png) 8px 5px no-repeat;
590 background-color: #dfffdf;
591 background-color: #dfffdf;
591 border-color: #9fcf9f;
592 border-color: #9fcf9f;
592 color: #005f00;
593 color: #005f00;
593 }
594 }
594
595
595 div.flash.warning, .conflict {
596 div.flash.warning, .conflict {
596 background: url(../images/warning.png) 8px 5px no-repeat;
597 background: url(../images/warning.png) 8px 5px no-repeat;
597 background-color: #FFEBC1;
598 background-color: #FFEBC1;
598 border-color: #FDBF3B;
599 border-color: #FDBF3B;
599 color: #A6750C;
600 color: #A6750C;
600 text-align: left;
601 text-align: left;
601 }
602 }
602
603
603 .nodata, .warning {
604 .nodata, .warning {
604 text-align: center;
605 text-align: center;
605 background-color: #FFEBC1;
606 background-color: #FFEBC1;
606 border-color: #FDBF3B;
607 border-color: #FDBF3B;
607 color: #A6750C;
608 color: #A6750C;
608 }
609 }
609
610
610 #errorExplanation ul { font-size: 0.9em;}
611 #errorExplanation ul { font-size: 0.9em;}
611 #errorExplanation h2, #errorExplanation p { display: none; }
612 #errorExplanation h2, #errorExplanation p { display: none; }
612
613
613 .conflict-details {font-size:80%;}
614 .conflict-details {font-size:80%;}
614
615
615 /***** Ajax indicator ******/
616 /***** Ajax indicator ******/
616 #ajax-indicator {
617 #ajax-indicator {
617 position: absolute; /* fixed not supported by IE */
618 position: absolute; /* fixed not supported by IE */
618 background-color:#eee;
619 background-color:#eee;
619 border: 1px solid #bbb;
620 border: 1px solid #bbb;
620 top:35%;
621 top:35%;
621 left:40%;
622 left:40%;
622 width:20%;
623 width:20%;
623 font-weight:bold;
624 font-weight:bold;
624 text-align:center;
625 text-align:center;
625 padding:0.6em;
626 padding:0.6em;
626 z-index:100;
627 z-index:100;
627 opacity: 0.5;
628 opacity: 0.5;
628 }
629 }
629
630
630 html>body #ajax-indicator { position: fixed; }
631 html>body #ajax-indicator { position: fixed; }
631
632
632 #ajax-indicator span {
633 #ajax-indicator span {
633 background-position: 0% 40%;
634 background-position: 0% 40%;
634 background-repeat: no-repeat;
635 background-repeat: no-repeat;
635 background-image: url(../images/loading.gif);
636 background-image: url(../images/loading.gif);
636 padding-left: 26px;
637 padding-left: 26px;
637 vertical-align: bottom;
638 vertical-align: bottom;
638 }
639 }
639
640
640 /***** Calendar *****/
641 /***** Calendar *****/
641 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
642 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
642 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
643 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
643 table.cal thead th.week-number {width: auto;}
644 table.cal thead th.week-number {width: auto;}
644 table.cal tbody tr {height: 100px;}
645 table.cal tbody tr {height: 100px;}
645 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
646 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
646 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
647 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
647 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
648 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
648 table.cal td.odd p.day-num {color: #bbb;}
649 table.cal td.odd p.day-num {color: #bbb;}
649 table.cal td.today {background:#ffffdd;}
650 table.cal td.today {background:#ffffdd;}
650 table.cal td.today p.day-num {font-weight: bold;}
651 table.cal td.today p.day-num {font-weight: bold;}
651 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
652 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
652 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
653 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
653 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
654 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
654 p.cal.legend span {display:block;}
655 p.cal.legend span {display:block;}
655
656
656 /***** Tooltips ******/
657 /***** Tooltips ******/
657 .tooltip{position:relative;z-index:24;}
658 .tooltip{position:relative;z-index:24;}
658 .tooltip:hover{z-index:25;color:#000;}
659 .tooltip:hover{z-index:25;color:#000;}
659 .tooltip span.tip{display: none; text-align:left;}
660 .tooltip span.tip{display: none; text-align:left;}
660
661
661 div.tooltip:hover span.tip{
662 div.tooltip:hover span.tip{
662 display:block;
663 display:block;
663 position:absolute;
664 position:absolute;
664 top:12px; left:24px; width:270px;
665 top:12px; left:24px; width:270px;
665 border:1px solid #555;
666 border:1px solid #555;
666 background-color:#fff;
667 background-color:#fff;
667 padding: 4px;
668 padding: 4px;
668 font-size: 0.8em;
669 font-size: 0.8em;
669 color:#505050;
670 color:#505050;
670 }
671 }
671
672
672 img.ui-datepicker-trigger {
673 img.ui-datepicker-trigger {
673 cursor: pointer;
674 cursor: pointer;
674 vertical-align: middle;
675 vertical-align: middle;
675 margin-left: 4px;
676 margin-left: 4px;
676 }
677 }
677
678
678 /***** Progress bar *****/
679 /***** Progress bar *****/
679 table.progress {
680 table.progress {
680 border-collapse: collapse;
681 border-collapse: collapse;
681 border-spacing: 0pt;
682 border-spacing: 0pt;
682 empty-cells: show;
683 empty-cells: show;
683 text-align: center;
684 text-align: center;
684 float:left;
685 float:left;
685 margin: 1px 6px 1px 0px;
686 margin: 1px 6px 1px 0px;
686 }
687 }
687
688
688 table.progress td { height: 1em; }
689 table.progress td { height: 1em; }
689 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
690 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
690 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
691 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
691 table.progress td.todo { background: #eee none repeat scroll 0%; }
692 table.progress td.todo { background: #eee none repeat scroll 0%; }
692 p.pourcent {font-size: 80%;}
693 p.pourcent {font-size: 80%;}
693 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
694 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
694
695
695 #roadmap table.progress td { height: 1.2em; }
696 #roadmap table.progress td { height: 1.2em; }
696 /***** Tabs *****/
697 /***** Tabs *****/
697 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
698 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
698 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
699 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
699 #content .tabs ul li {
700 #content .tabs ul li {
700 float:left;
701 float:left;
701 list-style-type:none;
702 list-style-type:none;
702 white-space:nowrap;
703 white-space:nowrap;
703 margin-right:4px;
704 margin-right:4px;
704 background:#fff;
705 background:#fff;
705 position:relative;
706 position:relative;
706 margin-bottom:-1px;
707 margin-bottom:-1px;
707 }
708 }
708 #content .tabs ul li a{
709 #content .tabs ul li a{
709 display:block;
710 display:block;
710 font-size: 0.9em;
711 font-size: 0.9em;
711 text-decoration:none;
712 text-decoration:none;
712 line-height:1.3em;
713 line-height:1.3em;
713 padding:4px 6px 4px 6px;
714 padding:4px 6px 4px 6px;
714 border: 1px solid #ccc;
715 border: 1px solid #ccc;
715 border-bottom: 1px solid #bbbbbb;
716 border-bottom: 1px solid #bbbbbb;
716 background-color: #f6f6f6;
717 background-color: #f6f6f6;
717 color:#999;
718 color:#999;
718 font-weight:bold;
719 font-weight:bold;
719 border-top-left-radius:3px;
720 border-top-left-radius:3px;
720 border-top-right-radius:3px;
721 border-top-right-radius:3px;
721 }
722 }
722
723
723 #content .tabs ul li a:hover {
724 #content .tabs ul li a:hover {
724 background-color: #ffffdd;
725 background-color: #ffffdd;
725 text-decoration:none;
726 text-decoration:none;
726 }
727 }
727
728
728 #content .tabs ul li a.selected {
729 #content .tabs ul li a.selected {
729 background-color: #fff;
730 background-color: #fff;
730 border: 1px solid #bbbbbb;
731 border: 1px solid #bbbbbb;
731 border-bottom: 1px solid #fff;
732 border-bottom: 1px solid #fff;
732 color:#444;
733 color:#444;
733 }
734 }
734
735
735 #content .tabs ul li a.selected:hover {background-color: #fff;}
736 #content .tabs ul li a.selected:hover {background-color: #fff;}
736
737
737 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
738 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
738
739
739 button.tab-left, button.tab-right {
740 button.tab-left, button.tab-right {
740 font-size: 0.9em;
741 font-size: 0.9em;
741 cursor: pointer;
742 cursor: pointer;
742 height:24px;
743 height:24px;
743 border: 1px solid #ccc;
744 border: 1px solid #ccc;
744 border-bottom: 1px solid #bbbbbb;
745 border-bottom: 1px solid #bbbbbb;
745 position:absolute;
746 position:absolute;
746 padding:4px;
747 padding:4px;
747 width: 20px;
748 width: 20px;
748 bottom: -1px;
749 bottom: -1px;
749 }
750 }
750
751
751 button.tab-left {
752 button.tab-left {
752 right: 20px;
753 right: 20px;
753 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
754 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
754 border-top-left-radius:3px;
755 border-top-left-radius:3px;
755 }
756 }
756
757
757 button.tab-right {
758 button.tab-right {
758 right: 0;
759 right: 0;
759 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
760 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
760 border-top-right-radius:3px;
761 border-top-right-radius:3px;
761 }
762 }
762
763
763 /***** Diff *****/
764 /***** Diff *****/
764 .diff_out { background: #fcc; }
765 .diff_out { background: #fcc; }
765 .diff_out span { background: #faa; }
766 .diff_out span { background: #faa; }
766 .diff_in { background: #cfc; }
767 .diff_in { background: #cfc; }
767 .diff_in span { background: #afa; }
768 .diff_in span { background: #afa; }
768
769
769 .text-diff {
770 .text-diff {
770 padding: 1em;
771 padding: 1em;
771 background-color:#f6f6f6;
772 background-color:#f6f6f6;
772 color:#505050;
773 color:#505050;
773 border: 1px solid #e4e4e4;
774 border: 1px solid #e4e4e4;
774 }
775 }
775
776
776 /***** Wiki *****/
777 /***** Wiki *****/
777 div.wiki table {
778 div.wiki table {
778 border-collapse: collapse;
779 border-collapse: collapse;
779 margin-bottom: 1em;
780 margin-bottom: 1em;
780 }
781 }
781
782
782 div.wiki table, div.wiki td, div.wiki th {
783 div.wiki table, div.wiki td, div.wiki th {
783 border: 1px solid #bbb;
784 border: 1px solid #bbb;
784 padding: 4px;
785 padding: 4px;
785 }
786 }
786
787
787 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
788 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
788
789
789 div.wiki .external {
790 div.wiki .external {
790 background-position: 0% 60%;
791 background-position: 0% 60%;
791 background-repeat: no-repeat;
792 background-repeat: no-repeat;
792 padding-left: 12px;
793 padding-left: 12px;
793 background-image: url(../images/external.png);
794 background-image: url(../images/external.png);
794 }
795 }
795
796
796 div.wiki a.new {color: #b73535;}
797 div.wiki a.new {color: #b73535;}
797
798
798 div.wiki ul, div.wiki ol {margin-bottom:1em;}
799 div.wiki ul, div.wiki ol {margin-bottom:1em;}
799
800
800 div.wiki pre {
801 div.wiki pre {
801 margin: 1em 1em 1em 1.6em;
802 margin: 1em 1em 1em 1.6em;
802 padding: 8px;
803 padding: 8px;
803 background-color: #fafafa;
804 background-color: #fafafa;
804 border: 1px solid #e2e2e2;
805 border: 1px solid #e2e2e2;
805 width:auto;
806 width:auto;
806 overflow-x: auto;
807 overflow-x: auto;
807 overflow-y: hidden;
808 overflow-y: hidden;
808 }
809 }
809
810
810 div.wiki ul.toc {
811 div.wiki ul.toc {
811 background-color: #ffffdd;
812 background-color: #ffffdd;
812 border: 1px solid #e4e4e4;
813 border: 1px solid #e4e4e4;
813 padding: 4px;
814 padding: 4px;
814 line-height: 1.2em;
815 line-height: 1.2em;
815 margin-bottom: 12px;
816 margin-bottom: 12px;
816 margin-right: 12px;
817 margin-right: 12px;
817 margin-left: 0;
818 margin-left: 0;
818 display: table
819 display: table
819 }
820 }
820 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
821 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
821
822
822 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
823 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
823 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
824 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
824 div.wiki ul.toc ul { margin: 0; padding: 0; }
825 div.wiki ul.toc ul { margin: 0; padding: 0; }
825 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
826 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
826 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
827 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
827 div.wiki ul.toc a {
828 div.wiki ul.toc a {
828 font-size: 0.9em;
829 font-size: 0.9em;
829 font-weight: normal;
830 font-weight: normal;
830 text-decoration: none;
831 text-decoration: none;
831 color: #606060;
832 color: #606060;
832 }
833 }
833 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
834 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
834
835
835 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
836 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
836 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
837 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
837 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
838 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
838
839
839 div.wiki img { vertical-align: middle; }
840 div.wiki img { vertical-align: middle; }
840
841
841 /***** My page layout *****/
842 /***** My page layout *****/
842 .block-receiver {
843 .block-receiver {
843 border:1px dashed #c0c0c0;
844 border:1px dashed #c0c0c0;
844 margin-bottom: 20px;
845 margin-bottom: 20px;
845 padding: 15px 0 15px 0;
846 padding: 15px 0 15px 0;
846 }
847 }
847
848
848 .mypage-box {
849 .mypage-box {
849 margin:0 0 20px 0;
850 margin:0 0 20px 0;
850 color:#505050;
851 color:#505050;
851 line-height:1.5em;
852 line-height:1.5em;
852 }
853 }
853
854
854 .handle {cursor: move;}
855 .handle {cursor: move;}
855
856
856 a.close-icon {
857 a.close-icon {
857 display:block;
858 display:block;
858 margin-top:3px;
859 margin-top:3px;
859 overflow:hidden;
860 overflow:hidden;
860 width:12px;
861 width:12px;
861 height:12px;
862 height:12px;
862 background-repeat: no-repeat;
863 background-repeat: no-repeat;
863 cursor:pointer;
864 cursor:pointer;
864 background-image:url('../images/close.png');
865 background-image:url('../images/close.png');
865 }
866 }
866 a.close-icon:hover {background-image:url('../images/close_hl.png');}
867 a.close-icon:hover {background-image:url('../images/close_hl.png');}
867
868
868 /***** Gantt chart *****/
869 /***** Gantt chart *****/
869 .gantt_hdr {
870 .gantt_hdr {
870 position:absolute;
871 position:absolute;
871 top:0;
872 top:0;
872 height:16px;
873 height:16px;
873 border-top: 1px solid #c0c0c0;
874 border-top: 1px solid #c0c0c0;
874 border-bottom: 1px solid #c0c0c0;
875 border-bottom: 1px solid #c0c0c0;
875 border-right: 1px solid #c0c0c0;
876 border-right: 1px solid #c0c0c0;
876 text-align: center;
877 text-align: center;
877 overflow: hidden;
878 overflow: hidden;
878 }
879 }
879
880
880 .gantt_subjects { font-size: 0.8em; }
881 .gantt_subjects { font-size: 0.8em; }
881 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
882 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
882
883
883 .task {
884 .task {
884 position: absolute;
885 position: absolute;
885 height:8px;
886 height:8px;
886 font-size:0.8em;
887 font-size:0.8em;
887 color:#888;
888 color:#888;
888 padding:0;
889 padding:0;
889 margin:0;
890 margin:0;
890 line-height:16px;
891 line-height:16px;
891 white-space:nowrap;
892 white-space:nowrap;
892 }
893 }
893
894
894 .task.label {width:100%;}
895 .task.label {width:100%;}
895 .task.label.project, .task.label.version { font-weight: bold; }
896 .task.label.project, .task.label.version { font-weight: bold; }
896
897
897 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
898 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
898 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
899 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
899 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
900 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
900
901
901 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
902 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
902 .task_late.parent, .task_done.parent { height: 3px;}
903 .task_late.parent, .task_done.parent { height: 3px;}
903 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
904 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
904 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
905 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
905
906
906 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
907 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
907 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
908 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
908 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
909 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
909 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
910 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
910
911
911 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
912 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
912 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
913 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
913 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
914 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
914 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
915 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
915
916
916 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
917 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
917 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
918 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
918
919
919 /***** Icons *****/
920 /***** Icons *****/
920 .icon {
921 .icon {
921 background-position: 0% 50%;
922 background-position: 0% 50%;
922 background-repeat: no-repeat;
923 background-repeat: no-repeat;
923 padding-left: 20px;
924 padding-left: 20px;
924 padding-top: 2px;
925 padding-top: 2px;
925 padding-bottom: 3px;
926 padding-bottom: 3px;
926 }
927 }
927
928
928 .icon-add { background-image: url(../images/add.png); }
929 .icon-add { background-image: url(../images/add.png); }
929 .icon-edit { background-image: url(../images/edit.png); }
930 .icon-edit { background-image: url(../images/edit.png); }
930 .icon-copy { background-image: url(../images/copy.png); }
931 .icon-copy { background-image: url(../images/copy.png); }
931 .icon-duplicate { background-image: url(../images/duplicate.png); }
932 .icon-duplicate { background-image: url(../images/duplicate.png); }
932 .icon-del { background-image: url(../images/delete.png); }
933 .icon-del { background-image: url(../images/delete.png); }
933 .icon-move { background-image: url(../images/move.png); }
934 .icon-move { background-image: url(../images/move.png); }
934 .icon-save { background-image: url(../images/save.png); }
935 .icon-save { background-image: url(../images/save.png); }
935 .icon-cancel { background-image: url(../images/cancel.png); }
936 .icon-cancel { background-image: url(../images/cancel.png); }
936 .icon-multiple { background-image: url(../images/table_multiple.png); }
937 .icon-multiple { background-image: url(../images/table_multiple.png); }
937 .icon-folder { background-image: url(../images/folder.png); }
938 .icon-folder { background-image: url(../images/folder.png); }
938 .open .icon-folder { background-image: url(../images/folder_open.png); }
939 .open .icon-folder { background-image: url(../images/folder_open.png); }
939 .icon-package { background-image: url(../images/package.png); }
940 .icon-package { background-image: url(../images/package.png); }
940 .icon-user { background-image: url(../images/user.png); }
941 .icon-user { background-image: url(../images/user.png); }
941 .icon-projects { background-image: url(../images/projects.png); }
942 .icon-projects { background-image: url(../images/projects.png); }
942 .icon-help { background-image: url(../images/help.png); }
943 .icon-help { background-image: url(../images/help.png); }
943 .icon-attachment { background-image: url(../images/attachment.png); }
944 .icon-attachment { background-image: url(../images/attachment.png); }
944 .icon-history { background-image: url(../images/history.png); }
945 .icon-history { background-image: url(../images/history.png); }
945 .icon-time { background-image: url(../images/time.png); }
946 .icon-time { background-image: url(../images/time.png); }
946 .icon-time-add { background-image: url(../images/time_add.png); }
947 .icon-time-add { background-image: url(../images/time_add.png); }
947 .icon-stats { background-image: url(../images/stats.png); }
948 .icon-stats { background-image: url(../images/stats.png); }
948 .icon-warning { background-image: url(../images/warning.png); }
949 .icon-warning { background-image: url(../images/warning.png); }
949 .icon-fav { background-image: url(../images/fav.png); }
950 .icon-fav { background-image: url(../images/fav.png); }
950 .icon-fav-off { background-image: url(../images/fav_off.png); }
951 .icon-fav-off { background-image: url(../images/fav_off.png); }
951 .icon-reload { background-image: url(../images/reload.png); }
952 .icon-reload { background-image: url(../images/reload.png); }
952 .icon-lock { background-image: url(../images/locked.png); }
953 .icon-lock { background-image: url(../images/locked.png); }
953 .icon-unlock { background-image: url(../images/unlock.png); }
954 .icon-unlock { background-image: url(../images/unlock.png); }
954 .icon-checked { background-image: url(../images/true.png); }
955 .icon-checked { background-image: url(../images/true.png); }
955 .icon-details { background-image: url(../images/zoom_in.png); }
956 .icon-details { background-image: url(../images/zoom_in.png); }
956 .icon-report { background-image: url(../images/report.png); }
957 .icon-report { background-image: url(../images/report.png); }
957 .icon-comment { background-image: url(../images/comment.png); }
958 .icon-comment { background-image: url(../images/comment.png); }
958 .icon-summary { background-image: url(../images/lightning.png); }
959 .icon-summary { background-image: url(../images/lightning.png); }
959 .icon-server-authentication { background-image: url(../images/server_key.png); }
960 .icon-server-authentication { background-image: url(../images/server_key.png); }
960 .icon-issue { background-image: url(../images/ticket.png); }
961 .icon-issue { background-image: url(../images/ticket.png); }
961 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
962 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
962 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
963 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
963 .icon-passwd { background-image: url(../images/textfield_key.png); }
964 .icon-passwd { background-image: url(../images/textfield_key.png); }
964 .icon-test { background-image: url(../images/bullet_go.png); }
965 .icon-test { background-image: url(../images/bullet_go.png); }
965
966
966 .icon-file { background-image: url(../images/files/default.png); }
967 .icon-file { background-image: url(../images/files/default.png); }
967 .icon-file.text-plain { background-image: url(../images/files/text.png); }
968 .icon-file.text-plain { background-image: url(../images/files/text.png); }
968 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
969 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
969 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
970 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
970 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
971 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
971 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
972 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
972 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
973 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
973 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
974 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
974 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
975 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
975 .icon-file.text-css { background-image: url(../images/files/css.png); }
976 .icon-file.text-css { background-image: url(../images/files/css.png); }
976 .icon-file.text-html { background-image: url(../images/files/html.png); }
977 .icon-file.text-html { background-image: url(../images/files/html.png); }
977 .icon-file.image-gif { background-image: url(../images/files/image.png); }
978 .icon-file.image-gif { background-image: url(../images/files/image.png); }
978 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
979 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
979 .icon-file.image-png { background-image: url(../images/files/image.png); }
980 .icon-file.image-png { background-image: url(../images/files/image.png); }
980 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
981 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
981 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
982 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
982 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
983 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
983 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
984 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
984
985
985 img.gravatar {
986 img.gravatar {
986 padding: 2px;
987 padding: 2px;
987 border: solid 1px #d5d5d5;
988 border: solid 1px #d5d5d5;
988 background: #fff;
989 background: #fff;
989 vertical-align: middle;
990 vertical-align: middle;
990 }
991 }
991
992
992 div.issue img.gravatar {
993 div.issue img.gravatar {
993 float: left;
994 float: left;
994 margin: 0 6px 0 0;
995 margin: 0 6px 0 0;
995 padding: 5px;
996 padding: 5px;
996 }
997 }
997
998
998 div.issue table img.gravatar {
999 div.issue table img.gravatar {
999 height: 14px;
1000 height: 14px;
1000 width: 14px;
1001 width: 14px;
1001 padding: 2px;
1002 padding: 2px;
1002 float: left;
1003 float: left;
1003 margin: 0 0.5em 0 0;
1004 margin: 0 0.5em 0 0;
1004 }
1005 }
1005
1006
1006 h2 img.gravatar {margin: -2px 4px -4px 0;}
1007 h2 img.gravatar {margin: -2px 4px -4px 0;}
1007 h3 img.gravatar {margin: -4px 4px -4px 0;}
1008 h3 img.gravatar {margin: -4px 4px -4px 0;}
1008 h4 img.gravatar {margin: -6px 4px -4px 0;}
1009 h4 img.gravatar {margin: -6px 4px -4px 0;}
1009 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1010 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1010 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1011 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1011 /* Used on 12px Gravatar img tags without the icon background */
1012 /* Used on 12px Gravatar img tags without the icon background */
1012 .icon-gravatar {float: left; margin-right: 4px;}
1013 .icon-gravatar {float: left; margin-right: 4px;}
1013
1014
1014 #activity dt, .journal {clear: left;}
1015 #activity dt, .journal {clear: left;}
1015
1016
1016 .journal-link {float: right;}
1017 .journal-link {float: right;}
1017
1018
1018 h2 img { vertical-align:middle; }
1019 h2 img { vertical-align:middle; }
1019
1020
1020 .hascontextmenu { cursor: context-menu; }
1021 .hascontextmenu { cursor: context-menu; }
1021
1022
1022 /************* CodeRay styles *************/
1023 /************* CodeRay styles *************/
1023 .syntaxhl div {display: inline;}
1024 .syntaxhl div {display: inline;}
1024 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1025 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1025 .syntaxhl .code pre { overflow: auto }
1026 .syntaxhl .code pre { overflow: auto }
1026 .syntaxhl .debug { color: white !important; background: blue !important; }
1027 .syntaxhl .debug { color: white !important; background: blue !important; }
1027
1028
1028 .syntaxhl .annotation { color:#007 }
1029 .syntaxhl .annotation { color:#007 }
1029 .syntaxhl .attribute-name { color:#b48 }
1030 .syntaxhl .attribute-name { color:#b48 }
1030 .syntaxhl .attribute-value { color:#700 }
1031 .syntaxhl .attribute-value { color:#700 }
1031 .syntaxhl .binary { color:#509 }
1032 .syntaxhl .binary { color:#509 }
1032 .syntaxhl .char .content { color:#D20 }
1033 .syntaxhl .char .content { color:#D20 }
1033 .syntaxhl .char .delimiter { color:#710 }
1034 .syntaxhl .char .delimiter { color:#710 }
1034 .syntaxhl .char { color:#D20 }
1035 .syntaxhl .char { color:#D20 }
1035 .syntaxhl .class { color:#258; font-weight:bold }
1036 .syntaxhl .class { color:#258; font-weight:bold }
1036 .syntaxhl .class-variable { color:#369 }
1037 .syntaxhl .class-variable { color:#369 }
1037 .syntaxhl .color { color:#0A0 }
1038 .syntaxhl .color { color:#0A0 }
1038 .syntaxhl .comment { color:#385 }
1039 .syntaxhl .comment { color:#385 }
1039 .syntaxhl .comment .char { color:#385 }
1040 .syntaxhl .comment .char { color:#385 }
1040 .syntaxhl .comment .delimiter { color:#385 }
1041 .syntaxhl .comment .delimiter { color:#385 }
1041 .syntaxhl .complex { color:#A08 }
1042 .syntaxhl .complex { color:#A08 }
1042 .syntaxhl .constant { color:#258; font-weight:bold }
1043 .syntaxhl .constant { color:#258; font-weight:bold }
1043 .syntaxhl .decorator { color:#B0B }
1044 .syntaxhl .decorator { color:#B0B }
1044 .syntaxhl .definition { color:#099; font-weight:bold }
1045 .syntaxhl .definition { color:#099; font-weight:bold }
1045 .syntaxhl .delimiter { color:black }
1046 .syntaxhl .delimiter { color:black }
1046 .syntaxhl .directive { color:#088; font-weight:bold }
1047 .syntaxhl .directive { color:#088; font-weight:bold }
1047 .syntaxhl .doc { color:#970 }
1048 .syntaxhl .doc { color:#970 }
1048 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1049 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1049 .syntaxhl .doctype { color:#34b }
1050 .syntaxhl .doctype { color:#34b }
1050 .syntaxhl .entity { color:#800; font-weight:bold }
1051 .syntaxhl .entity { color:#800; font-weight:bold }
1051 .syntaxhl .error { color:#F00; background-color:#FAA }
1052 .syntaxhl .error { color:#F00; background-color:#FAA }
1052 .syntaxhl .escape { color:#666 }
1053 .syntaxhl .escape { color:#666 }
1053 .syntaxhl .exception { color:#C00; font-weight:bold }
1054 .syntaxhl .exception { color:#C00; font-weight:bold }
1054 .syntaxhl .float { color:#06D }
1055 .syntaxhl .float { color:#06D }
1055 .syntaxhl .function { color:#06B; font-weight:bold }
1056 .syntaxhl .function { color:#06B; font-weight:bold }
1056 .syntaxhl .global-variable { color:#d70 }
1057 .syntaxhl .global-variable { color:#d70 }
1057 .syntaxhl .hex { color:#02b }
1058 .syntaxhl .hex { color:#02b }
1058 .syntaxhl .imaginary { color:#f00 }
1059 .syntaxhl .imaginary { color:#f00 }
1059 .syntaxhl .include { color:#B44; font-weight:bold }
1060 .syntaxhl .include { color:#B44; font-weight:bold }
1060 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1061 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1061 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1062 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1062 .syntaxhl .instance-variable { color:#33B }
1063 .syntaxhl .instance-variable { color:#33B }
1063 .syntaxhl .integer { color:#06D }
1064 .syntaxhl .integer { color:#06D }
1064 .syntaxhl .key .char { color: #60f }
1065 .syntaxhl .key .char { color: #60f }
1065 .syntaxhl .key .delimiter { color: #404 }
1066 .syntaxhl .key .delimiter { color: #404 }
1066 .syntaxhl .key { color: #606 }
1067 .syntaxhl .key { color: #606 }
1067 .syntaxhl .keyword { color:#939; font-weight:bold }
1068 .syntaxhl .keyword { color:#939; font-weight:bold }
1068 .syntaxhl .label { color:#970; font-weight:bold }
1069 .syntaxhl .label { color:#970; font-weight:bold }
1069 .syntaxhl .local-variable { color:#963 }
1070 .syntaxhl .local-variable { color:#963 }
1070 .syntaxhl .namespace { color:#707; font-weight:bold }
1071 .syntaxhl .namespace { color:#707; font-weight:bold }
1071 .syntaxhl .octal { color:#40E }
1072 .syntaxhl .octal { color:#40E }
1072 .syntaxhl .operator { }
1073 .syntaxhl .operator { }
1073 .syntaxhl .predefined { color:#369; font-weight:bold }
1074 .syntaxhl .predefined { color:#369; font-weight:bold }
1074 .syntaxhl .predefined-constant { color:#069 }
1075 .syntaxhl .predefined-constant { color:#069 }
1075 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1076 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1076 .syntaxhl .preprocessor { color:#579 }
1077 .syntaxhl .preprocessor { color:#579 }
1077 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1078 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1078 .syntaxhl .regexp .content { color:#808 }
1079 .syntaxhl .regexp .content { color:#808 }
1079 .syntaxhl .regexp .delimiter { color:#404 }
1080 .syntaxhl .regexp .delimiter { color:#404 }
1080 .syntaxhl .regexp .modifier { color:#C2C }
1081 .syntaxhl .regexp .modifier { color:#C2C }
1081 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1082 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1082 .syntaxhl .reserved { color:#080; font-weight:bold }
1083 .syntaxhl .reserved { color:#080; font-weight:bold }
1083 .syntaxhl .shell .content { color:#2B2 }
1084 .syntaxhl .shell .content { color:#2B2 }
1084 .syntaxhl .shell .delimiter { color:#161 }
1085 .syntaxhl .shell .delimiter { color:#161 }
1085 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1086 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1086 .syntaxhl .string .char { color: #46a }
1087 .syntaxhl .string .char { color: #46a }
1087 .syntaxhl .string .content { color: #46a }
1088 .syntaxhl .string .content { color: #46a }
1088 .syntaxhl .string .delimiter { color: #46a }
1089 .syntaxhl .string .delimiter { color: #46a }
1089 .syntaxhl .string .modifier { color: #46a }
1090 .syntaxhl .string .modifier { color: #46a }
1090 .syntaxhl .symbol .content { color:#d33 }
1091 .syntaxhl .symbol .content { color:#d33 }
1091 .syntaxhl .symbol .delimiter { color:#d33 }
1092 .syntaxhl .symbol .delimiter { color:#d33 }
1092 .syntaxhl .symbol { color:#d33 }
1093 .syntaxhl .symbol { color:#d33 }
1093 .syntaxhl .tag { color:#070 }
1094 .syntaxhl .tag { color:#070 }
1094 .syntaxhl .type { color:#339; font-weight:bold }
1095 .syntaxhl .type { color:#339; font-weight:bold }
1095 .syntaxhl .value { color: #088; }
1096 .syntaxhl .value { color: #088; }
1096 .syntaxhl .variable { color:#037 }
1097 .syntaxhl .variable { color:#037 }
1097
1098
1098 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1099 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1099 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1100 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1100 .syntaxhl .change { color: #bbf; background: #007; }
1101 .syntaxhl .change { color: #bbf; background: #007; }
1101 .syntaxhl .head { color: #f8f; background: #505 }
1102 .syntaxhl .head { color: #f8f; background: #505 }
1102 .syntaxhl .head .filename { color: white; }
1103 .syntaxhl .head .filename { color: white; }
1103
1104
1104 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1105 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1105 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1106 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1106
1107
1107 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1108 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1108 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1109 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1109 .syntaxhl .change .change { color: #88f }
1110 .syntaxhl .change .change { color: #88f }
1110 .syntaxhl .head .head { color: #f4f }
1111 .syntaxhl .head .head { color: #f4f }
1111
1112
1112 /***** Media print specific styles *****/
1113 /***** Media print specific styles *****/
1113 @media print {
1114 @media print {
1114 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1115 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1115 #main { background: #fff; }
1116 #main { background: #fff; }
1116 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1117 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1117 #wiki_add_attachment { display:none; }
1118 #wiki_add_attachment { display:none; }
1118 .hide-when-print { display: none; }
1119 .hide-when-print { display: none; }
1119 .autoscroll {overflow-x: visible;}
1120 .autoscroll {overflow-x: visible;}
1120 table.list {margin-top:0.5em;}
1121 table.list {margin-top:0.5em;}
1121 table.list th, table.list td {border: 1px solid #aaa;}
1122 table.list th, table.list td {border: 1px solid #aaa;}
1122 }
1123 }
1123
1124
1124 /* Accessibility specific styles */
1125 /* Accessibility specific styles */
1125 .hidden-for-sighted {
1126 .hidden-for-sighted {
1126 position:absolute;
1127 position:absolute;
1127 left:-10000px;
1128 left:-10000px;
1128 top:auto;
1129 top:auto;
1129 width:1px;
1130 width:1px;
1130 height:1px;
1131 height:1px;
1131 overflow:hidden;
1132 overflow:hidden;
1132 }
1133 }
@@ -1,480 +1,489
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 'shoulda'
18 #require 'shoulda'
19 ENV["RAILS_ENV"] = "test"
19 ENV["RAILS_ENV"] = "test"
20 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
21 require 'rails/test_help'
21 require 'rails/test_help'
22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
23
23
24 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
24 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
25 include ObjectHelpers
25 include ObjectHelpers
26
26
27 class ActiveSupport::TestCase
27 class ActiveSupport::TestCase
28 include ActionDispatch::TestProcess
28 include ActionDispatch::TestProcess
29
29
30 # Transactional fixtures accelerate your tests by wrapping each test method
30 # Transactional fixtures accelerate your tests by wrapping each test method
31 # in a transaction that's rolled back on completion. This ensures that the
31 # in a transaction that's rolled back on completion. This ensures that the
32 # test database remains unchanged so your fixtures don't have to be reloaded
32 # test database remains unchanged so your fixtures don't have to be reloaded
33 # between every test method. Fewer database queries means faster tests.
33 # between every test method. Fewer database queries means faster tests.
34 #
34 #
35 # Read Mike Clark's excellent walkthrough at
35 # Read Mike Clark's excellent walkthrough at
36 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
36 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
37 #
37 #
38 # Every Active Record database supports transactions except MyISAM tables
38 # Every Active Record database supports transactions except MyISAM tables
39 # in MySQL. Turn off transactional fixtures in this case; however, if you
39 # in MySQL. Turn off transactional fixtures in this case; however, if you
40 # don't care one way or the other, switching from MyISAM to InnoDB tables
40 # don't care one way or the other, switching from MyISAM to InnoDB tables
41 # is recommended.
41 # is recommended.
42 self.use_transactional_fixtures = true
42 self.use_transactional_fixtures = true
43
43
44 # Instantiated fixtures are slow, but give you @david where otherwise you
44 # Instantiated fixtures are slow, but give you @david where otherwise you
45 # would need people(:david). If you don't want to migrate your existing
45 # would need people(:david). If you don't want to migrate your existing
46 # test cases which use the @david style and don't mind the speed hit (each
46 # test cases which use the @david style and don't mind the speed hit (each
47 # instantiated fixtures translates to a database query per test method),
47 # instantiated fixtures translates to a database query per test method),
48 # then set this back to true.
48 # then set this back to true.
49 self.use_instantiated_fixtures = false
49 self.use_instantiated_fixtures = false
50
50
51 # Add more helper methods to be used by all tests here...
51 # Add more helper methods to be used by all tests here...
52
52
53 def log_user(login, password)
53 def log_user(login, password)
54 User.anonymous
54 User.anonymous
55 get "/login"
55 get "/login"
56 assert_equal nil, session[:user_id]
56 assert_equal nil, session[:user_id]
57 assert_response :success
57 assert_response :success
58 assert_template "account/login"
58 assert_template "account/login"
59 post "/login", :username => login, :password => password
59 post "/login", :username => login, :password => password
60 assert_equal login, User.find(session[:user_id]).login
60 assert_equal login, User.find(session[:user_id]).login
61 end
61 end
62
62
63 def uploaded_test_file(name, mime)
63 def uploaded_test_file(name, mime)
64 fixture_file_upload("files/#{name}", mime, true)
64 fixture_file_upload("files/#{name}", mime, true)
65 end
65 end
66
66
67 def credentials(user, password=nil)
67 def credentials(user, password=nil)
68 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
68 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
69 end
69 end
70
70
71 # Mock out a file
71 # Mock out a file
72 def self.mock_file
72 def self.mock_file
73 file = 'a_file.png'
73 file = 'a_file.png'
74 file.stubs(:size).returns(32)
74 file.stubs(:size).returns(32)
75 file.stubs(:original_filename).returns('a_file.png')
75 file.stubs(:original_filename).returns('a_file.png')
76 file.stubs(:content_type).returns('image/png')
76 file.stubs(:content_type).returns('image/png')
77 file.stubs(:read).returns(false)
77 file.stubs(:read).returns(false)
78 file
78 file
79 end
79 end
80
80
81 def mock_file
81 def mock_file
82 self.class.mock_file
82 self.class.mock_file
83 end
83 end
84
84
85 def mock_file_with_options(options={})
85 def mock_file_with_options(options={})
86 file = ''
86 file = ''
87 file.stubs(:size).returns(32)
87 file.stubs(:size).returns(32)
88 original_filename = options[:original_filename] || nil
88 original_filename = options[:original_filename] || nil
89 file.stubs(:original_filename).returns(original_filename)
89 file.stubs(:original_filename).returns(original_filename)
90 content_type = options[:content_type] || nil
90 content_type = options[:content_type] || nil
91 file.stubs(:content_type).returns(content_type)
91 file.stubs(:content_type).returns(content_type)
92 file.stubs(:read).returns(false)
92 file.stubs(:read).returns(false)
93 file
93 file
94 end
94 end
95
95
96 # Use a temporary directory for attachment related tests
96 # Use a temporary directory for attachment related tests
97 def set_tmp_attachments_directory
97 def set_tmp_attachments_directory
98 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
98 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
99 unless File.directory?("#{Rails.root}/tmp/test/attachments")
99 unless File.directory?("#{Rails.root}/tmp/test/attachments")
100 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
100 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
101 end
101 end
102 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
102 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
103 end
103 end
104
104
105 def set_fixtures_attachments_directory
105 def set_fixtures_attachments_directory
106 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
106 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
107 end
107 end
108
108
109 def with_settings(options, &block)
109 def with_settings(options, &block)
110 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].is_a?(Symbol) ? Setting[k] : Setting[k].dup; h}
110 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].is_a?(Symbol) ? Setting[k] : Setting[k].dup; h}
111 options.each {|k, v| Setting[k] = v}
111 options.each {|k, v| Setting[k] = v}
112 yield
112 yield
113 ensure
113 ensure
114 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
114 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
115 end
115 end
116
116
117 # Yields the block with user as the current user
118 def with_current_user(user, &block)
119 saved_user = User.current
120 User.current = user
121 yield
122 ensure
123 User.current = saved_user
124 end
125
117 def change_user_password(login, new_password)
126 def change_user_password(login, new_password)
118 user = User.first(:conditions => {:login => login})
127 user = User.first(:conditions => {:login => login})
119 user.password, user.password_confirmation = new_password, new_password
128 user.password, user.password_confirmation = new_password, new_password
120 user.save!
129 user.save!
121 end
130 end
122
131
123 def self.ldap_configured?
132 def self.ldap_configured?
124 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
133 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
125 return @test_ldap.bind
134 return @test_ldap.bind
126 rescue Exception => e
135 rescue Exception => e
127 # LDAP is not listening
136 # LDAP is not listening
128 return nil
137 return nil
129 end
138 end
130
139
131 def self.convert_installed?
140 def self.convert_installed?
132 Redmine::Thumbnail.convert_available?
141 Redmine::Thumbnail.convert_available?
133 end
142 end
134
143
135 # Returns the path to the test +vendor+ repository
144 # Returns the path to the test +vendor+ repository
136 def self.repository_path(vendor)
145 def self.repository_path(vendor)
137 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
146 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
138 end
147 end
139
148
140 # Returns the url of the subversion test repository
149 # Returns the url of the subversion test repository
141 def self.subversion_repository_url
150 def self.subversion_repository_url
142 path = repository_path('subversion')
151 path = repository_path('subversion')
143 path = '/' + path unless path.starts_with?('/')
152 path = '/' + path unless path.starts_with?('/')
144 "file://#{path}"
153 "file://#{path}"
145 end
154 end
146
155
147 # Returns true if the +vendor+ test repository is configured
156 # Returns true if the +vendor+ test repository is configured
148 def self.repository_configured?(vendor)
157 def self.repository_configured?(vendor)
149 File.directory?(repository_path(vendor))
158 File.directory?(repository_path(vendor))
150 end
159 end
151
160
152 def repository_path_hash(arr)
161 def repository_path_hash(arr)
153 hs = {}
162 hs = {}
154 hs[:path] = arr.join("/")
163 hs[:path] = arr.join("/")
155 hs[:param] = arr.join("/")
164 hs[:param] = arr.join("/")
156 hs
165 hs
157 end
166 end
158
167
159 def assert_save(object)
168 def assert_save(object)
160 saved = object.save
169 saved = object.save
161 message = "#{object.class} could not be saved"
170 message = "#{object.class} could not be saved"
162 errors = object.errors.full_messages.map {|m| "- #{m}"}
171 errors = object.errors.full_messages.map {|m| "- #{m}"}
163 message << ":\n#{errors.join("\n")}" if errors.any?
172 message << ":\n#{errors.join("\n")}" if errors.any?
164 assert_equal true, saved, message
173 assert_equal true, saved, message
165 end
174 end
166
175
167 def assert_error_tag(options={})
176 def assert_error_tag(options={})
168 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
177 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
169 end
178 end
170
179
171 def assert_include(expected, s, message=nil)
180 def assert_include(expected, s, message=nil)
172 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
181 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
173 end
182 end
174
183
175 def assert_not_include(expected, s)
184 def assert_not_include(expected, s)
176 assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\""
185 assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\""
177 end
186 end
178
187
179 def assert_mail_body_match(expected, mail)
188 def assert_mail_body_match(expected, mail)
180 if expected.is_a?(String)
189 if expected.is_a?(String)
181 assert_include expected, mail_body(mail)
190 assert_include expected, mail_body(mail)
182 else
191 else
183 assert_match expected, mail_body(mail)
192 assert_match expected, mail_body(mail)
184 end
193 end
185 end
194 end
186
195
187 def assert_mail_body_no_match(expected, mail)
196 def assert_mail_body_no_match(expected, mail)
188 if expected.is_a?(String)
197 if expected.is_a?(String)
189 assert_not_include expected, mail_body(mail)
198 assert_not_include expected, mail_body(mail)
190 else
199 else
191 assert_no_match expected, mail_body(mail)
200 assert_no_match expected, mail_body(mail)
192 end
201 end
193 end
202 end
194
203
195 def mail_body(mail)
204 def mail_body(mail)
196 mail.parts.first.body.encoded
205 mail.parts.first.body.encoded
197 end
206 end
198
207
199 # Shoulda macros
208 # Shoulda macros
200 def self.should_render_404
209 def self.should_render_404
201 should_respond_with :not_found
210 should_respond_with :not_found
202 should_render_template 'common/error'
211 should_render_template 'common/error'
203 end
212 end
204
213
205 def self.should_have_before_filter(expected_method, options = {})
214 def self.should_have_before_filter(expected_method, options = {})
206 should_have_filter('before', expected_method, options)
215 should_have_filter('before', expected_method, options)
207 end
216 end
208
217
209 def self.should_have_after_filter(expected_method, options = {})
218 def self.should_have_after_filter(expected_method, options = {})
210 should_have_filter('after', expected_method, options)
219 should_have_filter('after', expected_method, options)
211 end
220 end
212
221
213 def self.should_have_filter(filter_type, expected_method, options)
222 def self.should_have_filter(filter_type, expected_method, options)
214 description = "have #{filter_type}_filter :#{expected_method}"
223 description = "have #{filter_type}_filter :#{expected_method}"
215 description << " with #{options.inspect}" unless options.empty?
224 description << " with #{options.inspect}" unless options.empty?
216
225
217 should description do
226 should description do
218 klass = "action_controller/filters/#{filter_type}_filter".classify.constantize
227 klass = "action_controller/filters/#{filter_type}_filter".classify.constantize
219 expected = klass.new(:filter, expected_method.to_sym, options)
228 expected = klass.new(:filter, expected_method.to_sym, options)
220 assert_equal 1, @controller.class.filter_chain.select { |filter|
229 assert_equal 1, @controller.class.filter_chain.select { |filter|
221 filter.method == expected.method && filter.kind == expected.kind &&
230 filter.method == expected.method && filter.kind == expected.kind &&
222 filter.options == expected.options && filter.class == expected.class
231 filter.options == expected.options && filter.class == expected.class
223 }.size
232 }.size
224 end
233 end
225 end
234 end
226
235
227 # Test that a request allows the three types of API authentication
236 # Test that a request allows the three types of API authentication
228 #
237 #
229 # * HTTP Basic with username and password
238 # * HTTP Basic with username and password
230 # * HTTP Basic with an api key for the username
239 # * HTTP Basic with an api key for the username
231 # * Key based with the key=X parameter
240 # * Key based with the key=X parameter
232 #
241 #
233 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
242 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
234 # @param [String] url the request url
243 # @param [String] url the request url
235 # @param [optional, Hash] parameters additional request parameters
244 # @param [optional, Hash] parameters additional request parameters
236 # @param [optional, Hash] options additional options
245 # @param [optional, Hash] options additional options
237 # @option options [Symbol] :success_code Successful response code (:success)
246 # @option options [Symbol] :success_code Successful response code (:success)
238 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
247 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
239 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
248 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
240 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
249 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
241 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
250 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
242 should_allow_key_based_auth(http_method, url, parameters, options)
251 should_allow_key_based_auth(http_method, url, parameters, options)
243 end
252 end
244
253
245 # Test that a request allows the username and password for HTTP BASIC
254 # Test that a request allows the username and password for HTTP BASIC
246 #
255 #
247 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
256 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
248 # @param [String] url the request url
257 # @param [String] url the request url
249 # @param [optional, Hash] parameters additional request parameters
258 # @param [optional, Hash] parameters additional request parameters
250 # @param [optional, Hash] options additional options
259 # @param [optional, Hash] options additional options
251 # @option options [Symbol] :success_code Successful response code (:success)
260 # @option options [Symbol] :success_code Successful response code (:success)
252 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
261 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
253 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
262 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
254 success_code = options[:success_code] || :success
263 success_code = options[:success_code] || :success
255 failure_code = options[:failure_code] || :unauthorized
264 failure_code = options[:failure_code] || :unauthorized
256
265
257 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
266 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
258 context "with a valid HTTP authentication" do
267 context "with a valid HTTP authentication" do
259 setup do
268 setup do
260 @user = User.generate! do |user|
269 @user = User.generate! do |user|
261 user.admin = true
270 user.admin = true
262 user.password = 'my_password'
271 user.password = 'my_password'
263 end
272 end
264 send(http_method, url, parameters, credentials(@user.login, 'my_password'))
273 send(http_method, url, parameters, credentials(@user.login, 'my_password'))
265 end
274 end
266
275
267 should_respond_with success_code
276 should_respond_with success_code
268 should_respond_with_content_type_based_on_url(url)
277 should_respond_with_content_type_based_on_url(url)
269 should "login as the user" do
278 should "login as the user" do
270 assert_equal @user, User.current
279 assert_equal @user, User.current
271 end
280 end
272 end
281 end
273
282
274 context "with an invalid HTTP authentication" do
283 context "with an invalid HTTP authentication" do
275 setup do
284 setup do
276 @user = User.generate!
285 @user = User.generate!
277 send(http_method, url, parameters, credentials(@user.login, 'wrong_password'))
286 send(http_method, url, parameters, credentials(@user.login, 'wrong_password'))
278 end
287 end
279
288
280 should_respond_with failure_code
289 should_respond_with failure_code
281 should_respond_with_content_type_based_on_url(url)
290 should_respond_with_content_type_based_on_url(url)
282 should "not login as the user" do
291 should "not login as the user" do
283 assert_equal User.anonymous, User.current
292 assert_equal User.anonymous, User.current
284 end
293 end
285 end
294 end
286
295
287 context "without credentials" do
296 context "without credentials" do
288 setup do
297 setup do
289 send(http_method, url, parameters)
298 send(http_method, url, parameters)
290 end
299 end
291
300
292 should_respond_with failure_code
301 should_respond_with failure_code
293 should_respond_with_content_type_based_on_url(url)
302 should_respond_with_content_type_based_on_url(url)
294 should "include_www_authenticate_header" do
303 should "include_www_authenticate_header" do
295 assert @controller.response.headers.has_key?('WWW-Authenticate')
304 assert @controller.response.headers.has_key?('WWW-Authenticate')
296 end
305 end
297 end
306 end
298 end
307 end
299 end
308 end
300
309
301 # Test that a request allows the API key with HTTP BASIC
310 # Test that a request allows the API key with HTTP BASIC
302 #
311 #
303 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
312 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
304 # @param [String] url the request url
313 # @param [String] url the request url
305 # @param [optional, Hash] parameters additional request parameters
314 # @param [optional, Hash] parameters additional request parameters
306 # @param [optional, Hash] options additional options
315 # @param [optional, Hash] options additional options
307 # @option options [Symbol] :success_code Successful response code (:success)
316 # @option options [Symbol] :success_code Successful response code (:success)
308 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
317 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
309 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
318 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
310 success_code = options[:success_code] || :success
319 success_code = options[:success_code] || :success
311 failure_code = options[:failure_code] || :unauthorized
320 failure_code = options[:failure_code] || :unauthorized
312
321
313 context "should allow http basic auth with a key for #{http_method} #{url}" do
322 context "should allow http basic auth with a key for #{http_method} #{url}" do
314 context "with a valid HTTP authentication using the API token" do
323 context "with a valid HTTP authentication using the API token" do
315 setup do
324 setup do
316 @user = User.generate! do |user|
325 @user = User.generate! do |user|
317 user.admin = true
326 user.admin = true
318 end
327 end
319 @token = Token.create!(:user => @user, :action => 'api')
328 @token = Token.create!(:user => @user, :action => 'api')
320 send(http_method, url, parameters, credentials(@token.value, 'X'))
329 send(http_method, url, parameters, credentials(@token.value, 'X'))
321 end
330 end
322 should_respond_with success_code
331 should_respond_with success_code
323 should_respond_with_content_type_based_on_url(url)
332 should_respond_with_content_type_based_on_url(url)
324 should_be_a_valid_response_string_based_on_url(url)
333 should_be_a_valid_response_string_based_on_url(url)
325 should "login as the user" do
334 should "login as the user" do
326 assert_equal @user, User.current
335 assert_equal @user, User.current
327 end
336 end
328 end
337 end
329
338
330 context "with an invalid HTTP authentication" do
339 context "with an invalid HTTP authentication" do
331 setup do
340 setup do
332 @user = User.generate!
341 @user = User.generate!
333 @token = Token.create!(:user => @user, :action => 'feeds')
342 @token = Token.create!(:user => @user, :action => 'feeds')
334 send(http_method, url, parameters, credentials(@token.value, 'X'))
343 send(http_method, url, parameters, credentials(@token.value, 'X'))
335 end
344 end
336 should_respond_with failure_code
345 should_respond_with failure_code
337 should_respond_with_content_type_based_on_url(url)
346 should_respond_with_content_type_based_on_url(url)
338 should "not login as the user" do
347 should "not login as the user" do
339 assert_equal User.anonymous, User.current
348 assert_equal User.anonymous, User.current
340 end
349 end
341 end
350 end
342 end
351 end
343 end
352 end
344
353
345 # Test that a request allows full key authentication
354 # Test that a request allows full key authentication
346 #
355 #
347 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
356 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
348 # @param [String] url the request url, without the key=ZXY parameter
357 # @param [String] url the request url, without the key=ZXY parameter
349 # @param [optional, Hash] parameters additional request parameters
358 # @param [optional, Hash] parameters additional request parameters
350 # @param [optional, Hash] options additional options
359 # @param [optional, Hash] options additional options
351 # @option options [Symbol] :success_code Successful response code (:success)
360 # @option options [Symbol] :success_code Successful response code (:success)
352 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
361 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
353 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
362 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
354 success_code = options[:success_code] || :success
363 success_code = options[:success_code] || :success
355 failure_code = options[:failure_code] || :unauthorized
364 failure_code = options[:failure_code] || :unauthorized
356
365
357 context "should allow key based auth using key=X for #{http_method} #{url}" do
366 context "should allow key based auth using key=X for #{http_method} #{url}" do
358 context "with a valid api token" do
367 context "with a valid api token" do
359 setup do
368 setup do
360 @user = User.generate! do |user|
369 @user = User.generate! do |user|
361 user.admin = true
370 user.admin = true
362 end
371 end
363 @token = Token.create!(:user => @user, :action => 'api')
372 @token = Token.create!(:user => @user, :action => 'api')
364 # Simple url parse to add on ?key= or &key=
373 # Simple url parse to add on ?key= or &key=
365 request_url = if url.match(/\?/)
374 request_url = if url.match(/\?/)
366 url + "&key=#{@token.value}"
375 url + "&key=#{@token.value}"
367 else
376 else
368 url + "?key=#{@token.value}"
377 url + "?key=#{@token.value}"
369 end
378 end
370 send(http_method, request_url, parameters)
379 send(http_method, request_url, parameters)
371 end
380 end
372 should_respond_with success_code
381 should_respond_with success_code
373 should_respond_with_content_type_based_on_url(url)
382 should_respond_with_content_type_based_on_url(url)
374 should_be_a_valid_response_string_based_on_url(url)
383 should_be_a_valid_response_string_based_on_url(url)
375 should "login as the user" do
384 should "login as the user" do
376 assert_equal @user, User.current
385 assert_equal @user, User.current
377 end
386 end
378 end
387 end
379
388
380 context "with an invalid api token" do
389 context "with an invalid api token" do
381 setup do
390 setup do
382 @user = User.generate! do |user|
391 @user = User.generate! do |user|
383 user.admin = true
392 user.admin = true
384 end
393 end
385 @token = Token.create!(:user => @user, :action => 'feeds')
394 @token = Token.create!(:user => @user, :action => 'feeds')
386 # Simple url parse to add on ?key= or &key=
395 # Simple url parse to add on ?key= or &key=
387 request_url = if url.match(/\?/)
396 request_url = if url.match(/\?/)
388 url + "&key=#{@token.value}"
397 url + "&key=#{@token.value}"
389 else
398 else
390 url + "?key=#{@token.value}"
399 url + "?key=#{@token.value}"
391 end
400 end
392 send(http_method, request_url, parameters)
401 send(http_method, request_url, parameters)
393 end
402 end
394 should_respond_with failure_code
403 should_respond_with failure_code
395 should_respond_with_content_type_based_on_url(url)
404 should_respond_with_content_type_based_on_url(url)
396 should "not login as the user" do
405 should "not login as the user" do
397 assert_equal User.anonymous, User.current
406 assert_equal User.anonymous, User.current
398 end
407 end
399 end
408 end
400 end
409 end
401
410
402 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
411 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
403 setup do
412 setup do
404 @user = User.generate! do |user|
413 @user = User.generate! do |user|
405 user.admin = true
414 user.admin = true
406 end
415 end
407 @token = Token.create!(:user => @user, :action => 'api')
416 @token = Token.create!(:user => @user, :action => 'api')
408 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
417 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
409 end
418 end
410 should_respond_with success_code
419 should_respond_with success_code
411 should_respond_with_content_type_based_on_url(url)
420 should_respond_with_content_type_based_on_url(url)
412 should_be_a_valid_response_string_based_on_url(url)
421 should_be_a_valid_response_string_based_on_url(url)
413 should "login as the user" do
422 should "login as the user" do
414 assert_equal @user, User.current
423 assert_equal @user, User.current
415 end
424 end
416 end
425 end
417 end
426 end
418
427
419 # Uses should_respond_with_content_type based on what's in the url:
428 # Uses should_respond_with_content_type based on what's in the url:
420 #
429 #
421 # '/project/issues.xml' => should_respond_with_content_type :xml
430 # '/project/issues.xml' => should_respond_with_content_type :xml
422 # '/project/issues.json' => should_respond_with_content_type :json
431 # '/project/issues.json' => should_respond_with_content_type :json
423 #
432 #
424 # @param [String] url Request
433 # @param [String] url Request
425 def self.should_respond_with_content_type_based_on_url(url)
434 def self.should_respond_with_content_type_based_on_url(url)
426 case
435 case
427 when url.match(/xml/i)
436 when url.match(/xml/i)
428 should "respond with XML" do
437 should "respond with XML" do
429 assert_equal 'application/xml', @response.content_type
438 assert_equal 'application/xml', @response.content_type
430 end
439 end
431 when url.match(/json/i)
440 when url.match(/json/i)
432 should "respond with JSON" do
441 should "respond with JSON" do
433 assert_equal 'application/json', @response.content_type
442 assert_equal 'application/json', @response.content_type
434 end
443 end
435 else
444 else
436 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
445 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
437 end
446 end
438 end
447 end
439
448
440 # Uses the url to assert which format the response should be in
449 # Uses the url to assert which format the response should be in
441 #
450 #
442 # '/project/issues.xml' => should_be_a_valid_xml_string
451 # '/project/issues.xml' => should_be_a_valid_xml_string
443 # '/project/issues.json' => should_be_a_valid_json_string
452 # '/project/issues.json' => should_be_a_valid_json_string
444 #
453 #
445 # @param [String] url Request
454 # @param [String] url Request
446 def self.should_be_a_valid_response_string_based_on_url(url)
455 def self.should_be_a_valid_response_string_based_on_url(url)
447 case
456 case
448 when url.match(/xml/i)
457 when url.match(/xml/i)
449 should_be_a_valid_xml_string
458 should_be_a_valid_xml_string
450 when url.match(/json/i)
459 when url.match(/json/i)
451 should_be_a_valid_json_string
460 should_be_a_valid_json_string
452 else
461 else
453 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
462 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
454 end
463 end
455 end
464 end
456
465
457 # Checks that the response is a valid JSON string
466 # Checks that the response is a valid JSON string
458 def self.should_be_a_valid_json_string
467 def self.should_be_a_valid_json_string
459 should "be a valid JSON string (or empty)" do
468 should "be a valid JSON string (or empty)" do
460 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
469 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
461 end
470 end
462 end
471 end
463
472
464 # Checks that the response is a valid XML string
473 # Checks that the response is a valid XML string
465 def self.should_be_a_valid_xml_string
474 def self.should_be_a_valid_xml_string
466 should "be a valid XML string" do
475 should "be a valid XML string" do
467 assert REXML::Document.new(response.body)
476 assert REXML::Document.new(response.body)
468 end
477 end
469 end
478 end
470
479
471 def self.should_respond_with(status)
480 def self.should_respond_with(status)
472 should "respond with #{status}" do
481 should "respond with #{status}" do
473 assert_response status
482 assert_response status
474 end
483 end
475 end
484 end
476 end
485 end
477
486
478 # Simple module to "namespace" all of the API tests
487 # Simple module to "namespace" all of the API tests
479 module ApiTest
488 module ApiTest
480 end
489 end
@@ -1,1133 +1,1141
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class ApplicationHelperTest < ActionView::TestCase
22 class ApplicationHelperTest < ActionView::TestCase
23 include ERB::Util
23 include ERB::Util
24
24
25 fixtures :projects, :roles, :enabled_modules, :users,
25 fixtures :projects, :roles, :enabled_modules, :users,
26 :repositories, :changesets,
26 :repositories, :changesets,
27 :trackers, :issue_statuses, :issues, :versions, :documents,
27 :trackers, :issue_statuses, :issues, :versions, :documents,
28 :wikis, :wiki_pages, :wiki_contents,
28 :wikis, :wiki_pages, :wiki_contents,
29 :boards, :messages, :news,
29 :boards, :messages, :news,
30 :attachments, :enumerations
30 :attachments, :enumerations
31
31
32 def setup
32 def setup
33 super
33 super
34 set_tmp_attachments_directory
34 set_tmp_attachments_directory
35 end
35 end
36
36
37 context "#link_to_if_authorized" do
37 context "#link_to_if_authorized" do
38 context "authorized user" do
38 context "authorized user" do
39 should "be tested"
39 should "be tested"
40 end
40 end
41
41
42 context "unauthorized user" do
42 context "unauthorized user" do
43 should "be tested"
43 should "be tested"
44 end
44 end
45
45
46 should "allow using the :controller and :action for the target link" do
46 should "allow using the :controller and :action for the target link" do
47 User.current = User.find_by_login('admin')
47 User.current = User.find_by_login('admin')
48
48
49 @project = Issue.first.project # Used by helper
49 @project = Issue.first.project # Used by helper
50 response = link_to_if_authorized("By controller/action",
50 response = link_to_if_authorized("By controller/action",
51 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
51 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
52 assert_match /href/, response
52 assert_match /href/, response
53 end
53 end
54
54
55 end
55 end
56
56
57 def test_auto_links
57 def test_auto_links
58 to_test = {
58 to_test = {
59 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
59 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
60 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
60 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
61 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
61 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
62 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
62 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
63 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
63 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
64 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
64 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
65 '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>.',
65 '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>.',
66 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
66 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
67 '(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>)',
67 '(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>)',
68 '(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>)',
68 '(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>)',
69 '(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>).',
69 '(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>).',
70 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
70 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
71 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
71 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
72 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
72 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
73 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
73 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
74 '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>',
74 '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>',
75 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
75 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
76 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
76 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
77 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
77 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
78 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
78 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
79 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
79 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
80 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
80 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
81 # two exclamation marks
81 # two exclamation marks
82 '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>',
82 '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>',
83 # escaping
83 # escaping
84 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
84 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
85 # wrap in angle brackets
85 # wrap in angle brackets
86 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
86 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
87 }
87 }
88 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
88 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
89 end
89 end
90
90
91 def test_auto_mailto
91 def test_auto_mailto
92 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
92 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
93 textilizable('test@foo.bar')
93 textilizable('test@foo.bar')
94 end
94 end
95
95
96 def test_inline_images
96 def test_inline_images
97 to_test = {
97 to_test = {
98 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
98 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
99 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
99 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
100 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
100 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
101 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
101 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
102 '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" />',
102 '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" />',
103 '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;" />',
103 '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;" />',
104 }
104 }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 end
106 end
107
107
108 def test_inline_images_inside_tags
108 def test_inline_images_inside_tags
109 raw = <<-RAW
109 raw = <<-RAW
110 h1. !foo.png! Heading
110 h1. !foo.png! Heading
111
111
112 Centered image:
112 Centered image:
113
113
114 p=. !bar.gif!
114 p=. !bar.gif!
115 RAW
115 RAW
116
116
117 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
117 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
118 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
118 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
119 end
119 end
120
120
121 def test_attached_images
121 def test_attached_images
122 to_test = {
122 to_test = {
123 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
123 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
124 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
124 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
125 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
125 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
126 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
126 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
127 # link image
127 # link image
128 '!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>',
128 '!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>',
129 }
129 }
130 attachments = Attachment.find(:all)
130 attachments = Attachment.find(:all)
131 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
131 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
132 end
132 end
133
133
134 def test_attached_images_filename_extension
134 def test_attached_images_filename_extension
135 set_tmp_attachments_directory
135 set_tmp_attachments_directory
136 a1 = Attachment.new(
136 a1 = Attachment.new(
137 :container => Issue.find(1),
137 :container => Issue.find(1),
138 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
138 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
139 :author => User.find(1))
139 :author => User.find(1))
140 assert a1.save
140 assert a1.save
141 assert_equal "testtest.JPG", a1.filename
141 assert_equal "testtest.JPG", a1.filename
142 assert_equal "image/jpeg", a1.content_type
142 assert_equal "image/jpeg", a1.content_type
143 assert a1.image?
143 assert a1.image?
144
144
145 a2 = Attachment.new(
145 a2 = Attachment.new(
146 :container => Issue.find(1),
146 :container => Issue.find(1),
147 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
147 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
148 :author => User.find(1))
148 :author => User.find(1))
149 assert a2.save
149 assert a2.save
150 assert_equal "testtest.jpeg", a2.filename
150 assert_equal "testtest.jpeg", a2.filename
151 assert_equal "image/jpeg", a2.content_type
151 assert_equal "image/jpeg", a2.content_type
152 assert a2.image?
152 assert a2.image?
153
153
154 a3 = Attachment.new(
154 a3 = Attachment.new(
155 :container => Issue.find(1),
155 :container => Issue.find(1),
156 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
156 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
157 :author => User.find(1))
157 :author => User.find(1))
158 assert a3.save
158 assert a3.save
159 assert_equal "testtest.JPE", a3.filename
159 assert_equal "testtest.JPE", a3.filename
160 assert_equal "image/jpeg", a3.content_type
160 assert_equal "image/jpeg", a3.content_type
161 assert a3.image?
161 assert a3.image?
162
162
163 a4 = Attachment.new(
163 a4 = Attachment.new(
164 :container => Issue.find(1),
164 :container => Issue.find(1),
165 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
165 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
166 :author => User.find(1))
166 :author => User.find(1))
167 assert a4.save
167 assert a4.save
168 assert_equal "Testtest.BMP", a4.filename
168 assert_equal "Testtest.BMP", a4.filename
169 assert_equal "image/x-ms-bmp", a4.content_type
169 assert_equal "image/x-ms-bmp", a4.content_type
170 assert a4.image?
170 assert a4.image?
171
171
172 to_test = {
172 to_test = {
173 'Inline image: !testtest.jpg!' =>
173 'Inline image: !testtest.jpg!' =>
174 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
174 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
175 'Inline image: !testtest.jpeg!' =>
175 'Inline image: !testtest.jpeg!' =>
176 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
176 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
177 'Inline image: !testtest.jpe!' =>
177 'Inline image: !testtest.jpe!' =>
178 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
178 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
179 'Inline image: !testtest.bmp!' =>
179 'Inline image: !testtest.bmp!' =>
180 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
180 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
181 }
181 }
182
182
183 attachments = [a1, a2, a3, a4]
183 attachments = [a1, a2, a3, a4]
184 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
184 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
185 end
185 end
186
186
187 def test_attached_images_should_read_later
187 def test_attached_images_should_read_later
188 set_fixtures_attachments_directory
188 set_fixtures_attachments_directory
189 a1 = Attachment.find(16)
189 a1 = Attachment.find(16)
190 assert_equal "testfile.png", a1.filename
190 assert_equal "testfile.png", a1.filename
191 assert a1.readable?
191 assert a1.readable?
192 assert (! a1.visible?(User.anonymous))
192 assert (! a1.visible?(User.anonymous))
193 assert a1.visible?(User.find(2))
193 assert a1.visible?(User.find(2))
194 a2 = Attachment.find(17)
194 a2 = Attachment.find(17)
195 assert_equal "testfile.PNG", a2.filename
195 assert_equal "testfile.PNG", a2.filename
196 assert a2.readable?
196 assert a2.readable?
197 assert (! a2.visible?(User.anonymous))
197 assert (! a2.visible?(User.anonymous))
198 assert a2.visible?(User.find(2))
198 assert a2.visible?(User.find(2))
199 assert a1.created_on < a2.created_on
199 assert a1.created_on < a2.created_on
200
200
201 to_test = {
201 to_test = {
202 'Inline image: !testfile.png!' =>
202 'Inline image: !testfile.png!' =>
203 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
203 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
204 'Inline image: !Testfile.PNG!' =>
204 'Inline image: !Testfile.PNG!' =>
205 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
205 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
206 }
206 }
207 attachments = [a1, a2]
207 attachments = [a1, a2]
208 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
208 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
209 set_tmp_attachments_directory
209 set_tmp_attachments_directory
210 end
210 end
211
211
212 def test_textile_external_links
212 def test_textile_external_links
213 to_test = {
213 to_test = {
214 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
214 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
215 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
215 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
216 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
216 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
217 '"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>',
217 '"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>',
218 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
218 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
219 # no multiline link text
219 # no multiline link text
220 "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",
220 "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",
221 # mailto link
221 # mailto link
222 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
222 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
223 # two exclamation marks
223 # two exclamation marks
224 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
224 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
225 # escaping
225 # escaping
226 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
226 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
227 }
227 }
228 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
228 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
229 end
229 end
230
230
231 def test_redmine_links
231 def test_redmine_links
232 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
232 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
233 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
233 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
234 note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
234 note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
235 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
235 :class => 'issue status-1 priority-4 overdue', :title => 'Error 281 when updating a recipe (New)')
236
236
237 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
237 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
238 :class => 'changeset', :title => 'My very first commit')
238 :class => 'changeset', :title => 'My very first commit')
239 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
239 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
240 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
240 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
241
241
242 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
242 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
243 :class => 'document')
243 :class => 'document')
244
244
245 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
245 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
246 :class => 'version')
246 :class => 'version')
247
247
248 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
248 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
249
249
250 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
250 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
251
251
252 news_url = {:controller => 'news', :action => 'show', :id => 1}
252 news_url = {:controller => 'news', :action => 'show', :id => 1}
253
253
254 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
254 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
255
255
256 source_url = '/projects/ecookbook/repository/entry/some/file'
256 source_url = '/projects/ecookbook/repository/entry/some/file'
257 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
257 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
258 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
258 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
259 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
259 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
260
260
261 export_url = '/projects/ecookbook/repository/raw/some/file'
261 export_url = '/projects/ecookbook/repository/raw/some/file'
262 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
262 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
263 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
263 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
264 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
264 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
265
265
266 to_test = {
266 to_test = {
267 # tickets
267 # tickets
268 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
268 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
269 # ticket notes
269 # ticket notes
270 '#3-14' => note_link,
270 '#3-14' => note_link,
271 '#3#note-14' => note_link,
271 '#3#note-14' => note_link,
272 # should not ignore leading zero
272 # should not ignore leading zero
273 '#03' => '#03',
273 '#03' => '#03',
274 # changesets
274 # changesets
275 'r1' => changeset_link,
275 'r1' => changeset_link,
276 'r1.' => "#{changeset_link}.",
276 'r1.' => "#{changeset_link}.",
277 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
277 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
278 'r1,r2' => "#{changeset_link},#{changeset_link2}",
278 'r1,r2' => "#{changeset_link},#{changeset_link2}",
279 # documents
279 # documents
280 'document#1' => document_link,
280 'document#1' => document_link,
281 'document:"Test document"' => document_link,
281 'document:"Test document"' => document_link,
282 # versions
282 # versions
283 'version#2' => version_link,
283 'version#2' => version_link,
284 'version:1.0' => version_link,
284 'version:1.0' => version_link,
285 'version:"1.0"' => version_link,
285 'version:"1.0"' => version_link,
286 # source
286 # source
287 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
287 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
288 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
288 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
289 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
289 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
290 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
290 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
291 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
291 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
292 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
292 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
293 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
293 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
294 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
294 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
295 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
295 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
296 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
296 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
297 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
297 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
298 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
298 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
299 # export
299 # export
300 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
300 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
301 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
301 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
302 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
302 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
303 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
303 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
304 # forum
304 # forum
305 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
305 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
306 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
306 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
307 # message
307 # message
308 'message#4' => link_to('Post 2', message_url, :class => 'message'),
308 'message#4' => link_to('Post 2', message_url, :class => 'message'),
309 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
309 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
310 # news
310 # news
311 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
311 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
312 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
312 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
313 # project
313 # project
314 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
314 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
315 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
315 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
316 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
316 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
317 # not found
317 # not found
318 '#0123456789' => '#0123456789',
318 '#0123456789' => '#0123456789',
319 # invalid expressions
319 # invalid expressions
320 'source:' => 'source:',
320 'source:' => 'source:',
321 # url hash
321 # url hash
322 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
322 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
323 }
323 }
324 @project = Project.find(1)
324 @project = Project.find(1)
325 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
325 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
326 end
326 end
327
327
328 def test_escaped_redmine_links_should_not_be_parsed
328 def test_escaped_redmine_links_should_not_be_parsed
329 to_test = [
329 to_test = [
330 '#3.',
330 '#3.',
331 '#3-14.',
331 '#3-14.',
332 '#3#-note14.',
332 '#3#-note14.',
333 'r1',
333 'r1',
334 'document#1',
334 'document#1',
335 'document:"Test document"',
335 'document:"Test document"',
336 'version#2',
336 'version#2',
337 'version:1.0',
337 'version:1.0',
338 'version:"1.0"',
338 'version:"1.0"',
339 'source:/some/file'
339 'source:/some/file'
340 ]
340 ]
341 @project = Project.find(1)
341 @project = Project.find(1)
342 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
342 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
343 end
343 end
344
344
345 def test_cross_project_redmine_links
345 def test_cross_project_redmine_links
346 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
346 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
347 :class => 'source')
347 :class => 'source')
348
348
349 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
349 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
350 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
350 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
351
351
352 to_test = {
352 to_test = {
353 # documents
353 # documents
354 'document:"Test document"' => 'document:"Test document"',
354 'document:"Test document"' => 'document:"Test document"',
355 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
355 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
356 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
356 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
357 # versions
357 # versions
358 'version:"1.0"' => 'version:"1.0"',
358 'version:"1.0"' => 'version:"1.0"',
359 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
359 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
360 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
360 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
361 # changeset
361 # changeset
362 'r2' => 'r2',
362 'r2' => 'r2',
363 'ecookbook:r2' => changeset_link,
363 'ecookbook:r2' => changeset_link,
364 'invalid:r2' => 'invalid:r2',
364 'invalid:r2' => 'invalid:r2',
365 # source
365 # source
366 'source:/some/file' => 'source:/some/file',
366 'source:/some/file' => 'source:/some/file',
367 'ecookbook:source:/some/file' => source_link,
367 'ecookbook:source:/some/file' => source_link,
368 'invalid:source:/some/file' => 'invalid:source:/some/file',
368 'invalid:source:/some/file' => 'invalid:source:/some/file',
369 }
369 }
370 @project = Project.find(3)
370 @project = Project.find(3)
371 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
371 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
372 end
372 end
373
373
374 def test_multiple_repositories_redmine_links
374 def test_multiple_repositories_redmine_links
375 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
375 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
376 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
376 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
377 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
377 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
378 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
378 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
379
379
380 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
380 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
381 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
381 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
382 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
382 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
383 :class => 'changeset', :title => '')
383 :class => 'changeset', :title => '')
384 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
384 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
385 :class => 'changeset', :title => '')
385 :class => 'changeset', :title => '')
386
386
387 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
387 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
388 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
388 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
389
389
390 to_test = {
390 to_test = {
391 'r2' => changeset_link,
391 'r2' => changeset_link,
392 'svn1|r123' => svn_changeset_link,
392 'svn1|r123' => svn_changeset_link,
393 'invalid|r123' => 'invalid|r123',
393 'invalid|r123' => 'invalid|r123',
394 'commit:hg1|abcd' => hg_changeset_link,
394 'commit:hg1|abcd' => hg_changeset_link,
395 'commit:invalid|abcd' => 'commit:invalid|abcd',
395 'commit:invalid|abcd' => 'commit:invalid|abcd',
396 # source
396 # source
397 'source:some/file' => source_link,
397 'source:some/file' => source_link,
398 'source:hg1|some/file' => hg_source_link,
398 'source:hg1|some/file' => hg_source_link,
399 'source:invalid|some/file' => 'source:invalid|some/file',
399 'source:invalid|some/file' => 'source:invalid|some/file',
400 }
400 }
401
401
402 @project = Project.find(1)
402 @project = Project.find(1)
403 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
403 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
404 end
404 end
405
405
406 def test_cross_project_multiple_repositories_redmine_links
406 def test_cross_project_multiple_repositories_redmine_links
407 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
407 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
408 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
408 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
409 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
409 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
410 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
410 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
411
411
412 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
412 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
413 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
413 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
414 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
414 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
415 :class => 'changeset', :title => '')
415 :class => 'changeset', :title => '')
416 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
416 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
417 :class => 'changeset', :title => '')
417 :class => 'changeset', :title => '')
418
418
419 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
419 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
420 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
420 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
421
421
422 to_test = {
422 to_test = {
423 'ecookbook:r2' => changeset_link,
423 'ecookbook:r2' => changeset_link,
424 'ecookbook:svn1|r123' => svn_changeset_link,
424 'ecookbook:svn1|r123' => svn_changeset_link,
425 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
425 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
426 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
426 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
427 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
427 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
428 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
428 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
429 # source
429 # source
430 'ecookbook:source:some/file' => source_link,
430 'ecookbook:source:some/file' => source_link,
431 'ecookbook:source:hg1|some/file' => hg_source_link,
431 'ecookbook:source:hg1|some/file' => hg_source_link,
432 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
432 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
433 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
433 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
434 }
434 }
435
435
436 @project = Project.find(3)
436 @project = Project.find(3)
437 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
437 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
438 end
438 end
439
439
440 def test_redmine_links_git_commit
440 def test_redmine_links_git_commit
441 changeset_link = link_to('abcd',
441 changeset_link = link_to('abcd',
442 {
442 {
443 :controller => 'repositories',
443 :controller => 'repositories',
444 :action => 'revision',
444 :action => 'revision',
445 :id => 'subproject1',
445 :id => 'subproject1',
446 :rev => 'abcd',
446 :rev => 'abcd',
447 },
447 },
448 :class => 'changeset', :title => 'test commit')
448 :class => 'changeset', :title => 'test commit')
449 to_test = {
449 to_test = {
450 'commit:abcd' => changeset_link,
450 'commit:abcd' => changeset_link,
451 }
451 }
452 @project = Project.find(3)
452 @project = Project.find(3)
453 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
453 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
454 assert r
454 assert r
455 c = Changeset.new(:repository => r,
455 c = Changeset.new(:repository => r,
456 :committed_on => Time.now,
456 :committed_on => Time.now,
457 :revision => 'abcd',
457 :revision => 'abcd',
458 :scmid => 'abcd',
458 :scmid => 'abcd',
459 :comments => 'test commit')
459 :comments => 'test commit')
460 assert( c.save )
460 assert( c.save )
461 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
461 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
462 end
462 end
463
463
464 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
464 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
465 def test_redmine_links_darcs_commit
465 def test_redmine_links_darcs_commit
466 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
466 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
467 {
467 {
468 :controller => 'repositories',
468 :controller => 'repositories',
469 :action => 'revision',
469 :action => 'revision',
470 :id => 'subproject1',
470 :id => 'subproject1',
471 :rev => '123',
471 :rev => '123',
472 },
472 },
473 :class => 'changeset', :title => 'test commit')
473 :class => 'changeset', :title => 'test commit')
474 to_test = {
474 to_test = {
475 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
475 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
476 }
476 }
477 @project = Project.find(3)
477 @project = Project.find(3)
478 r = Repository::Darcs.create!(
478 r = Repository::Darcs.create!(
479 :project => @project, :url => '/tmp/test/darcs',
479 :project => @project, :url => '/tmp/test/darcs',
480 :log_encoding => 'UTF-8')
480 :log_encoding => 'UTF-8')
481 assert r
481 assert r
482 c = Changeset.new(:repository => r,
482 c = Changeset.new(:repository => r,
483 :committed_on => Time.now,
483 :committed_on => Time.now,
484 :revision => '123',
484 :revision => '123',
485 :scmid => '20080308225258-98289-abcd456efg.gz',
485 :scmid => '20080308225258-98289-abcd456efg.gz',
486 :comments => 'test commit')
486 :comments => 'test commit')
487 assert( c.save )
487 assert( c.save )
488 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
488 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
489 end
489 end
490
490
491 def test_redmine_links_mercurial_commit
491 def test_redmine_links_mercurial_commit
492 changeset_link_rev = link_to('r123',
492 changeset_link_rev = link_to('r123',
493 {
493 {
494 :controller => 'repositories',
494 :controller => 'repositories',
495 :action => 'revision',
495 :action => 'revision',
496 :id => 'subproject1',
496 :id => 'subproject1',
497 :rev => '123' ,
497 :rev => '123' ,
498 },
498 },
499 :class => 'changeset', :title => 'test commit')
499 :class => 'changeset', :title => 'test commit')
500 changeset_link_commit = link_to('abcd',
500 changeset_link_commit = link_to('abcd',
501 {
501 {
502 :controller => 'repositories',
502 :controller => 'repositories',
503 :action => 'revision',
503 :action => 'revision',
504 :id => 'subproject1',
504 :id => 'subproject1',
505 :rev => 'abcd' ,
505 :rev => 'abcd' ,
506 },
506 },
507 :class => 'changeset', :title => 'test commit')
507 :class => 'changeset', :title => 'test commit')
508 to_test = {
508 to_test = {
509 'r123' => changeset_link_rev,
509 'r123' => changeset_link_rev,
510 'commit:abcd' => changeset_link_commit,
510 'commit:abcd' => changeset_link_commit,
511 }
511 }
512 @project = Project.find(3)
512 @project = Project.find(3)
513 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
513 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
514 assert r
514 assert r
515 c = Changeset.new(:repository => r,
515 c = Changeset.new(:repository => r,
516 :committed_on => Time.now,
516 :committed_on => Time.now,
517 :revision => '123',
517 :revision => '123',
518 :scmid => 'abcd',
518 :scmid => 'abcd',
519 :comments => 'test commit')
519 :comments => 'test commit')
520 assert( c.save )
520 assert( c.save )
521 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
521 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
522 end
522 end
523
523
524 def test_attachment_links
524 def test_attachment_links
525 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
525 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
526 to_test = {
526 to_test = {
527 'attachment:error281.txt' => attachment_link
527 'attachment:error281.txt' => attachment_link
528 }
528 }
529 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
529 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
530 end
530 end
531
531
532 def test_wiki_links
532 def test_wiki_links
533 to_test = {
533 to_test = {
534 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
534 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
535 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
535 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
536 # title content should be formatted
536 # title content should be formatted
537 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
537 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
538 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
538 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
539 # link with anchor
539 # link with anchor
540 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
540 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
541 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
541 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
542 # UTF8 anchor
542 # UTF8 anchor
543 '[[Another_page#ВСст|ВСст]]' => %|<a href="/projects/ecookbook/wiki/Another_page##{CGI.escape 'ВСст'}" class="wiki-page">ВСст</a>|,
543 '[[Another_page#ВСст|ВСст]]' => %|<a href="/projects/ecookbook/wiki/Another_page##{CGI.escape 'ВСст'}" class="wiki-page">ВСст</a>|,
544 # page that doesn't exist
544 # page that doesn't exist
545 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
545 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
546 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
546 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
547 # link to another project wiki
547 # link to another project wiki
548 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
548 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
549 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
549 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
550 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
550 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
551 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
551 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
552 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
552 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
553 # striked through link
553 # striked through link
554 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
554 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
555 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
555 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
556 # escaping
556 # escaping
557 '![[Another page|Page]]' => '[[Another page|Page]]',
557 '![[Another page|Page]]' => '[[Another page|Page]]',
558 # project does not exist
558 # project does not exist
559 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
559 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
560 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
560 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
561 }
561 }
562
562
563 @project = Project.find(1)
563 @project = Project.find(1)
564 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
564 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
565 end
565 end
566
566
567 def test_wiki_links_within_local_file_generation_context
567 def test_wiki_links_within_local_file_generation_context
568
568
569 to_test = {
569 to_test = {
570 # link to a page
570 # link to a page
571 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
571 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
572 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
572 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
573 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
573 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
574 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
574 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
575 # page that doesn't exist
575 # page that doesn't exist
576 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
576 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
577 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
577 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
578 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
578 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
579 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
579 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
580 }
580 }
581
581
582 @project = Project.find(1)
582 @project = Project.find(1)
583
583
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
585 end
585 end
586
586
587 def test_wiki_links_within_wiki_page_context
587 def test_wiki_links_within_wiki_page_context
588
588
589 page = WikiPage.find_by_title('Another_page' )
589 page = WikiPage.find_by_title('Another_page' )
590
590
591 to_test = {
591 to_test = {
592 # link to another page
592 # link to another page
593 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
593 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
594 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
594 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
595 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
595 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
596 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
596 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
597 # link to the current page
597 # link to the current page
598 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
598 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
599 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
599 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
600 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
600 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
601 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
601 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
602 # page that doesn't exist
602 # page that doesn't exist
603 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
603 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
604 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
604 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
605 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
605 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
606 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
606 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
607 }
607 }
608
608
609 @project = Project.find(1)
609 @project = Project.find(1)
610
610
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
612 end
612 end
613
613
614 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
614 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
615
615
616 to_test = {
616 to_test = {
617 # link to a page
617 # link to a page
618 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
618 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
619 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
619 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
620 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
620 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
621 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
621 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
622 # page that doesn't exist
622 # page that doesn't exist
623 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
623 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
624 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
624 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
625 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
625 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
626 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
626 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
627 }
627 }
628
628
629 @project = Project.find(1)
629 @project = Project.find(1)
630
630
631 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
631 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
632 end
632 end
633
633
634 def test_html_tags
634 def test_html_tags
635 to_test = {
635 to_test = {
636 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
636 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
637 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
637 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
638 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
638 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
639 # do not escape pre/code tags
639 # do not escape pre/code tags
640 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
640 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
641 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
641 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
642 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
642 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
643 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
643 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
644 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
644 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
645 # remove attributes except class
645 # remove attributes except class
646 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
646 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
647 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
647 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
648 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
648 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
649 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
649 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
650 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
650 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
651 # xss
651 # xss
652 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
652 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
653 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
653 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
654 }
654 }
655 to_test.each { |text, result| assert_equal result, textilizable(text) }
655 to_test.each { |text, result| assert_equal result, textilizable(text) }
656 end
656 end
657
657
658 def test_allowed_html_tags
658 def test_allowed_html_tags
659 to_test = {
659 to_test = {
660 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
660 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
661 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
661 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
662 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
662 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
663 }
663 }
664 to_test.each { |text, result| assert_equal result, textilizable(text) }
664 to_test.each { |text, result| assert_equal result, textilizable(text) }
665 end
665 end
666
666
667 def test_pre_tags
667 def test_pre_tags
668 raw = <<-RAW
668 raw = <<-RAW
669 Before
669 Before
670
670
671 <pre>
671 <pre>
672 <prepared-statement-cache-size>32</prepared-statement-cache-size>
672 <prepared-statement-cache-size>32</prepared-statement-cache-size>
673 </pre>
673 </pre>
674
674
675 After
675 After
676 RAW
676 RAW
677
677
678 expected = <<-EXPECTED
678 expected = <<-EXPECTED
679 <p>Before</p>
679 <p>Before</p>
680 <pre>
680 <pre>
681 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
681 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
682 </pre>
682 </pre>
683 <p>After</p>
683 <p>After</p>
684 EXPECTED
684 EXPECTED
685
685
686 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
686 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
687 end
687 end
688
688
689 def test_pre_content_should_not_parse_wiki_and_redmine_links
689 def test_pre_content_should_not_parse_wiki_and_redmine_links
690 raw = <<-RAW
690 raw = <<-RAW
691 [[CookBook documentation]]
691 [[CookBook documentation]]
692
692
693 #1
693 #1
694
694
695 <pre>
695 <pre>
696 [[CookBook documentation]]
696 [[CookBook documentation]]
697
697
698 #1
698 #1
699 </pre>
699 </pre>
700 RAW
700 RAW
701
701
702 expected = <<-EXPECTED
702 expected = <<-EXPECTED
703 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
703 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
704 <p><a href="/issues/1" class="issue status-1 priority-4" title="Can&#x27;t print recipes (New)">#1</a></p>
704 <p><a href="/issues/1" class="issue status-1 priority-4" title="Can&#x27;t print recipes (New)">#1</a></p>
705 <pre>
705 <pre>
706 [[CookBook documentation]]
706 [[CookBook documentation]]
707
707
708 #1
708 #1
709 </pre>
709 </pre>
710 EXPECTED
710 EXPECTED
711
711
712 @project = Project.find(1)
712 @project = Project.find(1)
713 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
713 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
714 end
714 end
715
715
716 def test_non_closing_pre_blocks_should_be_closed
716 def test_non_closing_pre_blocks_should_be_closed
717 raw = <<-RAW
717 raw = <<-RAW
718 <pre><code>
718 <pre><code>
719 RAW
719 RAW
720
720
721 expected = <<-EXPECTED
721 expected = <<-EXPECTED
722 <pre><code>
722 <pre><code>
723 </code></pre>
723 </code></pre>
724 EXPECTED
724 EXPECTED
725
725
726 @project = Project.find(1)
726 @project = Project.find(1)
727 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
727 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
728 end
728 end
729
729
730 def test_syntax_highlight
730 def test_syntax_highlight
731 raw = <<-RAW
731 raw = <<-RAW
732 <pre><code class="ruby">
732 <pre><code class="ruby">
733 # Some ruby code here
733 # Some ruby code here
734 </code></pre>
734 </code></pre>
735 RAW
735 RAW
736
736
737 expected = <<-EXPECTED
737 expected = <<-EXPECTED
738 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
738 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
739 </code></pre>
739 </code></pre>
740 EXPECTED
740 EXPECTED
741
741
742 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
742 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
743 end
743 end
744
744
745 def test_to_path_param
745 def test_to_path_param
746 assert_equal 'test1/test2', to_path_param('test1/test2')
746 assert_equal 'test1/test2', to_path_param('test1/test2')
747 assert_equal 'test1/test2', to_path_param('/test1/test2/')
747 assert_equal 'test1/test2', to_path_param('/test1/test2/')
748 assert_equal 'test1/test2', to_path_param('//test1/test2/')
748 assert_equal 'test1/test2', to_path_param('//test1/test2/')
749 assert_equal nil, to_path_param('/')
749 assert_equal nil, to_path_param('/')
750 end
750 end
751
751
752 def test_wiki_links_in_tables
752 def test_wiki_links_in_tables
753 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
753 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
754 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
754 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
755 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
755 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
756 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
756 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
757 }
757 }
758 @project = Project.find(1)
758 @project = Project.find(1)
759 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
759 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
760 end
760 end
761
761
762 def test_text_formatting
762 def test_text_formatting
763 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
763 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
764 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
764 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
765 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
765 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
766 '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>',
766 '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>',
767 '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',
767 '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',
768 }
768 }
769 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
769 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
770 end
770 end
771
771
772 def test_wiki_horizontal_rule
772 def test_wiki_horizontal_rule
773 assert_equal '<hr />', textilizable('---')
773 assert_equal '<hr />', textilizable('---')
774 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
774 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
775 end
775 end
776
776
777 def test_footnotes
777 def test_footnotes
778 raw = <<-RAW
778 raw = <<-RAW
779 This is some text[1].
779 This is some text[1].
780
780
781 fn1. This is the foot note
781 fn1. This is the foot note
782 RAW
782 RAW
783
783
784 expected = <<-EXPECTED
784 expected = <<-EXPECTED
785 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
785 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
786 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
786 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
787 EXPECTED
787 EXPECTED
788
788
789 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
789 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
790 end
790 end
791
791
792 def test_headings
792 def test_headings
793 raw = 'h1. Some heading'
793 raw = 'h1. Some heading'
794 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
794 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
795
795
796 assert_equal expected, textilizable(raw)
796 assert_equal expected, textilizable(raw)
797 end
797 end
798
798
799 def test_headings_with_special_chars
799 def test_headings_with_special_chars
800 # This test makes sure that the generated anchor names match the expected
800 # This test makes sure that the generated anchor names match the expected
801 # ones even if the heading text contains unconventional characters
801 # ones even if the heading text contains unconventional characters
802 raw = 'h1. Some heading related to version 0.5'
802 raw = 'h1. Some heading related to version 0.5'
803 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
803 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
804 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
804 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
805
805
806 assert_equal expected, textilizable(raw)
806 assert_equal expected, textilizable(raw)
807 end
807 end
808
808
809 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
809 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
810 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
810 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
811 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
811 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
812
812
813 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
813 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
814
814
815 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
815 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
816 end
816 end
817
817
818 def test_table_of_content
818 def test_table_of_content
819 raw = <<-RAW
819 raw = <<-RAW
820 {{toc}}
820 {{toc}}
821
821
822 h1. Title
822 h1. Title
823
823
824 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
824 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
825
825
826 h2. Subtitle with a [[Wiki]] link
826 h2. Subtitle with a [[Wiki]] link
827
827
828 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
828 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
829
829
830 h2. Subtitle with [[Wiki|another Wiki]] link
830 h2. Subtitle with [[Wiki|another Wiki]] link
831
831
832 h2. Subtitle with %{color:red}red text%
832 h2. Subtitle with %{color:red}red text%
833
833
834 <pre>
834 <pre>
835 some code
835 some code
836 </pre>
836 </pre>
837
837
838 h3. Subtitle with *some* _modifiers_
838 h3. Subtitle with *some* _modifiers_
839
839
840 h3. Subtitle with @inline code@
840 h3. Subtitle with @inline code@
841
841
842 h1. Another title
842 h1. Another title
843
843
844 h3. An "Internet link":http://www.redmine.org/ inside subtitle
844 h3. An "Internet link":http://www.redmine.org/ inside subtitle
845
845
846 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
846 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
847
847
848 RAW
848 RAW
849
849
850 expected = '<ul class="toc">' +
850 expected = '<ul class="toc">' +
851 '<li><a href="#Title">Title</a>' +
851 '<li><a href="#Title">Title</a>' +
852 '<ul>' +
852 '<ul>' +
853 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
853 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
854 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
854 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
855 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
855 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
856 '<ul>' +
856 '<ul>' +
857 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
857 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
858 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
858 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
859 '</ul>' +
859 '</ul>' +
860 '</li>' +
860 '</li>' +
861 '</ul>' +
861 '</ul>' +
862 '</li>' +
862 '</li>' +
863 '<li><a href="#Another-title">Another title</a>' +
863 '<li><a href="#Another-title">Another title</a>' +
864 '<ul>' +
864 '<ul>' +
865 '<li>' +
865 '<li>' +
866 '<ul>' +
866 '<ul>' +
867 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
867 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
868 '</ul>' +
868 '</ul>' +
869 '</li>' +
869 '</li>' +
870 '<li><a href="#Project-Name">Project Name</a></li>' +
870 '<li><a href="#Project-Name">Project Name</a></li>' +
871 '</ul>' +
871 '</ul>' +
872 '</li>' +
872 '</li>' +
873 '</ul>'
873 '</ul>'
874
874
875 @project = Project.find(1)
875 @project = Project.find(1)
876 assert textilizable(raw).gsub("\n", "").include?(expected)
876 assert textilizable(raw).gsub("\n", "").include?(expected)
877 end
877 end
878
878
879 def test_table_of_content_should_generate_unique_anchors
879 def test_table_of_content_should_generate_unique_anchors
880 raw = <<-RAW
880 raw = <<-RAW
881 {{toc}}
881 {{toc}}
882
882
883 h1. Title
883 h1. Title
884
884
885 h2. Subtitle
885 h2. Subtitle
886
886
887 h2. Subtitle
887 h2. Subtitle
888 RAW
888 RAW
889
889
890 expected = '<ul class="toc">' +
890 expected = '<ul class="toc">' +
891 '<li><a href="#Title">Title</a>' +
891 '<li><a href="#Title">Title</a>' +
892 '<ul>' +
892 '<ul>' +
893 '<li><a href="#Subtitle">Subtitle</a></li>' +
893 '<li><a href="#Subtitle">Subtitle</a></li>' +
894 '<li><a href="#Subtitle-2">Subtitle</a></li>'
894 '<li><a href="#Subtitle-2">Subtitle</a></li>'
895 '</ul>'
895 '</ul>'
896 '</li>' +
896 '</li>' +
897 '</ul>'
897 '</ul>'
898
898
899 @project = Project.find(1)
899 @project = Project.find(1)
900 result = textilizable(raw).gsub("\n", "")
900 result = textilizable(raw).gsub("\n", "")
901 assert_include expected, result
901 assert_include expected, result
902 assert_include '<a name="Subtitle">', result
902 assert_include '<a name="Subtitle">', result
903 assert_include '<a name="Subtitle-2">', result
903 assert_include '<a name="Subtitle-2">', result
904 end
904 end
905
905
906 def test_table_of_content_should_contain_included_page_headings
906 def test_table_of_content_should_contain_included_page_headings
907 raw = <<-RAW
907 raw = <<-RAW
908 {{toc}}
908 {{toc}}
909
909
910 h1. Included
910 h1. Included
911
911
912 {{include(Child_1)}}
912 {{include(Child_1)}}
913 RAW
913 RAW
914
914
915 expected = '<ul class="toc">' +
915 expected = '<ul class="toc">' +
916 '<li><a href="#Included">Included</a></li>' +
916 '<li><a href="#Included">Included</a></li>' +
917 '<li><a href="#Child-page-1">Child page 1</a></li>' +
917 '<li><a href="#Child-page-1">Child page 1</a></li>' +
918 '</ul>'
918 '</ul>'
919
919
920 @project = Project.find(1)
920 @project = Project.find(1)
921 assert textilizable(raw).gsub("\n", "").include?(expected)
921 assert textilizable(raw).gsub("\n", "").include?(expected)
922 end
922 end
923
923
924 def test_section_edit_links
924 def test_section_edit_links
925 raw = <<-RAW
925 raw = <<-RAW
926 h1. Title
926 h1. Title
927
927
928 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
928 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
929
929
930 h2. Subtitle with a [[Wiki]] link
930 h2. Subtitle with a [[Wiki]] link
931
931
932 h2. Subtitle with *some* _modifiers_
932 h2. Subtitle with *some* _modifiers_
933
933
934 h2. Subtitle with @inline code@
934 h2. Subtitle with @inline code@
935
935
936 <pre>
936 <pre>
937 some code
937 some code
938
938
939 h2. heading inside pre
939 h2. heading inside pre
940
940
941 <h2>html heading inside pre</h2>
941 <h2>html heading inside pre</h2>
942 </pre>
942 </pre>
943
943
944 h2. Subtitle after pre tag
944 h2. Subtitle after pre tag
945 RAW
945 RAW
946
946
947 @project = Project.find(1)
947 @project = Project.find(1)
948 set_language_if_valid 'en'
948 set_language_if_valid 'en'
949 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
949 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
950
950
951 # heading that contains inline code
951 # heading that contains inline code
952 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
952 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
953 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
953 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
954 '<a name="Subtitle-with-inline-code"></a>' +
954 '<a name="Subtitle-with-inline-code"></a>' +
955 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
955 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
956 result
956 result
957
957
958 # last heading
958 # last heading
959 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
959 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
960 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
960 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
961 '<a name="Subtitle-after-pre-tag"></a>' +
961 '<a name="Subtitle-after-pre-tag"></a>' +
962 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
962 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
963 result
963 result
964 end
964 end
965
965
966 def test_default_formatter
966 def test_default_formatter
967 with_settings :text_formatting => 'unknown' do
967 with_settings :text_formatting => 'unknown' do
968 text = 'a *link*: http://www.example.net/'
968 text = 'a *link*: http://www.example.net/'
969 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
969 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
970 end
970 end
971 end
971 end
972
972
973 def test_due_date_distance_in_words
973 def test_due_date_distance_in_words
974 to_test = { Date.today => 'Due in 0 days',
974 to_test = { Date.today => 'Due in 0 days',
975 Date.today + 1 => 'Due in 1 day',
975 Date.today + 1 => 'Due in 1 day',
976 Date.today + 100 => 'Due in about 3 months',
976 Date.today + 100 => 'Due in about 3 months',
977 Date.today + 20000 => 'Due in over 54 years',
977 Date.today + 20000 => 'Due in over 54 years',
978 Date.today - 1 => '1 day late',
978 Date.today - 1 => '1 day late',
979 Date.today - 100 => 'about 3 months late',
979 Date.today - 100 => 'about 3 months late',
980 Date.today - 20000 => 'over 54 years late',
980 Date.today - 20000 => 'over 54 years late',
981 }
981 }
982 ::I18n.locale = :en
982 ::I18n.locale = :en
983 to_test.each do |date, expected|
983 to_test.each do |date, expected|
984 assert_equal expected, due_date_distance_in_words(date)
984 assert_equal expected, due_date_distance_in_words(date)
985 end
985 end
986 end
986 end
987
987
988 def test_avatar_enabled
988 def test_avatar_enabled
989 with_settings :gravatar_enabled => '1' do
989 with_settings :gravatar_enabled => '1' do
990 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
990 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
991 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
991 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
992 # Default size is 50
992 # Default size is 50
993 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
993 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
994 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
994 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
995 # Non-avatar options should be considered html options
995 # Non-avatar options should be considered html options
996 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
996 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
997 # The default class of the img tag should be gravatar
997 # The default class of the img tag should be gravatar
998 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
998 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
999 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
999 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1000 assert_nil avatar('jsmith')
1000 assert_nil avatar('jsmith')
1001 assert_nil avatar(nil)
1001 assert_nil avatar(nil)
1002 end
1002 end
1003 end
1003 end
1004
1004
1005 def test_avatar_disabled
1005 def test_avatar_disabled
1006 with_settings :gravatar_enabled => '0' do
1006 with_settings :gravatar_enabled => '0' do
1007 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1007 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1008 end
1008 end
1009 end
1009 end
1010
1010
1011 def test_link_to_user
1011 def test_link_to_user
1012 user = User.find(2)
1012 user = User.find(2)
1013 t = link_to_user(user)
1013 assert_equal '<a href="/users/2" class="user active">John Smith</a>', link_to_user(user)
1014 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
1015 end
1014 end
1016
1015
1017 def test_link_to_user_should_not_link_to_locked_user
1016 def test_link_to_user_should_not_link_to_locked_user
1018 user = User.find(5)
1017 with_current_user nil do
1019 assert user.locked?
1018 user = User.find(5)
1020 t = link_to_user(user)
1019 assert user.locked?
1021 assert_equal user.name, t
1020 assert_equal 'Dave2 Lopper2', link_to_user(user)
1021 end
1022 end
1023
1024 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1025 with_current_user User.find(1) do
1026 user = User.find(5)
1027 assert user.locked?
1028 assert_equal '<a href="/users/5" class="user locked">Dave2 Lopper2</a>', link_to_user(user)
1029 end
1022 end
1030 end
1023
1031
1024 def test_link_to_user_should_not_link_to_anonymous
1032 def test_link_to_user_should_not_link_to_anonymous
1025 user = User.anonymous
1033 user = User.anonymous
1026 assert user.anonymous?
1034 assert user.anonymous?
1027 t = link_to_user(user)
1035 t = link_to_user(user)
1028 assert_equal ::I18n.t(:label_user_anonymous), t
1036 assert_equal ::I18n.t(:label_user_anonymous), t
1029 end
1037 end
1030
1038
1031 def test_link_to_project
1039 def test_link_to_project
1032 project = Project.find(1)
1040 project = Project.find(1)
1033 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1041 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1034 link_to_project(project)
1042 link_to_project(project)
1035 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
1043 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
1036 link_to_project(project, :action => 'settings')
1044 link_to_project(project, :action => 'settings')
1037 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1045 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1038 link_to_project(project, {:only_path => false, :jump => 'blah'})
1046 link_to_project(project, {:only_path => false, :jump => 'blah'})
1039 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
1047 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
1040 link_to_project(project, {:action => 'settings'}, :class => "project")
1048 link_to_project(project, {:action => 'settings'}, :class => "project")
1041 end
1049 end
1042
1050
1043 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1051 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1044 # numeric identifier are no longer allowed
1052 # numeric identifier are no longer allowed
1045 Project.update_all "identifier=25", "id=1"
1053 Project.update_all "identifier=25", "id=1"
1046
1054
1047 assert_equal '<a href="/projects/1">eCookbook</a>',
1055 assert_equal '<a href="/projects/1">eCookbook</a>',
1048 link_to_project(Project.find(1))
1056 link_to_project(Project.find(1))
1049 end
1057 end
1050
1058
1051 def test_principals_options_for_select_with_users
1059 def test_principals_options_for_select_with_users
1052 User.current = nil
1060 User.current = nil
1053 users = [User.find(2), User.find(4)]
1061 users = [User.find(2), User.find(4)]
1054 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1062 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1055 principals_options_for_select(users)
1063 principals_options_for_select(users)
1056 end
1064 end
1057
1065
1058 def test_principals_options_for_select_with_selected
1066 def test_principals_options_for_select_with_selected
1059 User.current = nil
1067 User.current = nil
1060 users = [User.find(2), User.find(4)]
1068 users = [User.find(2), User.find(4)]
1061 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1069 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1062 principals_options_for_select(users, User.find(4))
1070 principals_options_for_select(users, User.find(4))
1063 end
1071 end
1064
1072
1065 def test_principals_options_for_select_with_users_and_groups
1073 def test_principals_options_for_select_with_users_and_groups
1066 User.current = nil
1074 User.current = nil
1067 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1075 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1068 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1076 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1069 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1077 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1070 principals_options_for_select(users)
1078 principals_options_for_select(users)
1071 end
1079 end
1072
1080
1073 def test_principals_options_for_select_with_empty_collection
1081 def test_principals_options_for_select_with_empty_collection
1074 assert_equal '', principals_options_for_select([])
1082 assert_equal '', principals_options_for_select([])
1075 end
1083 end
1076
1084
1077 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1085 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1078 users = [User.find(2), User.find(4)]
1086 users = [User.find(2), User.find(4)]
1079 User.current = User.find(4)
1087 User.current = User.find(4)
1080 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1088 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1081 end
1089 end
1082
1090
1083 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1091 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1084 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1092 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1085 end
1093 end
1086
1094
1087 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1095 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1088 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1096 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1089 end
1097 end
1090
1098
1091 def test_image_tag_should_pick_the_default_image
1099 def test_image_tag_should_pick_the_default_image
1092 assert_match 'src="/images/image.png"', image_tag("image.png")
1100 assert_match 'src="/images/image.png"', image_tag("image.png")
1093 end
1101 end
1094
1102
1095 def test_image_tag_should_pick_the_theme_image_if_it_exists
1103 def test_image_tag_should_pick_the_theme_image_if_it_exists
1096 theme = Redmine::Themes.themes.last
1104 theme = Redmine::Themes.themes.last
1097 theme.images << 'image.png'
1105 theme.images << 'image.png'
1098
1106
1099 with_settings :ui_theme => theme.id do
1107 with_settings :ui_theme => theme.id do
1100 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1108 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1101 assert_match %|src="/images/other.png"|, image_tag("other.png")
1109 assert_match %|src="/images/other.png"|, image_tag("other.png")
1102 end
1110 end
1103 ensure
1111 ensure
1104 theme.images.delete 'image.png'
1112 theme.images.delete 'image.png'
1105 end
1113 end
1106
1114
1107 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1115 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1108 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1116 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1109 end
1117 end
1110
1118
1111 def test_javascript_include_tag_should_pick_the_default_javascript
1119 def test_javascript_include_tag_should_pick_the_default_javascript
1112 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1120 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1113 end
1121 end
1114
1122
1115 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1123 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1116 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1124 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1117 end
1125 end
1118
1126
1119 def test_per_page_links_should_show_usefull_values
1127 def test_per_page_links_should_show_usefull_values
1120 set_language_if_valid 'en'
1128 set_language_if_valid 'en'
1121 stubs(:link_to).returns("[link]")
1129 stubs(:link_to).returns("[link]")
1122
1130
1123 with_settings :per_page_options => '10, 25, 50, 100' do
1131 with_settings :per_page_options => '10, 25, 50, 100' do
1124 assert_nil per_page_links(10, 3)
1132 assert_nil per_page_links(10, 3)
1125 assert_nil per_page_links(25, 3)
1133 assert_nil per_page_links(25, 3)
1126 assert_equal "Per page: 10, [link]", per_page_links(10, 22)
1134 assert_equal "Per page: 10, [link]", per_page_links(10, 22)
1127 assert_equal "Per page: [link], 25", per_page_links(25, 22)
1135 assert_equal "Per page: [link], 25", per_page_links(25, 22)
1128 assert_equal "Per page: [link], [link], 50", per_page_links(50, 22)
1136 assert_equal "Per page: [link], [link], 50", per_page_links(50, 22)
1129 assert_equal "Per page: [link], 25, [link]", per_page_links(25, 26)
1137 assert_equal "Per page: [link], 25, [link]", per_page_links(25, 26)
1130 assert_equal "Per page: [link], 25, [link], [link]", per_page_links(25, 120)
1138 assert_equal "Per page: [link], 25, [link], [link]", per_page_links(25, 120)
1131 end
1139 end
1132 end
1140 end
1133 end
1141 end
General Comments 0
You need to be logged in to leave comments. Login now