##// END OF EJS Templates
Replace uses of image_tag() with CSS (#21256)....
Jean-Philippe Lang -
r14686:7467c145c03c
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1339 +1,1341
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Themes::Helper
29 include Redmine::Themes::Helper
30 include Redmine::Hook::Helper
30 include Redmine::Hook::Helper
31
31
32 extend Forwardable
32 extend Forwardable
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34
34
35 # Return true if user is authorized for controller/action, otherwise false
35 # Return true if user is authorized for controller/action, otherwise false
36 def authorize_for(controller, action)
36 def authorize_for(controller, action)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 end
38 end
39
39
40 # Display a link if user is authorized
40 # Display a link if user is authorized
41 #
41 #
42 # @param [String] name Anchor text (passed to link_to)
42 # @param [String] name Anchor text (passed to link_to)
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [optional, Hash] html_options Options passed to link_to
44 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active? || (User.current.admin? && user.logged?)
54 if user.active? || (User.current.admin? && user.logged?)
55 link_to name, user_path(user), :class => user.css_classes
55 link_to name, user_path(user), :class => user.css_classes
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 #
72 #
73 def link_to_issue(issue, options={})
73 def link_to_issue(issue, options={})
74 title = nil
74 title = nil
75 subject = nil
75 subject = nil
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 if options[:subject] == false
77 if options[:subject] == false
78 title = issue.subject.truncate(60)
78 title = issue.subject.truncate(60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if truncate_length = options[:truncate]
81 if truncate_length = options[:truncate]
82 subject = subject.truncate(truncate_length)
82 subject = subject.truncate(truncate_length)
83 end
83 end
84 end
84 end
85 only_path = options[:only_path].nil? ? true : options[:only_path]
85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 s = link_to(text, issue_url(issue, :only_path => only_path),
86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 :class => issue.css_classes, :title => title)
87 :class => issue.css_classes, :title => title)
88 s << h(": #{subject}") if subject
88 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
89 s = h("#{issue.project} - ") + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 html_options = options.slice!(:only_path)
100 html_options = options.slice!(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
102 url = send(route_method, attachment, attachment.filename, options)
102 url = send(route_method, attachment, attachment.filename, options)
103 link_to text, url, html_options
103 link_to text, url, html_options
104 end
104 end
105
105
106 # Generates a link to a SCM revision
106 # Generates a link to a SCM revision
107 # Options:
107 # Options:
108 # * :text - Link text (default to the formatted revision)
108 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, repository, options={})
109 def link_to_revision(revision, repository, options={})
110 if repository.is_a?(Project)
110 if repository.is_a?(Project)
111 repository = repository.repository
111 repository = repository.repository
112 end
112 end
113 text = options.delete(:text) || format_revision(revision)
113 text = options.delete(:text) || format_revision(revision)
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 link_to(
115 link_to(
116 h(text),
116 h(text),
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 :title => l(:label_revision_id, format_revision(revision)),
118 :title => l(:label_revision_id, format_revision(revision)),
119 :accesskey => options[:accesskey]
119 :accesskey => options[:accesskey]
120 )
120 )
121 end
121 end
122
122
123 # Generates a link to a message
123 # Generates a link to a message
124 def link_to_message(message, options={}, html_options = nil)
124 def link_to_message(message, options={}, html_options = nil)
125 link_to(
125 link_to(
126 message.subject.truncate(60),
126 message.subject.truncate(60),
127 board_message_url(message.board_id, message.parent_id || message.id, {
127 board_message_url(message.board_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 :only_path => true
130 :only_path => true
131 }.merge(options)),
131 }.merge(options)),
132 html_options
132 html_options
133 )
133 )
134 end
134 end
135
135
136 # Generates a link to a project if active
136 # Generates a link to a project if active
137 # Examples:
137 # Examples:
138 #
138 #
139 # link_to_project(project) # => link to the specified project overview
139 # link_to_project(project) # => link to the specified project overview
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.name)
145 h(project.name)
146 else
146 else
147 link_to project.name,
147 link_to project.name,
148 project_url(project, {:only_path => true}.merge(options)),
148 project_url(project, {:only_path => true}.merge(options)),
149 html_options
149 html_options
150 end
150 end
151 end
151 end
152
152
153 # Generates a link to a project settings if active
153 # Generates a link to a project settings if active
154 def link_to_project_settings(project, options={}, html_options=nil)
154 def link_to_project_settings(project, options={}, html_options=nil)
155 if project.active?
155 if project.active?
156 link_to project.name, settings_project_path(project, options), html_options
156 link_to project.name, settings_project_path(project, options), html_options
157 elsif project.archived?
157 elsif project.archived?
158 h(project.name)
158 h(project.name)
159 else
159 else
160 link_to project.name, project_path(project, options), html_options
160 link_to project.name, project_path(project, options), html_options
161 end
161 end
162 end
162 end
163
163
164 # Generates a link to a version
164 # Generates a link to a version
165 def link_to_version(version, options = {})
165 def link_to_version(version, options = {})
166 return '' unless version && version.is_a?(Version)
166 return '' unless version && version.is_a?(Version)
167 options = {:title => format_date(version.effective_date)}.merge(options)
167 options = {:title => format_date(version.effective_date)}.merge(options)
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 end
169 end
170
170
171 # Helper that formats object for html or text rendering
171 # Helper that formats object for html or text rendering
172 def format_object(object, html=true, &block)
172 def format_object(object, html=true, &block)
173 if block_given?
173 if block_given?
174 object = yield object
174 object = yield object
175 end
175 end
176 case object.class.name
176 case object.class.name
177 when 'Array'
177 when 'Array'
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 when 'Time'
179 when 'Time'
180 format_time(object)
180 format_time(object)
181 when 'Date'
181 when 'Date'
182 format_date(object)
182 format_date(object)
183 when 'Fixnum'
183 when 'Fixnum'
184 object.to_s
184 object.to_s
185 when 'Float'
185 when 'Float'
186 sprintf "%.2f", object
186 sprintf "%.2f", object
187 when 'User'
187 when 'User'
188 html ? link_to_user(object) : object.to_s
188 html ? link_to_user(object) : object.to_s
189 when 'Project'
189 when 'Project'
190 html ? link_to_project(object) : object.to_s
190 html ? link_to_project(object) : object.to_s
191 when 'Version'
191 when 'Version'
192 html ? link_to_version(object) : object.to_s
192 html ? link_to_version(object) : object.to_s
193 when 'TrueClass'
193 when 'TrueClass'
194 l(:general_text_Yes)
194 l(:general_text_Yes)
195 when 'FalseClass'
195 when 'FalseClass'
196 l(:general_text_No)
196 l(:general_text_No)
197 when 'Issue'
197 when 'Issue'
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 when 'CustomValue', 'CustomFieldValue'
199 when 'CustomValue', 'CustomFieldValue'
200 if object.custom_field
200 if object.custom_field
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 if f.nil? || f.is_a?(String)
202 if f.nil? || f.is_a?(String)
203 f
203 f
204 else
204 else
205 format_object(f, html, &block)
205 format_object(f, html, &block)
206 end
206 end
207 else
207 else
208 object.value.to_s
208 object.value.to_s
209 end
209 end
210 else
210 else
211 html ? h(object) : object.to_s
211 html ? h(object) : object.to_s
212 end
212 end
213 end
213 end
214
214
215 def wiki_page_path(page, options={})
215 def wiki_page_path(page, options={})
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 end
217 end
218
218
219 def thumbnail_tag(attachment)
219 def thumbnail_tag(attachment)
220 link_to image_tag(thumbnail_path(attachment)),
220 link_to image_tag(thumbnail_path(attachment)),
221 named_attachment_path(attachment, attachment.filename),
221 named_attachment_path(attachment, attachment.filename),
222 :title => attachment.filename
222 :title => attachment.filename
223 end
223 end
224
224
225 def toggle_link(name, id, options={})
225 def toggle_link(name, id, options={})
226 onclick = "$('##{id}').toggle(); "
226 onclick = "$('##{id}').toggle(); "
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 onclick << "return false;"
228 onclick << "return false;"
229 link_to(name, "#", :onclick => onclick)
229 link_to(name, "#", :onclick => onclick)
230 end
230 end
231
231
232 def format_activity_title(text)
232 def format_activity_title(text)
233 h(truncate_single_line_raw(text, 100))
233 h(truncate_single_line_raw(text, 100))
234 end
234 end
235
235
236 def format_activity_day(date)
236 def format_activity_day(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 end
238 end
239
239
240 def format_activity_description(text)
240 def format_activity_description(text)
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 ).gsub(/[\r\n]+/, "<br />").html_safe
242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 end
243 end
244
244
245 def format_version_name(version)
245 def format_version_name(version)
246 if version.project == @project
246 if version.project == @project
247 h(version)
247 h(version)
248 else
248 else
249 h("#{version.project} - #{version}")
249 h("#{version.project} - #{version}")
250 end
250 end
251 end
251 end
252
252
253 def due_date_distance_in_words(date)
253 def due_date_distance_in_words(date)
254 if date
254 if date
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 end
256 end
257 end
257 end
258
258
259 # Renders a tree of projects as a nested set of unordered lists
259 # Renders a tree of projects as a nested set of unordered lists
260 # The given collection may be a subset of the whole project tree
260 # The given collection may be a subset of the whole project tree
261 # (eg. some intermediate nodes are private and can not be seen)
261 # (eg. some intermediate nodes are private and can not be seen)
262 def render_project_nested_lists(projects, &block)
262 def render_project_nested_lists(projects, &block)
263 s = ''
263 s = ''
264 if projects.any?
264 if projects.any?
265 ancestors = []
265 ancestors = []
266 original_project = @project
266 original_project = @project
267 projects.sort_by(&:lft).each do |project|
267 projects.sort_by(&:lft).each do |project|
268 # set the project environment to please macros.
268 # set the project environment to please macros.
269 @project = project
269 @project = project
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 else
272 else
273 ancestors.pop
273 ancestors.pop
274 s << "</li>"
274 s << "</li>"
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 ancestors.pop
276 ancestors.pop
277 s << "</ul></li>\n"
277 s << "</ul></li>\n"
278 end
278 end
279 end
279 end
280 classes = (ancestors.empty? ? 'root' : 'child')
280 classes = (ancestors.empty? ? 'root' : 'child')
281 s << "<li class='#{classes}'><div class='#{classes}'>"
281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << h(block_given? ? capture(project, &block) : project.name)
282 s << h(block_given? ? capture(project, &block) : project.name)
283 s << "</div>\n"
283 s << "</div>\n"
284 ancestors << project
284 ancestors << project
285 end
285 end
286 s << ("</li></ul>\n" * ancestors.size)
286 s << ("</li></ul>\n" * ancestors.size)
287 @project = original_project
287 @project = original_project
288 end
288 end
289 s.html_safe
289 s.html_safe
290 end
290 end
291
291
292 def render_page_hierarchy(pages, node=nil, options={})
292 def render_page_hierarchy(pages, node=nil, options={})
293 content = ''
293 content = ''
294 if pages[node]
294 if pages[node]
295 content << "<ul class=\"pages-hierarchy\">\n"
295 content << "<ul class=\"pages-hierarchy\">\n"
296 pages[node].each do |page|
296 pages[node].each do |page|
297 content << "<li>"
297 content << "<li>"
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "</li>\n"
301 content << "</li>\n"
302 end
302 end
303 content << "</ul>\n"
303 content << "</ul>\n"
304 end
304 end
305 content.html_safe
305 content.html_safe
306 end
306 end
307
307
308 # Renders flash messages
308 # Renders flash messages
309 def render_flash_messages
309 def render_flash_messages
310 s = ''
310 s = ''
311 flash.each do |k,v|
311 flash.each do |k,v|
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 end
313 end
314 s.html_safe
314 s.html_safe
315 end
315 end
316
316
317 # Renders tabs and their content
317 # Renders tabs and their content
318 def render_tabs(tabs, selected=params[:tab])
318 def render_tabs(tabs, selected=params[:tab])
319 if tabs.any?
319 if tabs.any?
320 unless tabs.detect {|tab| tab[:name] == selected}
320 unless tabs.detect {|tab| tab[:name] == selected}
321 selected = nil
321 selected = nil
322 end
322 end
323 selected ||= tabs.first[:name]
323 selected ||= tabs.first[:name]
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 else
325 else
326 content_tag 'p', l(:label_no_data), :class => "nodata"
326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 end
327 end
328 end
328 end
329
329
330 # Renders the project quick-jump box
330 # Renders the project quick-jump box
331 def render_project_jump_box
331 def render_project_jump_box
332 return unless User.current.logged?
332 return unless User.current.logged?
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 if projects.any?
334 if projects.any?
335 options =
335 options =
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 '<option value="" disabled="disabled">---</option>').html_safe
337 '<option value="" disabled="disabled">---</option>').html_safe
338
338
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 { :value => project_path(:id => p, :jump => current_menu_item) }
340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 end
341 end
342
342
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 end
345 end
346 end
346 end
347
347
348 def project_tree_options_for_select(projects, options = {})
348 def project_tree_options_for_select(projects, options = {})
349 s = ''.html_safe
349 s = ''.html_safe
350 if blank_text = options[:include_blank]
350 if blank_text = options[:include_blank]
351 if blank_text == true
351 if blank_text == true
352 blank_text = '&nbsp;'.html_safe
352 blank_text = '&nbsp;'.html_safe
353 end
353 end
354 s << content_tag('option', blank_text, :value => '')
354 s << content_tag('option', blank_text, :value => '')
355 end
355 end
356 project_tree(projects) do |project, level|
356 project_tree(projects) do |project, level|
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 tag_options = {:value => project.id}
358 tag_options = {:value => project.id}
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 tag_options[:selected] = 'selected'
360 tag_options[:selected] = 'selected'
361 else
361 else
362 tag_options[:selected] = nil
362 tag_options[:selected] = nil
363 end
363 end
364 tag_options.merge!(yield(project)) if block_given?
364 tag_options.merge!(yield(project)) if block_given?
365 s << content_tag('option', name_prefix + h(project), tag_options)
365 s << content_tag('option', name_prefix + h(project), tag_options)
366 end
366 end
367 s.html_safe
367 s.html_safe
368 end
368 end
369
369
370 # Yields the given block for each project with its level in the tree
370 # Yields the given block for each project with its level in the tree
371 #
371 #
372 # Wrapper for Project#project_tree
372 # Wrapper for Project#project_tree
373 def project_tree(projects, &block)
373 def project_tree(projects, &block)
374 Project.project_tree(projects, &block)
374 Project.project_tree(projects, &block)
375 end
375 end
376
376
377 def principals_check_box_tags(name, principals)
377 def principals_check_box_tags(name, principals)
378 s = ''
378 s = ''
379 principals.each do |principal|
379 principals.each do |principal|
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 end
381 end
382 s.html_safe
382 s.html_safe
383 end
383 end
384
384
385 # Returns a string for users/groups option tags
385 # Returns a string for users/groups option tags
386 def principals_options_for_select(collection, selected=nil)
386 def principals_options_for_select(collection, selected=nil)
387 s = ''
387 s = ''
388 if collection.include?(User.current)
388 if collection.include?(User.current)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 end
390 end
391 groups = ''
391 groups = ''
392 collection.sort.each do |element|
392 collection.sort.each do |element|
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 end
395 end
396 unless groups.empty?
396 unless groups.empty?
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 end
398 end
399 s.html_safe
399 s.html_safe
400 end
400 end
401
401
402 def option_tag(name, text, value, selected=nil, options={})
402 def option_tag(name, text, value, selected=nil, options={})
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 end
404 end
405
405
406 def truncate_single_line_raw(string, length)
406 def truncate_single_line_raw(string, length)
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 end
408 end
409
409
410 # Truncates at line break after 250 characters or options[:length]
410 # Truncates at line break after 250 characters or options[:length]
411 def truncate_lines(string, options={})
411 def truncate_lines(string, options={})
412 length = options[:length] || 250
412 length = options[:length] || 250
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 "#{$1}..."
414 "#{$1}..."
415 else
415 else
416 string
416 string
417 end
417 end
418 end
418 end
419
419
420 def anchor(text)
420 def anchor(text)
421 text.to_s.gsub(' ', '_')
421 text.to_s.gsub(' ', '_')
422 end
422 end
423
423
424 def html_hours(text)
424 def html_hours(text)
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 end
426 end
427
427
428 def authoring(created, author, options={})
428 def authoring(created, author, options={})
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 end
430 end
431
431
432 def time_tag(time)
432 def time_tag(time)
433 text = distance_of_time_in_words(Time.now, time)
433 text = distance_of_time_in_words(Time.now, time)
434 if @project
434 if @project
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 else
436 else
437 content_tag('abbr', text, :title => format_time(time))
437 content_tag('abbr', text, :title => format_time(time))
438 end
438 end
439 end
439 end
440
440
441 def syntax_highlight_lines(name, content)
441 def syntax_highlight_lines(name, content)
442 lines = []
442 lines = []
443 syntax_highlight(name, content).each_line { |line| lines << line }
443 syntax_highlight(name, content).each_line { |line| lines << line }
444 lines
444 lines
445 end
445 end
446
446
447 def syntax_highlight(name, content)
447 def syntax_highlight(name, content)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 end
449 end
450
450
451 def to_path_param(path)
451 def to_path_param(path)
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 str.blank? ? nil : str
453 str.blank? ? nil : str
454 end
454 end
455
455
456 def reorder_links(name, url, method = :post)
456 def reorder_links(name, url, method = :post)
457 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
457 link_to('',
458 url.merge({"#{name}[move_to]" => 'highest'}),
458 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
459 :method => method, :title => l(:label_sort_highest)) +
459 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
460 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
460 link_to('',
461 url.merge({"#{name}[move_to]" => 'higher'}),
461 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
462 :method => method, :title => l(:label_sort_higher)) +
462 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
463 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
463 link_to('',
464 url.merge({"#{name}[move_to]" => 'lower'}),
464 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
465 :method => method, :title => l(:label_sort_lower)) +
465 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
466 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
466 link_to('',
467 url.merge({"#{name}[move_to]" => 'lowest'}),
467 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
468 :method => method, :title => l(:label_sort_lowest))
468 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
469 end
469 end
470
470
471 def breadcrumb(*args)
471 def breadcrumb(*args)
472 elements = args.flatten
472 elements = args.flatten
473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
474 end
474 end
475
475
476 def other_formats_links(&block)
476 def other_formats_links(&block)
477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
478 yield Redmine::Views::OtherFormatsBuilder.new(self)
478 yield Redmine::Views::OtherFormatsBuilder.new(self)
479 concat('</p>'.html_safe)
479 concat('</p>'.html_safe)
480 end
480 end
481
481
482 def page_header_title
482 def page_header_title
483 if @project.nil? || @project.new_record?
483 if @project.nil? || @project.new_record?
484 h(Setting.app_title)
484 h(Setting.app_title)
485 else
485 else
486 b = []
486 b = []
487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
488 if ancestors.any?
488 if ancestors.any?
489 root = ancestors.shift
489 root = ancestors.shift
490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
491 if ancestors.size > 2
491 if ancestors.size > 2
492 b << "\xe2\x80\xa6"
492 b << "\xe2\x80\xa6"
493 ancestors = ancestors[-2, 2]
493 ancestors = ancestors[-2, 2]
494 end
494 end
495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
496 end
496 end
497 b << h(@project)
497 b << h(@project)
498 b.join(" \xc2\xbb ").html_safe
498 b.join(" \xc2\xbb ").html_safe
499 end
499 end
500 end
500 end
501
501
502 # Returns a h2 tag and sets the html title with the given arguments
502 # Returns a h2 tag and sets the html title with the given arguments
503 def title(*args)
503 def title(*args)
504 strings = args.map do |arg|
504 strings = args.map do |arg|
505 if arg.is_a?(Array) && arg.size >= 2
505 if arg.is_a?(Array) && arg.size >= 2
506 link_to(*arg)
506 link_to(*arg)
507 else
507 else
508 h(arg.to_s)
508 h(arg.to_s)
509 end
509 end
510 end
510 end
511 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
511 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
512 content_tag('h2', strings.join(' &#187; ').html_safe)
512 content_tag('h2', strings.join(' &#187; ').html_safe)
513 end
513 end
514
514
515 # Sets the html title
515 # Sets the html title
516 # Returns the html title when called without arguments
516 # Returns the html title when called without arguments
517 # Current project name and app_title and automatically appended
517 # Current project name and app_title and automatically appended
518 # Exemples:
518 # Exemples:
519 # html_title 'Foo', 'Bar'
519 # html_title 'Foo', 'Bar'
520 # html_title # => 'Foo - Bar - My Project - Redmine'
520 # html_title # => 'Foo - Bar - My Project - Redmine'
521 def html_title(*args)
521 def html_title(*args)
522 if args.empty?
522 if args.empty?
523 title = @html_title || []
523 title = @html_title || []
524 title << @project.name if @project
524 title << @project.name if @project
525 title << Setting.app_title unless Setting.app_title == title.last
525 title << Setting.app_title unless Setting.app_title == title.last
526 title.reject(&:blank?).join(' - ')
526 title.reject(&:blank?).join(' - ')
527 else
527 else
528 @html_title ||= []
528 @html_title ||= []
529 @html_title += args
529 @html_title += args
530 end
530 end
531 end
531 end
532
532
533 # Returns the theme, controller name, and action as css classes for the
533 # Returns the theme, controller name, and action as css classes for the
534 # HTML body.
534 # HTML body.
535 def body_css_classes
535 def body_css_classes
536 css = []
536 css = []
537 if theme = Redmine::Themes.theme(Setting.ui_theme)
537 if theme = Redmine::Themes.theme(Setting.ui_theme)
538 css << 'theme-' + theme.name
538 css << 'theme-' + theme.name
539 end
539 end
540
540
541 css << 'project-' + @project.identifier if @project && @project.identifier.present?
541 css << 'project-' + @project.identifier if @project && @project.identifier.present?
542 css << 'controller-' + controller_name
542 css << 'controller-' + controller_name
543 css << 'action-' + action_name
543 css << 'action-' + action_name
544 css.join(' ')
544 css.join(' ')
545 end
545 end
546
546
547 def accesskey(s)
547 def accesskey(s)
548 @used_accesskeys ||= []
548 @used_accesskeys ||= []
549 key = Redmine::AccessKeys.key_for(s)
549 key = Redmine::AccessKeys.key_for(s)
550 return nil if @used_accesskeys.include?(key)
550 return nil if @used_accesskeys.include?(key)
551 @used_accesskeys << key
551 @used_accesskeys << key
552 key
552 key
553 end
553 end
554
554
555 # Formats text according to system settings.
555 # Formats text according to system settings.
556 # 2 ways to call this method:
556 # 2 ways to call this method:
557 # * with a String: textilizable(text, options)
557 # * with a String: textilizable(text, options)
558 # * with an object and one of its attribute: textilizable(issue, :description, options)
558 # * with an object and one of its attribute: textilizable(issue, :description, options)
559 def textilizable(*args)
559 def textilizable(*args)
560 options = args.last.is_a?(Hash) ? args.pop : {}
560 options = args.last.is_a?(Hash) ? args.pop : {}
561 case args.size
561 case args.size
562 when 1
562 when 1
563 obj = options[:object]
563 obj = options[:object]
564 text = args.shift
564 text = args.shift
565 when 2
565 when 2
566 obj = args.shift
566 obj = args.shift
567 attr = args.shift
567 attr = args.shift
568 text = obj.send(attr).to_s
568 text = obj.send(attr).to_s
569 else
569 else
570 raise ArgumentError, 'invalid arguments to textilizable'
570 raise ArgumentError, 'invalid arguments to textilizable'
571 end
571 end
572 return '' if text.blank?
572 return '' if text.blank?
573 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
573 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
574 @only_path = only_path = options.delete(:only_path) == false ? false : true
574 @only_path = only_path = options.delete(:only_path) == false ? false : true
575
575
576 text = text.dup
576 text = text.dup
577 macros = catch_macros(text)
577 macros = catch_macros(text)
578 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
578 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
579
579
580 @parsed_headings = []
580 @parsed_headings = []
581 @heading_anchors = {}
581 @heading_anchors = {}
582 @current_section = 0 if options[:edit_section_links]
582 @current_section = 0 if options[:edit_section_links]
583
583
584 parse_sections(text, project, obj, attr, only_path, options)
584 parse_sections(text, project, obj, attr, only_path, options)
585 text = parse_non_pre_blocks(text, obj, macros) do |text|
585 text = parse_non_pre_blocks(text, obj, macros) do |text|
586 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
586 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
587 send method_name, text, project, obj, attr, only_path, options
587 send method_name, text, project, obj, attr, only_path, options
588 end
588 end
589 end
589 end
590 parse_headings(text, project, obj, attr, only_path, options)
590 parse_headings(text, project, obj, attr, only_path, options)
591
591
592 if @parsed_headings.any?
592 if @parsed_headings.any?
593 replace_toc(text, @parsed_headings)
593 replace_toc(text, @parsed_headings)
594 end
594 end
595
595
596 text.html_safe
596 text.html_safe
597 end
597 end
598
598
599 def parse_non_pre_blocks(text, obj, macros)
599 def parse_non_pre_blocks(text, obj, macros)
600 s = StringScanner.new(text)
600 s = StringScanner.new(text)
601 tags = []
601 tags = []
602 parsed = ''
602 parsed = ''
603 while !s.eos?
603 while !s.eos?
604 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
604 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
605 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
605 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
606 if tags.empty?
606 if tags.empty?
607 yield text
607 yield text
608 inject_macros(text, obj, macros) if macros.any?
608 inject_macros(text, obj, macros) if macros.any?
609 else
609 else
610 inject_macros(text, obj, macros, false) if macros.any?
610 inject_macros(text, obj, macros, false) if macros.any?
611 end
611 end
612 parsed << text
612 parsed << text
613 if tag
613 if tag
614 if closing
614 if closing
615 if tags.last && tags.last.casecmp(tag) == 0
615 if tags.last && tags.last.casecmp(tag) == 0
616 tags.pop
616 tags.pop
617 end
617 end
618 else
618 else
619 tags << tag.downcase
619 tags << tag.downcase
620 end
620 end
621 parsed << full_tag
621 parsed << full_tag
622 end
622 end
623 end
623 end
624 # Close any non closing tags
624 # Close any non closing tags
625 while tag = tags.pop
625 while tag = tags.pop
626 parsed << "</#{tag}>"
626 parsed << "</#{tag}>"
627 end
627 end
628 parsed
628 parsed
629 end
629 end
630
630
631 def parse_inline_attachments(text, project, obj, attr, only_path, options)
631 def parse_inline_attachments(text, project, obj, attr, only_path, options)
632 return if options[:inline_attachments] == false
632 return if options[:inline_attachments] == false
633
633
634 # when using an image link, try to use an attachment, if possible
634 # when using an image link, try to use an attachment, if possible
635 attachments = options[:attachments] || []
635 attachments = options[:attachments] || []
636 attachments += obj.attachments if obj.respond_to?(:attachments)
636 attachments += obj.attachments if obj.respond_to?(:attachments)
637 if attachments.present?
637 if attachments.present?
638 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
638 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
639 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
639 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
640 # search for the picture in attachments
640 # search for the picture in attachments
641 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
641 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
642 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
642 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
643 desc = found.description.to_s.gsub('"', '')
643 desc = found.description.to_s.gsub('"', '')
644 if !desc.blank? && alttext.blank?
644 if !desc.blank? && alttext.blank?
645 alt = " title=\"#{desc}\" alt=\"#{desc}\""
645 alt = " title=\"#{desc}\" alt=\"#{desc}\""
646 end
646 end
647 "src=\"#{image_url}\"#{alt}"
647 "src=\"#{image_url}\"#{alt}"
648 else
648 else
649 m
649 m
650 end
650 end
651 end
651 end
652 end
652 end
653 end
653 end
654
654
655 # Wiki links
655 # Wiki links
656 #
656 #
657 # Examples:
657 # Examples:
658 # [[mypage]]
658 # [[mypage]]
659 # [[mypage|mytext]]
659 # [[mypage|mytext]]
660 # wiki links can refer other project wikis, using project name or identifier:
660 # wiki links can refer other project wikis, using project name or identifier:
661 # [[project:]] -> wiki starting page
661 # [[project:]] -> wiki starting page
662 # [[project:|mytext]]
662 # [[project:|mytext]]
663 # [[project:mypage]]
663 # [[project:mypage]]
664 # [[project:mypage|mytext]]
664 # [[project:mypage|mytext]]
665 def parse_wiki_links(text, project, obj, attr, only_path, options)
665 def parse_wiki_links(text, project, obj, attr, only_path, options)
666 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
666 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
667 link_project = project
667 link_project = project
668 esc, all, page, title = $1, $2, $3, $5
668 esc, all, page, title = $1, $2, $3, $5
669 if esc.nil?
669 if esc.nil?
670 if page =~ /^([^\:]+)\:(.*)$/
670 if page =~ /^([^\:]+)\:(.*)$/
671 identifier, page = $1, $2
671 identifier, page = $1, $2
672 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
672 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
673 title ||= identifier if page.blank?
673 title ||= identifier if page.blank?
674 end
674 end
675
675
676 if link_project && link_project.wiki
676 if link_project && link_project.wiki
677 # extract anchor
677 # extract anchor
678 anchor = nil
678 anchor = nil
679 if page =~ /^(.+?)\#(.+)$/
679 if page =~ /^(.+?)\#(.+)$/
680 page, anchor = $1, $2
680 page, anchor = $1, $2
681 end
681 end
682 anchor = sanitize_anchor_name(anchor) if anchor.present?
682 anchor = sanitize_anchor_name(anchor) if anchor.present?
683 # check if page exists
683 # check if page exists
684 wiki_page = link_project.wiki.find_page(page)
684 wiki_page = link_project.wiki.find_page(page)
685 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
685 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
686 "##{anchor}"
686 "##{anchor}"
687 else
687 else
688 case options[:wiki_links]
688 case options[:wiki_links]
689 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
689 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
690 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
690 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
691 else
691 else
692 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
692 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
693 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
693 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
694 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
694 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
695 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
695 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
696 end
696 end
697 end
697 end
698 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
698 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
699 else
699 else
700 # project or wiki doesn't exist
700 # project or wiki doesn't exist
701 all
701 all
702 end
702 end
703 else
703 else
704 all
704 all
705 end
705 end
706 end
706 end
707 end
707 end
708
708
709 # Redmine links
709 # Redmine links
710 #
710 #
711 # Examples:
711 # Examples:
712 # Issues:
712 # Issues:
713 # #52 -> Link to issue #52
713 # #52 -> Link to issue #52
714 # Changesets:
714 # Changesets:
715 # r52 -> Link to revision 52
715 # r52 -> Link to revision 52
716 # commit:a85130f -> Link to scmid starting with a85130f
716 # commit:a85130f -> Link to scmid starting with a85130f
717 # Documents:
717 # Documents:
718 # document#17 -> Link to document with id 17
718 # document#17 -> Link to document with id 17
719 # document:Greetings -> Link to the document with title "Greetings"
719 # document:Greetings -> Link to the document with title "Greetings"
720 # document:"Some document" -> Link to the document with title "Some document"
720 # document:"Some document" -> Link to the document with title "Some document"
721 # Versions:
721 # Versions:
722 # version#3 -> Link to version with id 3
722 # version#3 -> Link to version with id 3
723 # version:1.0.0 -> Link to version named "1.0.0"
723 # version:1.0.0 -> Link to version named "1.0.0"
724 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
724 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
725 # Attachments:
725 # Attachments:
726 # attachment:file.zip -> Link to the attachment of the current object named file.zip
726 # attachment:file.zip -> Link to the attachment of the current object named file.zip
727 # Source files:
727 # Source files:
728 # source:some/file -> Link to the file located at /some/file in the project's repository
728 # source:some/file -> Link to the file located at /some/file in the project's repository
729 # source:some/file@52 -> Link to the file's revision 52
729 # source:some/file@52 -> Link to the file's revision 52
730 # source:some/file#L120 -> Link to line 120 of the file
730 # source:some/file#L120 -> Link to line 120 of the file
731 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
731 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
732 # export:some/file -> Force the download of the file
732 # export:some/file -> Force the download of the file
733 # Forum messages:
733 # Forum messages:
734 # message#1218 -> Link to message with id 1218
734 # message#1218 -> Link to message with id 1218
735 # Projects:
735 # Projects:
736 # project:someproject -> Link to project named "someproject"
736 # project:someproject -> Link to project named "someproject"
737 # project#3 -> Link to project with id 3
737 # project#3 -> Link to project with id 3
738 #
738 #
739 # Links can refer other objects from other projects, using project identifier:
739 # Links can refer other objects from other projects, using project identifier:
740 # identifier:r52
740 # identifier:r52
741 # identifier:document:"Some document"
741 # identifier:document:"Some document"
742 # identifier:version:1.0.0
742 # identifier:version:1.0.0
743 # identifier:source:some/file
743 # identifier:source:some/file
744 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
744 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
745 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
745 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
746 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
746 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
747 if tag_content
747 if tag_content
748 $&
748 $&
749 else
749 else
750 link = nil
750 link = nil
751 project = default_project
751 project = default_project
752 if project_identifier
752 if project_identifier
753 project = Project.visible.find_by_identifier(project_identifier)
753 project = Project.visible.find_by_identifier(project_identifier)
754 end
754 end
755 if esc.nil?
755 if esc.nil?
756 if prefix.nil? && sep == 'r'
756 if prefix.nil? && sep == 'r'
757 if project
757 if project
758 repository = nil
758 repository = nil
759 if repo_identifier
759 if repo_identifier
760 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
760 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
761 else
761 else
762 repository = project.repository
762 repository = project.repository
763 end
763 end
764 # project.changesets.visible raises an SQL error because of a double join on repositories
764 # project.changesets.visible raises an SQL error because of a double join on repositories
765 if repository &&
765 if repository &&
766 (changeset = Changeset.visible.
766 (changeset = Changeset.visible.
767 find_by_repository_id_and_revision(repository.id, identifier))
767 find_by_repository_id_and_revision(repository.id, identifier))
768 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
768 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
769 {:only_path => only_path, :controller => 'repositories',
769 {:only_path => only_path, :controller => 'repositories',
770 :action => 'revision', :id => project,
770 :action => 'revision', :id => project,
771 :repository_id => repository.identifier_param,
771 :repository_id => repository.identifier_param,
772 :rev => changeset.revision},
772 :rev => changeset.revision},
773 :class => 'changeset',
773 :class => 'changeset',
774 :title => truncate_single_line_raw(changeset.comments, 100))
774 :title => truncate_single_line_raw(changeset.comments, 100))
775 end
775 end
776 end
776 end
777 elsif sep == '#'
777 elsif sep == '#'
778 oid = identifier.to_i
778 oid = identifier.to_i
779 case prefix
779 case prefix
780 when nil
780 when nil
781 if oid.to_s == identifier &&
781 if oid.to_s == identifier &&
782 issue = Issue.visible.find_by_id(oid)
782 issue = Issue.visible.find_by_id(oid)
783 anchor = comment_id ? "note-#{comment_id}" : nil
783 anchor = comment_id ? "note-#{comment_id}" : nil
784 link = link_to("##{oid}#{comment_suffix}",
784 link = link_to("##{oid}#{comment_suffix}",
785 issue_url(issue, :only_path => only_path, :anchor => anchor),
785 issue_url(issue, :only_path => only_path, :anchor => anchor),
786 :class => issue.css_classes,
786 :class => issue.css_classes,
787 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
787 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
788 end
788 end
789 when 'document'
789 when 'document'
790 if document = Document.visible.find_by_id(oid)
790 if document = Document.visible.find_by_id(oid)
791 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
791 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
792 end
792 end
793 when 'version'
793 when 'version'
794 if version = Version.visible.find_by_id(oid)
794 if version = Version.visible.find_by_id(oid)
795 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
795 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
796 end
796 end
797 when 'message'
797 when 'message'
798 if message = Message.visible.find_by_id(oid)
798 if message = Message.visible.find_by_id(oid)
799 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
799 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
800 end
800 end
801 when 'forum'
801 when 'forum'
802 if board = Board.visible.find_by_id(oid)
802 if board = Board.visible.find_by_id(oid)
803 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
803 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
804 end
804 end
805 when 'news'
805 when 'news'
806 if news = News.visible.find_by_id(oid)
806 if news = News.visible.find_by_id(oid)
807 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
807 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
808 end
808 end
809 when 'project'
809 when 'project'
810 if p = Project.visible.find_by_id(oid)
810 if p = Project.visible.find_by_id(oid)
811 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
811 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
812 end
812 end
813 end
813 end
814 elsif sep == ':'
814 elsif sep == ':'
815 # removes the double quotes if any
815 # removes the double quotes if any
816 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
816 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
817 name = CGI.unescapeHTML(name)
817 name = CGI.unescapeHTML(name)
818 case prefix
818 case prefix
819 when 'document'
819 when 'document'
820 if project && document = project.documents.visible.find_by_title(name)
820 if project && document = project.documents.visible.find_by_title(name)
821 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
821 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
822 end
822 end
823 when 'version'
823 when 'version'
824 if project && version = project.versions.visible.find_by_name(name)
824 if project && version = project.versions.visible.find_by_name(name)
825 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
825 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
826 end
826 end
827 when 'forum'
827 when 'forum'
828 if project && board = project.boards.visible.find_by_name(name)
828 if project && board = project.boards.visible.find_by_name(name)
829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
830 end
830 end
831 when 'news'
831 when 'news'
832 if project && news = project.news.visible.find_by_title(name)
832 if project && news = project.news.visible.find_by_title(name)
833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
834 end
834 end
835 when 'commit', 'source', 'export'
835 when 'commit', 'source', 'export'
836 if project
836 if project
837 repository = nil
837 repository = nil
838 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
838 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
839 repo_prefix, repo_identifier, name = $1, $2, $3
839 repo_prefix, repo_identifier, name = $1, $2, $3
840 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
840 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
841 else
841 else
842 repository = project.repository
842 repository = project.repository
843 end
843 end
844 if prefix == 'commit'
844 if prefix == 'commit'
845 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
845 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
846 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},
846 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},
847 :class => 'changeset',
847 :class => 'changeset',
848 :title => truncate_single_line_raw(changeset.comments, 100)
848 :title => truncate_single_line_raw(changeset.comments, 100)
849 end
849 end
850 else
850 else
851 if repository && User.current.allowed_to?(:browse_repository, project)
851 if repository && User.current.allowed_to?(:browse_repository, project)
852 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
852 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
853 path, rev, anchor = $1, $3, $5
853 path, rev, anchor = $1, $3, $5
854 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
854 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
855 :path => to_path_param(path),
855 :path => to_path_param(path),
856 :rev => rev,
856 :rev => rev,
857 :anchor => anchor},
857 :anchor => anchor},
858 :class => (prefix == 'export' ? 'source download' : 'source')
858 :class => (prefix == 'export' ? 'source download' : 'source')
859 end
859 end
860 end
860 end
861 repo_prefix = nil
861 repo_prefix = nil
862 end
862 end
863 when 'attachment'
863 when 'attachment'
864 attachments = options[:attachments] || []
864 attachments = options[:attachments] || []
865 attachments += obj.attachments if obj.respond_to?(:attachments)
865 attachments += obj.attachments if obj.respond_to?(:attachments)
866 if attachments && attachment = Attachment.latest_attach(attachments, name)
866 if attachments && attachment = Attachment.latest_attach(attachments, name)
867 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
867 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
868 end
868 end
869 when 'project'
869 when 'project'
870 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
870 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
871 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
871 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
872 end
872 end
873 end
873 end
874 end
874 end
875 end
875 end
876 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
876 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
877 end
877 end
878 end
878 end
879 end
879 end
880
880
881 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
881 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
882
882
883 def parse_sections(text, project, obj, attr, only_path, options)
883 def parse_sections(text, project, obj, attr, only_path, options)
884 return unless options[:edit_section_links]
884 return unless options[:edit_section_links]
885 text.gsub!(HEADING_RE) do
885 text.gsub!(HEADING_RE) do
886 heading = $1
886 heading = $1
887 @current_section += 1
887 @current_section += 1
888 if @current_section > 1
888 if @current_section > 1
889 content_tag('div',
889 content_tag('div',
890 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
890 link_to('', options[:edit_section_links].merge(:section => @current_section),
891 :class => 'icon-only icon-edit'),
891 :class => 'contextual',
892 :class => 'contextual',
892 :title => l(:button_edit_section),
893 :title => l(:button_edit_section),
893 :id => "section-#{@current_section}") + heading.html_safe
894 :id => "section-#{@current_section}") + heading.html_safe
894 else
895 else
895 heading
896 heading
896 end
897 end
897 end
898 end
898 end
899 end
899
900
900 # Headings and TOC
901 # Headings and TOC
901 # Adds ids and links to headings unless options[:headings] is set to false
902 # Adds ids and links to headings unless options[:headings] is set to false
902 def parse_headings(text, project, obj, attr, only_path, options)
903 def parse_headings(text, project, obj, attr, only_path, options)
903 return if options[:headings] == false
904 return if options[:headings] == false
904
905
905 text.gsub!(HEADING_RE) do
906 text.gsub!(HEADING_RE) do
906 level, attrs, content = $2.to_i, $3, $4
907 level, attrs, content = $2.to_i, $3, $4
907 item = strip_tags(content).strip
908 item = strip_tags(content).strip
908 anchor = sanitize_anchor_name(item)
909 anchor = sanitize_anchor_name(item)
909 # used for single-file wiki export
910 # used for single-file wiki export
910 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
911 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
911 @heading_anchors[anchor] ||= 0
912 @heading_anchors[anchor] ||= 0
912 idx = (@heading_anchors[anchor] += 1)
913 idx = (@heading_anchors[anchor] += 1)
913 if idx > 1
914 if idx > 1
914 anchor = "#{anchor}-#{idx}"
915 anchor = "#{anchor}-#{idx}"
915 end
916 end
916 @parsed_headings << [level, anchor, item]
917 @parsed_headings << [level, anchor, item]
917 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
918 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
918 end
919 end
919 end
920 end
920
921
921 MACROS_RE = /(
922 MACROS_RE = /(
922 (!)? # escaping
923 (!)? # escaping
923 (
924 (
924 \{\{ # opening tag
925 \{\{ # opening tag
925 ([\w]+) # macro name
926 ([\w]+) # macro name
926 (\(([^\n\r]*?)\))? # optional arguments
927 (\(([^\n\r]*?)\))? # optional arguments
927 ([\n\r].*?[\n\r])? # optional block of text
928 ([\n\r].*?[\n\r])? # optional block of text
928 \}\} # closing tag
929 \}\} # closing tag
929 )
930 )
930 )/mx unless const_defined?(:MACROS_RE)
931 )/mx unless const_defined?(:MACROS_RE)
931
932
932 MACRO_SUB_RE = /(
933 MACRO_SUB_RE = /(
933 \{\{
934 \{\{
934 macro\((\d+)\)
935 macro\((\d+)\)
935 \}\}
936 \}\}
936 )/x unless const_defined?(:MACRO_SUB_RE)
937 )/x unless const_defined?(:MACRO_SUB_RE)
937
938
938 # Extracts macros from text
939 # Extracts macros from text
939 def catch_macros(text)
940 def catch_macros(text)
940 macros = {}
941 macros = {}
941 text.gsub!(MACROS_RE) do
942 text.gsub!(MACROS_RE) do
942 all, macro = $1, $4.downcase
943 all, macro = $1, $4.downcase
943 if macro_exists?(macro) || all =~ MACRO_SUB_RE
944 if macro_exists?(macro) || all =~ MACRO_SUB_RE
944 index = macros.size
945 index = macros.size
945 macros[index] = all
946 macros[index] = all
946 "{{macro(#{index})}}"
947 "{{macro(#{index})}}"
947 else
948 else
948 all
949 all
949 end
950 end
950 end
951 end
951 macros
952 macros
952 end
953 end
953
954
954 # Executes and replaces macros in text
955 # Executes and replaces macros in text
955 def inject_macros(text, obj, macros, execute=true)
956 def inject_macros(text, obj, macros, execute=true)
956 text.gsub!(MACRO_SUB_RE) do
957 text.gsub!(MACRO_SUB_RE) do
957 all, index = $1, $2.to_i
958 all, index = $1, $2.to_i
958 orig = macros.delete(index)
959 orig = macros.delete(index)
959 if execute && orig && orig =~ MACROS_RE
960 if execute && orig && orig =~ MACROS_RE
960 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
961 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
961 if esc.nil?
962 if esc.nil?
962 h(exec_macro(macro, obj, args, block) || all)
963 h(exec_macro(macro, obj, args, block) || all)
963 else
964 else
964 h(all)
965 h(all)
965 end
966 end
966 elsif orig
967 elsif orig
967 h(orig)
968 h(orig)
968 else
969 else
969 h(all)
970 h(all)
970 end
971 end
971 end
972 end
972 end
973 end
973
974
974 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
975 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
975
976
976 # Renders the TOC with given headings
977 # Renders the TOC with given headings
977 def replace_toc(text, headings)
978 def replace_toc(text, headings)
978 text.gsub!(TOC_RE) do
979 text.gsub!(TOC_RE) do
979 left_align, right_align = $2, $3
980 left_align, right_align = $2, $3
980 # Keep only the 4 first levels
981 # Keep only the 4 first levels
981 headings = headings.select{|level, anchor, item| level <= 4}
982 headings = headings.select{|level, anchor, item| level <= 4}
982 if headings.empty?
983 if headings.empty?
983 ''
984 ''
984 else
985 else
985 div_class = 'toc'
986 div_class = 'toc'
986 div_class << ' right' if right_align
987 div_class << ' right' if right_align
987 div_class << ' left' if left_align
988 div_class << ' left' if left_align
988 out = "<ul class=\"#{div_class}\"><li>"
989 out = "<ul class=\"#{div_class}\"><li>"
989 root = headings.map(&:first).min
990 root = headings.map(&:first).min
990 current = root
991 current = root
991 started = false
992 started = false
992 headings.each do |level, anchor, item|
993 headings.each do |level, anchor, item|
993 if level > current
994 if level > current
994 out << '<ul><li>' * (level - current)
995 out << '<ul><li>' * (level - current)
995 elsif level < current
996 elsif level < current
996 out << "</li></ul>\n" * (current - level) + "</li><li>"
997 out << "</li></ul>\n" * (current - level) + "</li><li>"
997 elsif started
998 elsif started
998 out << '</li><li>'
999 out << '</li><li>'
999 end
1000 end
1000 out << "<a href=\"##{anchor}\">#{item}</a>"
1001 out << "<a href=\"##{anchor}\">#{item}</a>"
1001 current = level
1002 current = level
1002 started = true
1003 started = true
1003 end
1004 end
1004 out << '</li></ul>' * (current - root)
1005 out << '</li></ul>' * (current - root)
1005 out << '</li></ul>'
1006 out << '</li></ul>'
1006 end
1007 end
1007 end
1008 end
1008 end
1009 end
1009
1010
1010 # Same as Rails' simple_format helper without using paragraphs
1011 # Same as Rails' simple_format helper without using paragraphs
1011 def simple_format_without_paragraph(text)
1012 def simple_format_without_paragraph(text)
1012 text.to_s.
1013 text.to_s.
1013 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1014 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1014 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1015 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1015 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1016 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1016 html_safe
1017 html_safe
1017 end
1018 end
1018
1019
1019 def lang_options_for_select(blank=true)
1020 def lang_options_for_select(blank=true)
1020 (blank ? [["(auto)", ""]] : []) + languages_options
1021 (blank ? [["(auto)", ""]] : []) + languages_options
1021 end
1022 end
1022
1023
1023 def labelled_form_for(*args, &proc)
1024 def labelled_form_for(*args, &proc)
1024 args << {} unless args.last.is_a?(Hash)
1025 args << {} unless args.last.is_a?(Hash)
1025 options = args.last
1026 options = args.last
1026 if args.first.is_a?(Symbol)
1027 if args.first.is_a?(Symbol)
1027 options.merge!(:as => args.shift)
1028 options.merge!(:as => args.shift)
1028 end
1029 end
1029 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1030 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1030 form_for(*args, &proc)
1031 form_for(*args, &proc)
1031 end
1032 end
1032
1033
1033 def labelled_fields_for(*args, &proc)
1034 def labelled_fields_for(*args, &proc)
1034 args << {} unless args.last.is_a?(Hash)
1035 args << {} unless args.last.is_a?(Hash)
1035 options = args.last
1036 options = args.last
1036 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1037 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1037 fields_for(*args, &proc)
1038 fields_for(*args, &proc)
1038 end
1039 end
1039
1040
1040 def error_messages_for(*objects)
1041 def error_messages_for(*objects)
1041 html = ""
1042 html = ""
1042 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1043 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1043 errors = objects.map {|o| o.errors.full_messages}.flatten
1044 errors = objects.map {|o| o.errors.full_messages}.flatten
1044 if errors.any?
1045 if errors.any?
1045 html << "<div id='errorExplanation'><ul>\n"
1046 html << "<div id='errorExplanation'><ul>\n"
1046 errors.each do |error|
1047 errors.each do |error|
1047 html << "<li>#{h error}</li>\n"
1048 html << "<li>#{h error}</li>\n"
1048 end
1049 end
1049 html << "</ul></div>\n"
1050 html << "</ul></div>\n"
1050 end
1051 end
1051 html.html_safe
1052 html.html_safe
1052 end
1053 end
1053
1054
1054 def delete_link(url, options={})
1055 def delete_link(url, options={})
1055 options = {
1056 options = {
1056 :method => :delete,
1057 :method => :delete,
1057 :data => {:confirm => l(:text_are_you_sure)},
1058 :data => {:confirm => l(:text_are_you_sure)},
1058 :class => 'icon icon-del'
1059 :class => 'icon icon-del'
1059 }.merge(options)
1060 }.merge(options)
1060
1061
1061 link_to l(:button_delete), url, options
1062 link_to l(:button_delete), url, options
1062 end
1063 end
1063
1064
1064 def preview_link(url, form, target='preview', options={})
1065 def preview_link(url, form, target='preview', options={})
1065 content_tag 'a', l(:label_preview), {
1066 content_tag 'a', l(:label_preview), {
1066 :href => "#",
1067 :href => "#",
1067 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1068 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1068 :accesskey => accesskey(:preview)
1069 :accesskey => accesskey(:preview)
1069 }.merge(options)
1070 }.merge(options)
1070 end
1071 end
1071
1072
1072 def link_to_function(name, function, html_options={})
1073 def link_to_function(name, function, html_options={})
1073 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1074 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1074 end
1075 end
1075
1076
1076 # Helper to render JSON in views
1077 # Helper to render JSON in views
1077 def raw_json(arg)
1078 def raw_json(arg)
1078 arg.to_json.to_s.gsub('/', '\/').html_safe
1079 arg.to_json.to_s.gsub('/', '\/').html_safe
1079 end
1080 end
1080
1081
1081 def back_url
1082 def back_url
1082 url = params[:back_url]
1083 url = params[:back_url]
1083 if url.nil? && referer = request.env['HTTP_REFERER']
1084 if url.nil? && referer = request.env['HTTP_REFERER']
1084 url = CGI.unescape(referer.to_s)
1085 url = CGI.unescape(referer.to_s)
1085 end
1086 end
1086 url
1087 url
1087 end
1088 end
1088
1089
1089 def back_url_hidden_field_tag
1090 def back_url_hidden_field_tag
1090 url = back_url
1091 url = back_url
1091 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1092 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1092 end
1093 end
1093
1094
1094 def check_all_links(form_name)
1095 def check_all_links(form_name)
1095 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1096 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1096 " | ".html_safe +
1097 " | ".html_safe +
1097 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1098 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1098 end
1099 end
1099
1100
1100 def toggle_checkboxes_link(selector)
1101 def toggle_checkboxes_link(selector)
1101 link_to_function image_tag('toggle_check.png'),
1102 link_to_function '',
1102 "toggleCheckboxesBySelector('#{selector}')",
1103 "toggleCheckboxesBySelector('#{selector}')",
1103 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1104 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1105 :class => 'toggle-checkboxes'
1104 end
1106 end
1105
1107
1106 def progress_bar(pcts, options={})
1108 def progress_bar(pcts, options={})
1107 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1109 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1108 pcts = pcts.collect(&:round)
1110 pcts = pcts.collect(&:round)
1109 pcts[1] = pcts[1] - pcts[0]
1111 pcts[1] = pcts[1] - pcts[0]
1110 pcts << (100 - pcts[1] - pcts[0])
1112 pcts << (100 - pcts[1] - pcts[0])
1111 titles = options[:titles].to_a
1113 titles = options[:titles].to_a
1112 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1114 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1113 legend = options[:legend] || ''
1115 legend = options[:legend] || ''
1114 content_tag('table',
1116 content_tag('table',
1115 content_tag('tr',
1117 content_tag('tr',
1116 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1118 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1117 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1119 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1118 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1120 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1119 ), :class => "progress progress-#{pcts[0]}").html_safe +
1121 ), :class => "progress progress-#{pcts[0]}").html_safe +
1120 content_tag('p', legend, :class => 'percent').html_safe
1122 content_tag('p', legend, :class => 'percent').html_safe
1121 end
1123 end
1122
1124
1123 def checked_image(checked=true)
1125 def checked_image(checked=true)
1124 if checked
1126 if checked
1125 @checked_image_tag ||= image_tag('toggle_check.png')
1127 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1126 end
1128 end
1127 end
1129 end
1128
1130
1129 def context_menu(url)
1131 def context_menu(url)
1130 unless @context_menu_included
1132 unless @context_menu_included
1131 content_for :header_tags do
1133 content_for :header_tags do
1132 javascript_include_tag('context_menu') +
1134 javascript_include_tag('context_menu') +
1133 stylesheet_link_tag('context_menu')
1135 stylesheet_link_tag('context_menu')
1134 end
1136 end
1135 if l(:direction) == 'rtl'
1137 if l(:direction) == 'rtl'
1136 content_for :header_tags do
1138 content_for :header_tags do
1137 stylesheet_link_tag('context_menu_rtl')
1139 stylesheet_link_tag('context_menu_rtl')
1138 end
1140 end
1139 end
1141 end
1140 @context_menu_included = true
1142 @context_menu_included = true
1141 end
1143 end
1142 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1144 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1143 end
1145 end
1144
1146
1145 def calendar_for(field_id)
1147 def calendar_for(field_id)
1146 include_calendar_headers_tags
1148 include_calendar_headers_tags
1147 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1149 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1148 end
1150 end
1149
1151
1150 def include_calendar_headers_tags
1152 def include_calendar_headers_tags
1151 unless @calendar_headers_tags_included
1153 unless @calendar_headers_tags_included
1152 tags = ''.html_safe
1154 tags = ''.html_safe
1153 @calendar_headers_tags_included = true
1155 @calendar_headers_tags_included = true
1154 content_for :header_tags do
1156 content_for :header_tags do
1155 start_of_week = Setting.start_of_week
1157 start_of_week = Setting.start_of_week
1156 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1158 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1157 # Redmine uses 1..7 (monday..sunday) in settings and locales
1159 # Redmine uses 1..7 (monday..sunday) in settings and locales
1158 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1160 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1159 start_of_week = start_of_week.to_i % 7
1161 start_of_week = start_of_week.to_i % 7
1160 tags << javascript_tag(
1162 tags << javascript_tag(
1161 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1163 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1162 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1164 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1163 path_to_image('/images/calendar.png') +
1165 path_to_image('/images/calendar.png') +
1164 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1166 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1165 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1167 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1166 "beforeShow: beforeShowDatePicker};")
1168 "beforeShow: beforeShowDatePicker};")
1167 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1169 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1168 unless jquery_locale == 'en'
1170 unless jquery_locale == 'en'
1169 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1171 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1170 end
1172 end
1171 tags
1173 tags
1172 end
1174 end
1173 end
1175 end
1174 end
1176 end
1175
1177
1176 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1178 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1177 # Examples:
1179 # Examples:
1178 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1180 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1179 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1181 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1180 #
1182 #
1181 def stylesheet_link_tag(*sources)
1183 def stylesheet_link_tag(*sources)
1182 options = sources.last.is_a?(Hash) ? sources.pop : {}
1184 options = sources.last.is_a?(Hash) ? sources.pop : {}
1183 plugin = options.delete(:plugin)
1185 plugin = options.delete(:plugin)
1184 sources = sources.map do |source|
1186 sources = sources.map do |source|
1185 if plugin
1187 if plugin
1186 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1188 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1187 elsif current_theme && current_theme.stylesheets.include?(source)
1189 elsif current_theme && current_theme.stylesheets.include?(source)
1188 current_theme.stylesheet_path(source)
1190 current_theme.stylesheet_path(source)
1189 else
1191 else
1190 source
1192 source
1191 end
1193 end
1192 end
1194 end
1193 super *sources, options
1195 super *sources, options
1194 end
1196 end
1195
1197
1196 # Overrides Rails' image_tag with themes and plugins support.
1198 # Overrides Rails' image_tag with themes and plugins support.
1197 # Examples:
1199 # Examples:
1198 # image_tag('image.png') # => picks image.png from the current theme or defaults
1200 # image_tag('image.png') # => picks image.png from the current theme or defaults
1199 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1201 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1200 #
1202 #
1201 def image_tag(source, options={})
1203 def image_tag(source, options={})
1202 if plugin = options.delete(:plugin)
1204 if plugin = options.delete(:plugin)
1203 source = "/plugin_assets/#{plugin}/images/#{source}"
1205 source = "/plugin_assets/#{plugin}/images/#{source}"
1204 elsif current_theme && current_theme.images.include?(source)
1206 elsif current_theme && current_theme.images.include?(source)
1205 source = current_theme.image_path(source)
1207 source = current_theme.image_path(source)
1206 end
1208 end
1207 super source, options
1209 super source, options
1208 end
1210 end
1209
1211
1210 # Overrides Rails' javascript_include_tag with plugins support
1212 # Overrides Rails' javascript_include_tag with plugins support
1211 # Examples:
1213 # Examples:
1212 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1214 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1213 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1215 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1214 #
1216 #
1215 def javascript_include_tag(*sources)
1217 def javascript_include_tag(*sources)
1216 options = sources.last.is_a?(Hash) ? sources.pop : {}
1218 options = sources.last.is_a?(Hash) ? sources.pop : {}
1217 if plugin = options.delete(:plugin)
1219 if plugin = options.delete(:plugin)
1218 sources = sources.map do |source|
1220 sources = sources.map do |source|
1219 if plugin
1221 if plugin
1220 "/plugin_assets/#{plugin}/javascripts/#{source}"
1222 "/plugin_assets/#{plugin}/javascripts/#{source}"
1221 else
1223 else
1222 source
1224 source
1223 end
1225 end
1224 end
1226 end
1225 end
1227 end
1226 super *sources, options
1228 super *sources, options
1227 end
1229 end
1228
1230
1229 def sidebar_content?
1231 def sidebar_content?
1230 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1232 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1231 end
1233 end
1232
1234
1233 def view_layouts_base_sidebar_hook_response
1235 def view_layouts_base_sidebar_hook_response
1234 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1236 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1235 end
1237 end
1236
1238
1237 def email_delivery_enabled?
1239 def email_delivery_enabled?
1238 !!ActionMailer::Base.perform_deliveries
1240 !!ActionMailer::Base.perform_deliveries
1239 end
1241 end
1240
1242
1241 # Returns the avatar image tag for the given +user+ if avatars are enabled
1243 # Returns the avatar image tag for the given +user+ if avatars are enabled
1242 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1244 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1243 def avatar(user, options = { })
1245 def avatar(user, options = { })
1244 if Setting.gravatar_enabled?
1246 if Setting.gravatar_enabled?
1245 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1247 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1246 email = nil
1248 email = nil
1247 if user.respond_to?(:mail)
1249 if user.respond_to?(:mail)
1248 email = user.mail
1250 email = user.mail
1249 elsif user.to_s =~ %r{<(.+?)>}
1251 elsif user.to_s =~ %r{<(.+?)>}
1250 email = $1
1252 email = $1
1251 end
1253 end
1252 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1254 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1253 else
1255 else
1254 ''
1256 ''
1255 end
1257 end
1256 end
1258 end
1257
1259
1258 # Returns a link to edit user's avatar if avatars are enabled
1260 # Returns a link to edit user's avatar if avatars are enabled
1259 def avatar_edit_link(user, options={})
1261 def avatar_edit_link(user, options={})
1260 if Setting.gravatar_enabled?
1262 if Setting.gravatar_enabled?
1261 url = "https://gravatar.com"
1263 url = "https://gravatar.com"
1262 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1264 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1263 end
1265 end
1264 end
1266 end
1265
1267
1266 def sanitize_anchor_name(anchor)
1268 def sanitize_anchor_name(anchor)
1267 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1269 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1268 end
1270 end
1269
1271
1270 # Returns the javascript tags that are included in the html layout head
1272 # Returns the javascript tags that are included in the html layout head
1271 def javascript_heads
1273 def javascript_heads
1272 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1274 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1273 unless User.current.pref.warn_on_leaving_unsaved == '0'
1275 unless User.current.pref.warn_on_leaving_unsaved == '0'
1274 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1276 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1275 end
1277 end
1276 tags
1278 tags
1277 end
1279 end
1278
1280
1279 def favicon
1281 def favicon
1280 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1282 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1281 end
1283 end
1282
1284
1283 # Returns the path to the favicon
1285 # Returns the path to the favicon
1284 def favicon_path
1286 def favicon_path
1285 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1287 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1286 image_path(icon)
1288 image_path(icon)
1287 end
1289 end
1288
1290
1289 # Returns the full URL to the favicon
1291 # Returns the full URL to the favicon
1290 def favicon_url
1292 def favicon_url
1291 # TODO: use #image_url introduced in Rails4
1293 # TODO: use #image_url introduced in Rails4
1292 path = favicon_path
1294 path = favicon_path
1293 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1295 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1294 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1296 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1295 end
1297 end
1296
1298
1297 def robot_exclusion_tag
1299 def robot_exclusion_tag
1298 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1300 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1299 end
1301 end
1300
1302
1301 # Returns true if arg is expected in the API response
1303 # Returns true if arg is expected in the API response
1302 def include_in_api_response?(arg)
1304 def include_in_api_response?(arg)
1303 unless @included_in_api_response
1305 unless @included_in_api_response
1304 param = params[:include]
1306 param = params[:include]
1305 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1307 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1306 @included_in_api_response.collect!(&:strip)
1308 @included_in_api_response.collect!(&:strip)
1307 end
1309 end
1308 @included_in_api_response.include?(arg.to_s)
1310 @included_in_api_response.include?(arg.to_s)
1309 end
1311 end
1310
1312
1311 # Returns options or nil if nometa param or X-Redmine-Nometa header
1313 # Returns options or nil if nometa param or X-Redmine-Nometa header
1312 # was set in the request
1314 # was set in the request
1313 def api_meta(options)
1315 def api_meta(options)
1314 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1316 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1315 # compatibility mode for activeresource clients that raise
1317 # compatibility mode for activeresource clients that raise
1316 # an error when deserializing an array with attributes
1318 # an error when deserializing an array with attributes
1317 nil
1319 nil
1318 else
1320 else
1319 options
1321 options
1320 end
1322 end
1321 end
1323 end
1322
1324
1323 def generate_csv(&block)
1325 def generate_csv(&block)
1324 decimal_separator = l(:general_csv_decimal_separator)
1326 decimal_separator = l(:general_csv_decimal_separator)
1325 encoding = l(:general_csv_encoding)
1327 encoding = l(:general_csv_encoding)
1326 end
1328 end
1327
1329
1328 private
1330 private
1329
1331
1330 def wiki_helper
1332 def wiki_helper
1331 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1333 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1332 extend helper
1334 extend helper
1333 return self
1335 return self
1334 end
1336 end
1335
1337
1336 def link_to_content_update(text, url_params = {}, html_options = {})
1338 def link_to_content_update(text, url_params = {}, html_options = {})
1337 link_to(text, url_params, html_options)
1339 link_to(text, url_params, html_options)
1338 end
1340 end
1339 end
1341 end
@@ -1,38 +1,38
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 module EmailAddressesHelper
20 module EmailAddressesHelper
21
21
22 # Returns a link to enable or disable notifications for the address
22 # Returns a link to enable or disable notifications for the address
23 def toggle_email_address_notify_link(address)
23 def toggle_email_address_notify_link(address)
24 if address.notify?
24 if address.notify?
25 link_to image_tag('email.png'),
25 link_to '',
26 user_email_address_path(address.user, address, :notify => '0'),
26 user_email_address_path(address.user, address, :notify => '0'),
27 :method => :put,
27 :method => :put, :remote => true,
28 :title => l(:label_disable_notifications),
28 :title => l(:label_disable_notifications),
29 :remote => true
29 :class => 'icon icon-email'
30 else
30 else
31 link_to image_tag('email_disabled.png'),
31 link_to '',
32 user_email_address_path(address.user, address, :notify => '1'),
32 user_email_address_path(address.user, address, :notify => '1'),
33 :method => :put,
33 :method => :put, :remote => true,
34 :title => l(:label_enable_notifications),
34 :title => l(:label_enable_notifications),
35 :remote => true
35 :class => 'icon icon-email-disabled'
36 end
36 end
37 end
37 end
38 end
38 end
@@ -1,516 +1,515
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
23
23
24 def issue_list(issues, &block)
24 def issue_list(issues, &block)
25 ancestors = []
25 ancestors = []
26 issues.each do |issue|
26 issues.each do |issue|
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
28 ancestors.pop
28 ancestors.pop
29 end
29 end
30 yield issue, ancestors.size
30 yield issue, ancestors.size
31 ancestors << issue unless issue.leaf?
31 ancestors << issue unless issue.leaf?
32 end
32 end
33 end
33 end
34
34
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
36 previous_group, first = false, true
36 previous_group, first = false, true
37 totals_by_group = query.totalable_columns.inject({}) do |h, column|
37 totals_by_group = query.totalable_columns.inject({}) do |h, column|
38 h[column] = query.total_by_group_for(column)
38 h[column] = query.total_by_group_for(column)
39 h
39 h
40 end
40 end
41 issue_list(issues) do |issue, level|
41 issue_list(issues) do |issue, level|
42 group_name = group_count = nil
42 group_name = group_count = nil
43 if query.grouped?
43 if query.grouped?
44 group = query.group_by_column.value(issue)
44 group = query.group_by_column.value(issue)
45 if first || group != previous_group
45 if first || group != previous_group
46 if group.blank? && group != false
46 if group.blank? && group != false
47 group_name = "(#{l(:label_blank_value)})"
47 group_name = "(#{l(:label_blank_value)})"
48 else
48 else
49 group_name = format_object(group)
49 group_name = format_object(group)
50 end
50 end
51 group_name ||= ""
51 group_name ||= ""
52 group_count = issue_count_by_group[group]
52 group_count = issue_count_by_group[group]
53 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
53 group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
54 end
54 end
55 end
55 end
56 yield issue, level, group_name, group_count, group_totals
56 yield issue, level, group_name, group_count, group_totals
57 previous_group, first = group, false
57 previous_group, first = group, false
58 end
58 end
59 end
59 end
60
60
61 # Renders a HTML/CSS tooltip
61 # Renders a HTML/CSS tooltip
62 #
62 #
63 # To use, a trigger div is needed. This is a div with the class of "tooltip"
63 # To use, a trigger div is needed. This is a div with the class of "tooltip"
64 # that contains this method wrapped in a span with the class of "tip"
64 # that contains this method wrapped in a span with the class of "tip"
65 #
65 #
66 # <div class="tooltip"><%= link_to_issue(issue) %>
66 # <div class="tooltip"><%= link_to_issue(issue) %>
67 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
67 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
68 # </div>
68 # </div>
69 #
69 #
70 def render_issue_tooltip(issue)
70 def render_issue_tooltip(issue)
71 @cached_label_status ||= l(:field_status)
71 @cached_label_status ||= l(:field_status)
72 @cached_label_start_date ||= l(:field_start_date)
72 @cached_label_start_date ||= l(:field_start_date)
73 @cached_label_due_date ||= l(:field_due_date)
73 @cached_label_due_date ||= l(:field_due_date)
74 @cached_label_assigned_to ||= l(:field_assigned_to)
74 @cached_label_assigned_to ||= l(:field_assigned_to)
75 @cached_label_priority ||= l(:field_priority)
75 @cached_label_priority ||= l(:field_priority)
76 @cached_label_project ||= l(:field_project)
76 @cached_label_project ||= l(:field_project)
77
77
78 link_to_issue(issue) + "<br /><br />".html_safe +
78 link_to_issue(issue) + "<br /><br />".html_safe +
79 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
79 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
80 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
80 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
81 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
81 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
82 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
82 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
83 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
83 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
84 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
84 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
85 end
85 end
86
86
87 def issue_heading(issue)
87 def issue_heading(issue)
88 h("#{issue.tracker} ##{issue.id}")
88 h("#{issue.tracker} ##{issue.id}")
89 end
89 end
90
90
91 def render_issue_subject_with_tree(issue)
91 def render_issue_subject_with_tree(issue)
92 s = ''
92 s = ''
93 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
93 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
94 ancestors.each do |ancestor|
94 ancestors.each do |ancestor|
95 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
95 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
96 end
96 end
97 s << '<div>'
97 s << '<div>'
98 subject = h(issue.subject)
98 subject = h(issue.subject)
99 if issue.is_private?
99 if issue.is_private?
100 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
100 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
101 end
101 end
102 s << content_tag('h3', subject)
102 s << content_tag('h3', subject)
103 s << '</div>' * (ancestors.size + 1)
103 s << '</div>' * (ancestors.size + 1)
104 s.html_safe
104 s.html_safe
105 end
105 end
106
106
107 def render_descendants_tree(issue)
107 def render_descendants_tree(issue)
108 s = '<form><table class="list issues">'
108 s = '<form><table class="list issues">'
109 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker).sort_by(&:lft)) do |child, level|
109 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker).sort_by(&:lft)) do |child, level|
110 css = "issue issue-#{child.id} hascontextmenu #{issue.css_classes}"
110 css = "issue issue-#{child.id} hascontextmenu #{issue.css_classes}"
111 css << " idnt idnt-#{level}" if level > 0
111 css << " idnt idnt-#{level}" if level > 0
112 s << content_tag('tr',
112 s << content_tag('tr',
113 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
113 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
114 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
114 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
115 content_tag('td', h(child.status)) +
115 content_tag('td', h(child.status)) +
116 content_tag('td', link_to_user(child.assigned_to)) +
116 content_tag('td', link_to_user(child.assigned_to)) +
117 content_tag('td', progress_bar(child.done_ratio)),
117 content_tag('td', progress_bar(child.done_ratio)),
118 :class => css)
118 :class => css)
119 end
119 end
120 s << '</table></form>'
120 s << '</table></form>'
121 s.html_safe
121 s.html_safe
122 end
122 end
123
123
124 def issue_estimated_hours_details(issue)
124 def issue_estimated_hours_details(issue)
125 if issue.total_estimated_hours.present?
125 if issue.total_estimated_hours.present?
126 if issue.total_estimated_hours == issue.estimated_hours
126 if issue.total_estimated_hours == issue.estimated_hours
127 l_hours_short(issue.estimated_hours)
127 l_hours_short(issue.estimated_hours)
128 else
128 else
129 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
129 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
130 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
130 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
131 s.html_safe
131 s.html_safe
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def issue_spent_hours_details(issue)
136 def issue_spent_hours_details(issue)
137 if issue.total_spent_hours > 0
137 if issue.total_spent_hours > 0
138 if issue.total_spent_hours == issue.spent_hours
138 if issue.total_spent_hours == issue.spent_hours
139 link_to(l_hours_short(issue.spent_hours), issue_time_entries_path(issue))
139 link_to(l_hours_short(issue.spent_hours), issue_time_entries_path(issue))
140 else
140 else
141 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
141 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
142 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), issue_time_entries_path(issue)})"
142 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), issue_time_entries_path(issue)})"
143 s.html_safe
143 s.html_safe
144 end
144 end
145 end
145 end
146 end
146 end
147
147
148 # Returns an array of error messages for bulk edited issues
148 # Returns an array of error messages for bulk edited issues
149 def bulk_edit_error_messages(issues)
149 def bulk_edit_error_messages(issues)
150 messages = {}
150 messages = {}
151 issues.each do |issue|
151 issues.each do |issue|
152 issue.errors.full_messages.each do |message|
152 issue.errors.full_messages.each do |message|
153 messages[message] ||= []
153 messages[message] ||= []
154 messages[message] << issue
154 messages[message] << issue
155 end
155 end
156 end
156 end
157 messages.map { |message, issues|
157 messages.map { |message, issues|
158 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
158 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
159 }
159 }
160 end
160 end
161
161
162 # Returns a link for adding a new subtask to the given issue
162 # Returns a link for adding a new subtask to the given issue
163 def link_to_new_subtask(issue)
163 def link_to_new_subtask(issue)
164 attrs = {
164 attrs = {
165 :tracker_id => issue.tracker,
165 :tracker_id => issue.tracker,
166 :parent_issue_id => issue
166 :parent_issue_id => issue
167 }
167 }
168 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
168 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
169 end
169 end
170
170
171 class IssueFieldsRows
171 class IssueFieldsRows
172 include ActionView::Helpers::TagHelper
172 include ActionView::Helpers::TagHelper
173
173
174 def initialize
174 def initialize
175 @left = []
175 @left = []
176 @right = []
176 @right = []
177 end
177 end
178
178
179 def left(*args)
179 def left(*args)
180 args.any? ? @left << cells(*args) : @left
180 args.any? ? @left << cells(*args) : @left
181 end
181 end
182
182
183 def right(*args)
183 def right(*args)
184 args.any? ? @right << cells(*args) : @right
184 args.any? ? @right << cells(*args) : @right
185 end
185 end
186
186
187 def size
187 def size
188 @left.size > @right.size ? @left.size : @right.size
188 @left.size > @right.size ? @left.size : @right.size
189 end
189 end
190
190
191 def to_html
191 def to_html
192 content =
192 content =
193 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
193 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
194 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
194 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
195
195
196 content_tag('div', content, :class => 'splitcontent')
196 content_tag('div', content, :class => 'splitcontent')
197 end
197 end
198
198
199 def cells(label, text, options={})
199 def cells(label, text, options={})
200 options[:class] = [options[:class] || "", 'attribute'].join(' ')
200 options[:class] = [options[:class] || "", 'attribute'].join(' ')
201 content_tag 'div',
201 content_tag 'div',
202 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
202 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
203 options
203 options
204 end
204 end
205 end
205 end
206
206
207 def issue_fields_rows
207 def issue_fields_rows
208 r = IssueFieldsRows.new
208 r = IssueFieldsRows.new
209 yield r
209 yield r
210 r.to_html
210 r.to_html
211 end
211 end
212
212
213 def render_custom_fields_rows(issue)
213 def render_custom_fields_rows(issue)
214 values = issue.visible_custom_field_values
214 values = issue.visible_custom_field_values
215 return if values.empty?
215 return if values.empty?
216 half = (values.size / 2.0).ceil
216 half = (values.size / 2.0).ceil
217 issue_fields_rows do |rows|
217 issue_fields_rows do |rows|
218 values.each_with_index do |value, i|
218 values.each_with_index do |value, i|
219 css = "cf_#{value.custom_field.id}"
219 css = "cf_#{value.custom_field.id}"
220 m = (i < half ? :left : :right)
220 m = (i < half ? :left : :right)
221 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
221 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
222 end
222 end
223 end
223 end
224 end
224 end
225
225
226 # Returns the path for updating the issue form
226 # Returns the path for updating the issue form
227 # with project as the current project
227 # with project as the current project
228 def update_issue_form_path(project, issue)
228 def update_issue_form_path(project, issue)
229 options = {:format => 'js'}
229 options = {:format => 'js'}
230 if issue.new_record?
230 if issue.new_record?
231 if project
231 if project
232 new_project_issue_path(project, options)
232 new_project_issue_path(project, options)
233 else
233 else
234 new_issue_path(options)
234 new_issue_path(options)
235 end
235 end
236 else
236 else
237 edit_issue_path(issue, options)
237 edit_issue_path(issue, options)
238 end
238 end
239 end
239 end
240
240
241 # Returns the number of descendants for an array of issues
241 # Returns the number of descendants for an array of issues
242 def issues_descendant_count(issues)
242 def issues_descendant_count(issues)
243 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
243 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
244 ids -= issues.map(&:id)
244 ids -= issues.map(&:id)
245 ids.size
245 ids.size
246 end
246 end
247
247
248 def issues_destroy_confirmation_message(issues)
248 def issues_destroy_confirmation_message(issues)
249 issues = [issues] unless issues.is_a?(Array)
249 issues = [issues] unless issues.is_a?(Array)
250 message = l(:text_issues_destroy_confirmation)
250 message = l(:text_issues_destroy_confirmation)
251
251
252 descendant_count = issues_descendant_count(issues)
252 descendant_count = issues_descendant_count(issues)
253 if descendant_count > 0
253 if descendant_count > 0
254 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
254 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
255 end
255 end
256 message
256 message
257 end
257 end
258
258
259 # Returns an array of users that are proposed as watchers
259 # Returns an array of users that are proposed as watchers
260 # on the new issue form
260 # on the new issue form
261 def users_for_new_issue_watchers(issue)
261 def users_for_new_issue_watchers(issue)
262 users = issue.watcher_users
262 users = issue.watcher_users
263 if issue.project.users.count <= 20
263 if issue.project.users.count <= 20
264 users = (users + issue.project.users.sort).uniq
264 users = (users + issue.project.users.sort).uniq
265 end
265 end
266 users
266 users
267 end
267 end
268
268
269 def sidebar_queries
269 def sidebar_queries
270 unless @sidebar_queries
270 unless @sidebar_queries
271 @sidebar_queries = IssueQuery.visible.
271 @sidebar_queries = IssueQuery.visible.
272 order("#{Query.table_name}.name ASC").
272 order("#{Query.table_name}.name ASC").
273 # Project specific queries and global queries
273 # Project specific queries and global queries
274 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
274 where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]).
275 to_a
275 to_a
276 end
276 end
277 @sidebar_queries
277 @sidebar_queries
278 end
278 end
279
279
280 def query_links(title, queries)
280 def query_links(title, queries)
281 return '' if queries.empty?
281 return '' if queries.empty?
282 # links to #index on issues/show
282 # links to #index on issues/show
283 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
283 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
284
284
285 content_tag('h3', title) + "\n" +
285 content_tag('h3', title) + "\n" +
286 content_tag('ul',
286 content_tag('ul',
287 queries.collect {|query|
287 queries.collect {|query|
288 css = 'query'
288 css = 'query'
289 css << ' selected' if query == @query
289 css << ' selected' if query == @query
290 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
290 content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css))
291 }.join("\n").html_safe,
291 }.join("\n").html_safe,
292 :class => 'queries'
292 :class => 'queries'
293 ) + "\n"
293 ) + "\n"
294 end
294 end
295
295
296 def render_sidebar_queries
296 def render_sidebar_queries
297 out = ''.html_safe
297 out = ''.html_safe
298 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
298 out << query_links(l(:label_my_queries), sidebar_queries.select(&:is_private?))
299 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
299 out << query_links(l(:label_query_plural), sidebar_queries.reject(&:is_private?))
300 out
300 out
301 end
301 end
302
302
303 def email_issue_attributes(issue, user)
303 def email_issue_attributes(issue, user)
304 items = []
304 items = []
305 %w(author status priority assigned_to category fixed_version).each do |attribute|
305 %w(author status priority assigned_to category fixed_version).each do |attribute|
306 unless issue.disabled_core_fields.include?(attribute+"_id")
306 unless issue.disabled_core_fields.include?(attribute+"_id")
307 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
307 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
308 end
308 end
309 end
309 end
310 issue.visible_custom_field_values(user).each do |value|
310 issue.visible_custom_field_values(user).each do |value|
311 items << "#{value.custom_field.name}: #{show_value(value, false)}"
311 items << "#{value.custom_field.name}: #{show_value(value, false)}"
312 end
312 end
313 items
313 items
314 end
314 end
315
315
316 def render_email_issue_attributes(issue, user, html=false)
316 def render_email_issue_attributes(issue, user, html=false)
317 items = email_issue_attributes(issue, user)
317 items = email_issue_attributes(issue, user)
318 if html
318 if html
319 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
319 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
320 else
320 else
321 items.map{|s| "* #{s}"}.join("\n")
321 items.map{|s| "* #{s}"}.join("\n")
322 end
322 end
323 end
323 end
324
324
325 # Returns the textual representation of a journal details
325 # Returns the textual representation of a journal details
326 # as an array of strings
326 # as an array of strings
327 def details_to_strings(details, no_html=false, options={})
327 def details_to_strings(details, no_html=false, options={})
328 options[:only_path] = (options[:only_path] == false ? false : true)
328 options[:only_path] = (options[:only_path] == false ? false : true)
329 strings = []
329 strings = []
330 values_by_field = {}
330 values_by_field = {}
331 details.each do |detail|
331 details.each do |detail|
332 if detail.property == 'cf'
332 if detail.property == 'cf'
333 field = detail.custom_field
333 field = detail.custom_field
334 if field && field.multiple?
334 if field && field.multiple?
335 values_by_field[field] ||= {:added => [], :deleted => []}
335 values_by_field[field] ||= {:added => [], :deleted => []}
336 if detail.old_value
336 if detail.old_value
337 values_by_field[field][:deleted] << detail.old_value
337 values_by_field[field][:deleted] << detail.old_value
338 end
338 end
339 if detail.value
339 if detail.value
340 values_by_field[field][:added] << detail.value
340 values_by_field[field][:added] << detail.value
341 end
341 end
342 next
342 next
343 end
343 end
344 end
344 end
345 strings << show_detail(detail, no_html, options)
345 strings << show_detail(detail, no_html, options)
346 end
346 end
347 if values_by_field.present?
347 if values_by_field.present?
348 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
348 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
349 values_by_field.each do |field, changes|
349 values_by_field.each do |field, changes|
350 if changes[:added].any?
350 if changes[:added].any?
351 detail = multiple_values_detail.new('cf', field.id.to_s, field)
351 detail = multiple_values_detail.new('cf', field.id.to_s, field)
352 detail.value = changes[:added]
352 detail.value = changes[:added]
353 strings << show_detail(detail, no_html, options)
353 strings << show_detail(detail, no_html, options)
354 end
354 end
355 if changes[:deleted].any?
355 if changes[:deleted].any?
356 detail = multiple_values_detail.new('cf', field.id.to_s, field)
356 detail = multiple_values_detail.new('cf', field.id.to_s, field)
357 detail.old_value = changes[:deleted]
357 detail.old_value = changes[:deleted]
358 strings << show_detail(detail, no_html, options)
358 strings << show_detail(detail, no_html, options)
359 end
359 end
360 end
360 end
361 end
361 end
362 strings
362 strings
363 end
363 end
364
364
365 # Returns the textual representation of a single journal detail
365 # Returns the textual representation of a single journal detail
366 def show_detail(detail, no_html=false, options={})
366 def show_detail(detail, no_html=false, options={})
367 multiple = false
367 multiple = false
368 show_diff = false
368 show_diff = false
369
369
370 case detail.property
370 case detail.property
371 when 'attr'
371 when 'attr'
372 field = detail.prop_key.to_s.gsub(/\_id$/, "")
372 field = detail.prop_key.to_s.gsub(/\_id$/, "")
373 label = l(("field_" + field).to_sym)
373 label = l(("field_" + field).to_sym)
374 case detail.prop_key
374 case detail.prop_key
375 when 'due_date', 'start_date'
375 when 'due_date', 'start_date'
376 value = format_date(detail.value.to_date) if detail.value
376 value = format_date(detail.value.to_date) if detail.value
377 old_value = format_date(detail.old_value.to_date) if detail.old_value
377 old_value = format_date(detail.old_value.to_date) if detail.old_value
378
378
379 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
379 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
380 'priority_id', 'category_id', 'fixed_version_id'
380 'priority_id', 'category_id', 'fixed_version_id'
381 value = find_name_by_reflection(field, detail.value)
381 value = find_name_by_reflection(field, detail.value)
382 old_value = find_name_by_reflection(field, detail.old_value)
382 old_value = find_name_by_reflection(field, detail.old_value)
383
383
384 when 'estimated_hours'
384 when 'estimated_hours'
385 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
385 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
386 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
386 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
387
387
388 when 'parent_id'
388 when 'parent_id'
389 label = l(:field_parent_issue)
389 label = l(:field_parent_issue)
390 value = "##{detail.value}" unless detail.value.blank?
390 value = "##{detail.value}" unless detail.value.blank?
391 old_value = "##{detail.old_value}" unless detail.old_value.blank?
391 old_value = "##{detail.old_value}" unless detail.old_value.blank?
392
392
393 when 'is_private'
393 when 'is_private'
394 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
394 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
395 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
395 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
396
396
397 when 'description'
397 when 'description'
398 show_diff = true
398 show_diff = true
399 end
399 end
400 when 'cf'
400 when 'cf'
401 custom_field = detail.custom_field
401 custom_field = detail.custom_field
402 if custom_field
402 if custom_field
403 label = custom_field.name
403 label = custom_field.name
404 if custom_field.format.class.change_as_diff
404 if custom_field.format.class.change_as_diff
405 show_diff = true
405 show_diff = true
406 else
406 else
407 multiple = custom_field.multiple?
407 multiple = custom_field.multiple?
408 value = format_value(detail.value, custom_field) if detail.value
408 value = format_value(detail.value, custom_field) if detail.value
409 old_value = format_value(detail.old_value, custom_field) if detail.old_value
409 old_value = format_value(detail.old_value, custom_field) if detail.old_value
410 end
410 end
411 end
411 end
412 when 'attachment'
412 when 'attachment'
413 label = l(:label_attachment)
413 label = l(:label_attachment)
414 when 'relation'
414 when 'relation'
415 if detail.value && !detail.old_value
415 if detail.value && !detail.old_value
416 rel_issue = Issue.visible.find_by_id(detail.value)
416 rel_issue = Issue.visible.find_by_id(detail.value)
417 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
417 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
418 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
418 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
419 elsif detail.old_value && !detail.value
419 elsif detail.old_value && !detail.value
420 rel_issue = Issue.visible.find_by_id(detail.old_value)
420 rel_issue = Issue.visible.find_by_id(detail.old_value)
421 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
421 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
422 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
422 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
423 end
423 end
424 relation_type = IssueRelation::TYPES[detail.prop_key]
424 relation_type = IssueRelation::TYPES[detail.prop_key]
425 label = l(relation_type[:name]) if relation_type
425 label = l(relation_type[:name]) if relation_type
426 end
426 end
427 call_hook(:helper_issues_show_detail_after_setting,
427 call_hook(:helper_issues_show_detail_after_setting,
428 {:detail => detail, :label => label, :value => value, :old_value => old_value })
428 {:detail => detail, :label => label, :value => value, :old_value => old_value })
429
429
430 label ||= detail.prop_key
430 label ||= detail.prop_key
431 value ||= detail.value
431 value ||= detail.value
432 old_value ||= detail.old_value
432 old_value ||= detail.old_value
433
433
434 unless no_html
434 unless no_html
435 label = content_tag('strong', label)
435 label = content_tag('strong', label)
436 old_value = content_tag("i", h(old_value)) if detail.old_value
436 old_value = content_tag("i", h(old_value)) if detail.old_value
437 if detail.old_value && detail.value.blank? && detail.property != 'relation'
437 if detail.old_value && detail.value.blank? && detail.property != 'relation'
438 old_value = content_tag("del", old_value)
438 old_value = content_tag("del", old_value)
439 end
439 end
440 if detail.property == 'attachment' && value.present? &&
440 if detail.property == 'attachment' && value.present? &&
441 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
441 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
442 # Link to the attachment if it has not been removed
442 # Link to the attachment if it has not been removed
443 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
443 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
444 if options[:only_path] != false && atta.is_text?
444 if options[:only_path] != false && atta.is_text?
445 value += link_to(
445 value += link_to('',
446 image_tag('magnifier.png'),
446 { :controller => 'attachments', :action => 'show',
447 :controller => 'attachments', :action => 'show',
447 :id => atta, :filename => atta.filename },
448 :id => atta, :filename => atta.filename
448 :class => 'icon icon-magnifier')
449 )
450 end
449 end
451 else
450 else
452 value = content_tag("i", h(value)) if value
451 value = content_tag("i", h(value)) if value
453 end
452 end
454 end
453 end
455
454
456 if show_diff
455 if show_diff
457 s = l(:text_journal_changed_no_detail, :label => label)
456 s = l(:text_journal_changed_no_detail, :label => label)
458 unless no_html
457 unless no_html
459 diff_link = link_to 'diff',
458 diff_link = link_to 'diff',
460 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
459 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
461 :detail_id => detail.id, :only_path => options[:only_path]},
460 :detail_id => detail.id, :only_path => options[:only_path]},
462 :title => l(:label_view_diff)
461 :title => l(:label_view_diff)
463 s << " (#{ diff_link })"
462 s << " (#{ diff_link })"
464 end
463 end
465 s.html_safe
464 s.html_safe
466 elsif detail.value.present?
465 elsif detail.value.present?
467 case detail.property
466 case detail.property
468 when 'attr', 'cf'
467 when 'attr', 'cf'
469 if detail.old_value.present?
468 if detail.old_value.present?
470 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
469 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
471 elsif multiple
470 elsif multiple
472 l(:text_journal_added, :label => label, :value => value).html_safe
471 l(:text_journal_added, :label => label, :value => value).html_safe
473 else
472 else
474 l(:text_journal_set_to, :label => label, :value => value).html_safe
473 l(:text_journal_set_to, :label => label, :value => value).html_safe
475 end
474 end
476 when 'attachment', 'relation'
475 when 'attachment', 'relation'
477 l(:text_journal_added, :label => label, :value => value).html_safe
476 l(:text_journal_added, :label => label, :value => value).html_safe
478 end
477 end
479 else
478 else
480 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
479 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
481 end
480 end
482 end
481 end
483
482
484 # Find the name of an associated record stored in the field attribute
483 # Find the name of an associated record stored in the field attribute
485 def find_name_by_reflection(field, id)
484 def find_name_by_reflection(field, id)
486 unless id.present?
485 unless id.present?
487 return nil
486 return nil
488 end
487 end
489 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
488 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
490 association = Issue.reflect_on_association(key.first.to_sym)
489 association = Issue.reflect_on_association(key.first.to_sym)
491 name = nil
490 name = nil
492 if association
491 if association
493 record = association.klass.find_by_id(key.last)
492 record = association.klass.find_by_id(key.last)
494 if record
493 if record
495 name = record.name.force_encoding('UTF-8')
494 name = record.name.force_encoding('UTF-8')
496 end
495 end
497 end
496 end
498 hash[key] = name
497 hash[key] = name
499 end
498 end
500 @detail_value_name_by_reflection[[field, id]]
499 @detail_value_name_by_reflection[[field, id]]
501 end
500 end
502
501
503 # Renders issue children recursively
502 # Renders issue children recursively
504 def render_api_issue_children(issue, api)
503 def render_api_issue_children(issue, api)
505 return if issue.leaf?
504 return if issue.leaf?
506 api.array :children do
505 api.array :children do
507 issue.children.each do |child|
506 issue.children.each do |child|
508 api.issue(:id => child.id) do
507 api.issue(:id => child.id) do
509 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
508 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
510 api.subject child.subject
509 api.subject child.subject
511 render_api_issue_children(child, api)
510 render_api_issue_children(child, api)
512 end
511 end
513 end
512 end
514 end
513 end
515 end
514 end
516 end
515 end
@@ -1,53 +1,57
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 module JournalsHelper
20 module JournalsHelper
21
21
22 # Returns the attachments of a journal that are displayed as thumbnails
22 # Returns the attachments of a journal that are displayed as thumbnails
23 def journal_thumbnail_attachments(journal)
23 def journal_thumbnail_attachments(journal)
24 ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key)
24 ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key)
25 ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : []
25 ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : []
26 end
26 end
27
27
28 def render_notes(issue, journal, options={})
28 def render_notes(issue, journal, options={})
29 content = ''
29 content = ''
30 editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
30 editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
31 links = []
31 links = []
32 if !journal.notes.blank?
32 if !journal.notes.blank?
33 links << link_to(image_tag('comment.png'),
33 links << link_to('',
34 {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal},
34 {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal},
35 :remote => true,
35 :remote => true,
36 :method => 'post',
36 :method => 'post',
37 :title => l(:button_quote)) if options[:reply_links]
37 :title => l(:button_quote),
38 links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
38 :class => 'icon-only icon-comment'
39 ) if options[:reply_links]
40 links << link_to_in_place_notes_editor('', "journal-#{journal.id}-notes",
39 { :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
41 { :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' },
40 :title => l(:button_edit)) if editable
42 :title => l(:button_edit),
43 :class => 'icon-only icon-edit'
44 ) if editable
41 end
45 end
42 content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
46 content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
43 content << textilizable(journal, :notes)
47 content << textilizable(journal, :notes)
44 css_classes = "wiki"
48 css_classes = "wiki"
45 css_classes << " editable" if editable
49 css_classes << " editable" if editable
46 content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
50 content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
47 end
51 end
48
52
49 def link_to_in_place_notes_editor(text, field_id, url, options={})
53 def link_to_in_place_notes_editor(text, field_id, url, options={})
50 onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;"
54 onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;"
51 link_to text, '#', options.merge(:onclick => onclick)
55 link_to text, '#', options.merge(:onclick => onclick)
52 end
56 end
53 end
57 end
@@ -1,78 +1,80
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 module WatchersHelper
20 module WatchersHelper
21
21
22 def watcher_link(objects, user)
22 def watcher_link(objects, user)
23 return '' unless user && user.logged?
23 return '' unless user && user.logged?
24 objects = Array.wrap(objects)
24 objects = Array.wrap(objects)
25 return '' unless objects.any?
25 return '' unless objects.any?
26
26
27 watched = Watcher.any_watched?(objects, user)
27 watched = Watcher.any_watched?(objects, user)
28 css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
28 css = [watcher_css(objects), watched ? 'icon icon-fav' : 'icon icon-fav-off'].join(' ')
29 text = watched ? l(:button_unwatch) : l(:button_watch)
29 text = watched ? l(:button_unwatch) : l(:button_watch)
30 url = watch_path(
30 url = watch_path(
31 :object_type => objects.first.class.to_s.underscore,
31 :object_type => objects.first.class.to_s.underscore,
32 :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort)
32 :object_id => (objects.size == 1 ? objects.first.id : objects.map(&:id).sort)
33 )
33 )
34 method = watched ? 'delete' : 'post'
34 method = watched ? 'delete' : 'post'
35
35
36 link_to text, url, :remote => true, :method => method, :class => css
36 link_to text, url, :remote => true, :method => method, :class => css
37 end
37 end
38
38
39 # Returns the css class used to identify watch links for a given +object+
39 # Returns the css class used to identify watch links for a given +object+
40 def watcher_css(objects)
40 def watcher_css(objects)
41 objects = Array.wrap(objects)
41 objects = Array.wrap(objects)
42 id = (objects.size == 1 ? objects.first.id : 'bulk')
42 id = (objects.size == 1 ? objects.first.id : 'bulk')
43 "#{objects.first.class.to_s.underscore}-#{id}-watcher"
43 "#{objects.first.class.to_s.underscore}-#{id}-watcher"
44 end
44 end
45
45
46 # Returns a comma separated list of users watching the given object
46 # Returns a comma separated list of users watching the given object
47 def watchers_list(object)
47 def watchers_list(object)
48 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
48 remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
49 content = ''.html_safe
49 content = ''.html_safe
50 lis = object.watcher_users.collect do |user|
50 lis = object.watcher_users.collect do |user|
51 s = ''.html_safe
51 s = ''.html_safe
52 s << avatar(user, :size => "16").to_s
52 s << avatar(user, :size => "16").to_s
53 s << link_to_user(user, :class => 'user')
53 s << link_to_user(user, :class => 'user')
54 if remove_allowed
54 if remove_allowed
55 url = {:controller => 'watchers',
55 url = {:controller => 'watchers',
56 :action => 'destroy',
56 :action => 'destroy',
57 :object_type => object.class.to_s.underscore,
57 :object_type => object.class.to_s.underscore,
58 :object_id => object.id,
58 :object_id => object.id,
59 :user_id => user}
59 :user_id => user}
60 s << ' '
60 s << ' '
61 s << link_to(image_tag('delete.png'), url,
61 s << link_to('', url,
62 :remote => true, :method => 'delete', :class => "delete")
62 :remote => true, :method => 'delete',
63 :class => "delete icon-only icon-del",
64 :title => l(:button_delete))
63 end
65 end
64 content << content_tag('li', s, :class => "user-#{user.id}")
66 content << content_tag('li', s, :class => "user-#{user.id}")
65 end
67 end
66 content.present? ? content_tag('ul', content, :class => 'watchers') : content
68 content.present? ? content_tag('ul', content, :class => 'watchers') : content
67 end
69 end
68
70
69 def watchers_checkboxes(object, users, checked=nil)
71 def watchers_checkboxes(object, users, checked=nil)
70 users.map do |user|
72 users.map do |user|
71 c = checked.nil? ? object.watched_by?(user) : checked
73 c = checked.nil? ? object.watched_by?(user) : checked
72 tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
74 tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
73 content_tag 'label', "#{tag} #{h(user)}".html_safe,
75 content_tag 'label', "#{tag} #{h(user)}".html_safe,
74 :id => "issue_watcher_user_ids_#{user.id}",
76 :id => "issue_watcher_user_ids_#{user.id}",
75 :class => "floating"
77 :class => "floating"
76 end.join.html_safe
78 end.join.html_safe
77 end
79 end
78 end
80 end
@@ -1,19 +1,18
1 <h2><%=l(:label_information_plural)%></h2>
1 <h2><%=l(:label_information_plural)%></h2>
2
2
3 <p><strong><%= Redmine::Info.versioned_name %></strong></p>
3 <p><strong><%= Redmine::Info.versioned_name %></strong></p>
4
4
5 <table class="list">
5 <table class="list">
6 <% @checklist.each do |label, result| %>
6 <% @checklist.each do |label, result| %>
7 <tr class="<%= cycle 'odd', 'even' %>">
7 <tr class="<%= cycle 'odd', 'even' %>">
8 <td class="name"><%= label.is_a?(Symbol) ? l(label) : label %></td>
8 <td class="name"><%= label.is_a?(Symbol) ? l(label) : label %></td>
9 <td class="tick"><%= image_tag((result ? 'true.png' : 'exclamation.png'),
9 <td class="tick"><span class="icon-only <%= (result ? 'icon-ok' : 'icon-error') %>"></span></td>
10 :style => "vertical-align:bottom;") %></td>
11 </tr>
10 </tr>
12 <% end %>
11 <% end %>
13 </table>
12 </table>
14 <br />
13 <br />
15 <div class="box">
14 <div class="box">
16 <pre><%= Redmine::Info.environment %></pre>
15 <pre><%= Redmine::Info.environment %></pre>
17 </div>
16 </div>
18
17
19 <% html_title(l(:label_information_plural)) -%>
18 <% html_title(l(:label_information_plural)) -%>
@@ -1,63 +1,63
1 <%= title l(:label_plugins) %>
1 <%= title l(:label_plugins) %>
2
2
3 <% if @plugins.any? %>
3 <% if @plugins.any? %>
4 <table class="list plugins">
4 <table class="list plugins">
5 <% @plugins.each do |plugin| %>
5 <% @plugins.each do |plugin| %>
6 <tr id="plugin-<%= plugin.id %>" class="<%= cycle('odd', 'even') %>">
6 <tr id="plugin-<%= plugin.id %>" class="<%= cycle('odd', 'even') %>">
7 <td class="name"><span class="name"><%= plugin.name %></span>
7 <td class="name"><span class="name"><%= plugin.name %></span>
8 <%= content_tag('span', plugin.description, :class => 'description') unless plugin.description.blank? %>
8 <%= content_tag('span', plugin.description, :class => 'description') unless plugin.description.blank? %>
9 <%= content_tag('span', link_to(plugin.url, plugin.url), :class => 'url') unless plugin.url.blank? %>
9 <%= content_tag('span', link_to(plugin.url, plugin.url), :class => 'url') unless plugin.url.blank? %>
10 </td>
10 </td>
11 <td class="author"><%= plugin.author_url.blank? ? plugin.author : link_to(plugin.author, plugin.author_url) %></td>
11 <td class="author"><%= plugin.author_url.blank? ? plugin.author : link_to(plugin.author, plugin.author_url) %></td>
12 <td class="version"><span class="icon"><%= plugin.version %></span></td>
12 <td class="version"><span class="icon"><%= plugin.version %></span></td>
13 <td class="configure"><%= link_to(l(:button_configure), plugin_settings_path(plugin)) if plugin.configurable? %></td>
13 <td class="configure"><%= link_to(l(:button_configure), plugin_settings_path(plugin)) if plugin.configurable? %></td>
14 </tr>
14 </tr>
15 <% end %>
15 <% end %>
16 </table>
16 </table>
17 <p><a href="#" id="check-for-updates"><%= l(:label_check_for_updates) %></a></p>
17 <p><a href="#" id="check-for-updates"><%= l(:label_check_for_updates) %></a></p>
18 <% else %>
18 <% else %>
19 <p class="nodata"><%= l(:label_no_data) %></p>
19 <p class="nodata"><%= l(:label_no_data) %></p>
20 <% end %>
20 <% end %>
21
21
22 <%= javascript_tag do %>
22 <%= javascript_tag do %>
23 $(document).ready(function(){
23 $(document).ready(function(){
24 $("#check-for-updates").click(function(e){
24 $("#check-for-updates").click(function(e){
25 e.preventDefault();
25 e.preventDefault();
26 $.ajax({
26 $.ajax({
27 dataType: "jsonp",
27 dataType: "jsonp",
28 url: "https://www.redmine.org/plugins/check_updates",
28 url: "https://www.redmine.org/plugins/check_updates",
29 data: <%= raw_json plugin_data_for_updates(@plugins) %>,
29 data: <%= raw_json plugin_data_for_updates(@plugins) %>,
30 timeout: 3000,
30 timeout: 3000,
31 beforeSend: function(){
31 beforeSend: function(){
32 $('#ajax-indicator').show();
32 $('#ajax-indicator').show();
33 },
33 },
34 success: function(data){
34 success: function(data){
35 $('#ajax-indicator').hide();
35 $('#ajax-indicator').hide();
36 $("table.plugins td.version span").addClass("unknown");
36 $("table.plugins td.version span").addClass("unknown");
37 $.each(data, function(plugin_id, plugin_data){
37 $.each(data, function(plugin_id, plugin_data){
38 var s = $("tr#plugin-"+plugin_id+" td.version span");
38 var s = $("tr#plugin-"+plugin_id+" td.version span");
39 s.removeClass("icon-checked icon-warning unknown");
39 s.removeClass("icon-ok icon-warning unknown");
40 if (plugin_data.url) {
40 if (plugin_data.url) {
41 if (s.parent("a").length>0) {
41 if (s.parent("a").length>0) {
42 s.unwrap();
42 s.unwrap();
43 }
43 }
44 s.addClass("found");
44 s.addClass("found");
45 s.wrap($("<a></a>").attr("href", plugin_data.url).attr("target", "_blank"));
45 s.wrap($("<a></a>").attr("href", plugin_data.url).attr("target", "_blank"));
46 }
46 }
47 if (plugin_data.c == s.text()) {
47 if (plugin_data.c == s.text()) {
48 s.addClass("icon-checked");
48 s.addClass("icon-ok");
49 } else if (plugin_data.c) {
49 } else if (plugin_data.c) {
50 s.addClass("icon-warning");
50 s.addClass("icon-warning");
51 s.attr("title", "<%= escape_javascript l(:label_latest_compatible_version) %>: "+plugin_data.c);
51 s.attr("title", "<%= escape_javascript l(:label_latest_compatible_version) %>: "+plugin_data.c);
52 }
52 }
53 });
53 });
54 $("table.plugins td.version span.unknown").addClass("icon-help").attr("title", "<%= escape_javascript l(:label_unknown_plugin) %>");
54 $("table.plugins td.version span.unknown").addClass("icon-help").attr("title", "<%= escape_javascript l(:label_unknown_plugin) %>");
55 },
55 },
56 error: function(){
56 error: function(){
57 $('#ajax-indicator').hide();
57 $('#ajax-indicator').hide();
58 alert("Unable to retrieve plugin informations from www.redmine.org");
58 alert("Unable to retrieve plugin informations from www.redmine.org");
59 }
59 }
60 });
60 });
61 });
61 });
62 });
62 });
63 <% end if @plugins.any? %>
63 <% end if @plugins.any? %>
@@ -1,38 +1,42
1 <div class="attachments">
1 <div class="attachments">
2 <div class="contextual">
2 <div class="contextual">
3 <%= link_to image_tag('edit.png'),
3 <%= link_to('',
4 container_attachments_edit_path(container),
4 container_attachments_edit_path(container),
5 :title => l(:label_edit_attachments) if options[:editable] %>
5 :title => l(:label_edit_attachments),
6 :class => 'icon-only icon-edit'
7 ) if options[:editable] %>
6 </div>
8 </div>
7 <% for attachment in attachments %>
9 <% for attachment in attachments %>
8 <p><%= link_to_attachment attachment, :class => 'icon icon-attachment', :download => true -%>
10 <p><%= link_to_attachment attachment, :class => 'icon icon-attachment', :download => true -%>
9 <% if attachment.is_text? %>
11 <% if attachment.is_text? %>
10 <%= link_to image_tag('magnifier.png'),
12 <%= link_to '',
11 :controller => 'attachments', :action => 'show',
13 { :controller => 'attachments', :action => 'show',
12 :id => attachment, :filename => attachment.filename %>
14 :id => attachment, :filename => attachment.filename },
15 :class => 'icon icon-magnifier',
16 :title => l(:button_view) %>
13 <% end %>
17 <% end %>
14 <%= " - #{attachment.description}" unless attachment.description.blank? %>
18 <%= " - #{attachment.description}" unless attachment.description.blank? %>
15 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
19 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
16 <% if options[:deletable] %>
20 <% if options[:deletable] %>
17 <%= link_to image_tag('delete.png'), attachment_path(attachment),
21 <%= link_to '', attachment_path(attachment),
18 :data => {:confirm => l(:text_are_you_sure)},
22 :data => {:confirm => l(:text_are_you_sure)},
19 :method => :delete,
23 :method => :delete,
20 :class => 'delete',
24 :class => 'delete icon-only icon-del',
21 :title => l(:button_delete) %>
25 :title => l(:button_delete) %>
22 <% end %>
26 <% end %>
23 <% if options[:author] %>
27 <% if options[:author] %>
24 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
28 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
25 <% end %>
29 <% end %>
26 </p>
30 </p>
27 <% end %>
31 <% end %>
28 <% if defined?(thumbnails) && thumbnails %>
32 <% if defined?(thumbnails) && thumbnails %>
29 <% images = attachments.select(&:thumbnailable?) %>
33 <% images = attachments.select(&:thumbnailable?) %>
30 <% if images.any? %>
34 <% if images.any? %>
31 <div class="thumbnails">
35 <div class="thumbnails">
32 <% images.each do |attachment| %>
36 <% images.each do |attachment| %>
33 <div><%= thumbnail_tag(attachment) %></div>
37 <div><%= thumbnail_tag(attachment) %></div>
34 <% end %>
38 <% end %>
35 </div>
39 </div>
36 <% end %>
40 <% end %>
37 <% end %>
41 <% end %>
38 </div>
42 </div>
@@ -1,79 +1,83
1 <%= labelled_fields_for :issue, @issue do |f| %>
1 <%= labelled_fields_for :issue, @issue do |f| %>
2
2
3 <div class="splitcontent">
3 <div class="splitcontent">
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
5 <% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %>
5 <% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %>
6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true},
6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true},
7 :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %></p>
7 :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %></p>
8 <%= hidden_field_tag 'was_default_status', @issue.status_id, :id => nil if @issue.status == @issue.default_status %>
8 <%= hidden_field_tag 'was_default_status', @issue.status_id, :id => nil if @issue.status == @issue.default_status %>
9 <% else %>
9 <% else %>
10 <p><label><%= l(:field_status) %></label> <%= @issue.status %></p>
10 <p><label><%= l(:field_status) %></label> <%= @issue.status %></p>
11 <% end %>
11 <% end %>
12
12
13 <% if @issue.safe_attribute? 'priority_id' %>
13 <% if @issue.safe_attribute? 'priority_id' %>
14 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true} %></p>
14 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true} %></p>
15 <% end %>
15 <% end %>
16
16
17 <% if @issue.safe_attribute? 'assigned_to_id' %>
17 <% if @issue.safe_attribute? 'assigned_to_id' %>
18 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %></p>
18 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %></p>
19 <% end %>
19 <% end %>
20
20
21 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
21 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
22 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %>
22 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %>
23 <%= link_to(image_tag('add.png', :style => 'vertical-align: middle;'),
23 <%= link_to('',
24 new_project_issue_category_path(@issue.project),
24 new_project_issue_category_path(@issue.project),
25 :remote => true,
25 :remote => true,
26 :method => 'get',
26 :method => 'get',
27 :title => l(:label_issue_category_new),
27 :title => l(:label_issue_category_new),
28 :tabindex => 200) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
28 :tabindex => 200,
29 :class => 'icon-only icon-add'
30 ) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
29 <% end %>
31 <% end %>
30
32
31 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
33 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
32 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %>
34 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %>
33 <%= link_to(image_tag('add.png', :style => 'vertical-align: middle;'),
35 <%= link_to('',
34 new_project_version_path(@issue.project),
36 new_project_version_path(@issue.project),
35 :remote => true,
37 :remote => true,
36 :method => 'get',
38 :method => 'get',
37 :title => l(:label_version_new),
39 :title => l(:label_version_new),
38 :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %>
40 :tabindex => 200,
41 :class => 'icon-only icon-add'
42 ) if User.current.allowed_to?(:manage_versions, @issue.project) %>
39 </p>
43 </p>
40 <% end %>
44 <% end %>
41 </div>
45 </div>
42
46
43 <div class="splitcontentright">
47 <div class="splitcontentright">
44 <% if @issue.safe_attribute? 'parent_issue_id' %>
48 <% if @issue.safe_attribute? 'parent_issue_id' %>
45 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
49 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
46 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project, :scope => Setting.cross_project_subtasks)}')" %>
50 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project, :scope => Setting.cross_project_subtasks)}')" %>
47 <% end %>
51 <% end %>
48
52
49 <% if @issue.safe_attribute? 'start_date' %>
53 <% if @issue.safe_attribute? 'start_date' %>
50 <p id="start_date_area">
54 <p id="start_date_area">
51 <%= f.text_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
55 <%= f.text_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
52 <%= calendar_for('issue_start_date') %>
56 <%= calendar_for('issue_start_date') %>
53 </p>
57 </p>
54 <% end %>
58 <% end %>
55
59
56 <% if @issue.safe_attribute? 'due_date' %>
60 <% if @issue.safe_attribute? 'due_date' %>
57 <p id="due_date_area">
61 <p id="due_date_area">
58 <%= f.text_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
62 <%= f.text_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
59 <%= calendar_for('issue_due_date') %>
63 <%= calendar_for('issue_due_date') %>
60 </p>
64 </p>
61 <% end %>
65 <% end %>
62
66
63 <% if @issue.safe_attribute? 'estimated_hours' %>
67 <% if @issue.safe_attribute? 'estimated_hours' %>
64 <p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
68 <p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
65 <% end %>
69 <% end %>
66
70
67 <% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
71 <% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
68 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
72 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
69 <% end %>
73 <% end %>
70 </div>
74 </div>
71 </div>
75 </div>
72
76
73 <% if @issue.safe_attribute? 'custom_field_values' %>
77 <% if @issue.safe_attribute? 'custom_field_values' %>
74 <%= render :partial => 'issues/form_custom_fields' %>
78 <%= render :partial => 'issues/form_custom_fields' %>
75 <% end %>
79 <% end %>
76
80
77 <% end %>
81 <% end %>
78
82
79 <% include_calendar_headers_tags %>
83 <% include_calendar_headers_tags %>
@@ -1,41 +1,43
1 <div class="contextual">
1 <div class="contextual">
2 <% if User.current.allowed_to?(:manage_issue_relations, @project) %>
2 <% if User.current.allowed_to?(:manage_issue_relations, @project) %>
3 <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
3 <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
4 <% end %>
4 <% end %>
5 </div>
5 </div>
6
6
7 <p><strong><%=l(:label_related_issues)%></strong></p>
7 <p><strong><%=l(:label_related_issues)%></strong></p>
8
8
9 <% if @relations.present? %>
9 <% if @relations.present? %>
10 <form>
10 <form>
11 <table class="list issues">
11 <table class="list issues">
12 <% @relations.each do |relation| %>
12 <% @relations.each do |relation| %>
13 <% other_issue = relation.other_issue(@issue) -%>
13 <% other_issue = relation.other_issue(@issue) -%>
14 <tr class="issue hascontextmenu <%= other_issue.css_classes %>" id="relation-<%= relation.id %>">
14 <tr class="issue hascontextmenu <%= other_issue.css_classes %>" id="relation-<%= relation.id %>">
15 <td class="checkbox"><%= check_box_tag("ids[]", other_issue.id, false, :id => nil) %></td>
15 <td class="checkbox"><%= check_box_tag("ids[]", other_issue.id, false, :id => nil) %></td>
16 <td class="subject" style="width: 50%">
16 <td class="subject" style="width: 50%">
17 <%= relation.to_s(@issue) {|other| link_to_issue(other, :project => Setting.cross_project_issue_relations?)}.html_safe %>
17 <%= relation.to_s(@issue) {|other| link_to_issue(other, :project => Setting.cross_project_issue_relations?)}.html_safe %>
18 </td>
18 </td>
19 <td class="status"><%= other_issue.status.name %></td>
19 <td class="status"><%= other_issue.status.name %></td>
20 <td class="start_date"><%= format_date(other_issue.start_date) %></td>
20 <td class="start_date"><%= format_date(other_issue.start_date) %></td>
21 <td class="due_date"><%= format_date(other_issue.due_date) %></td>
21 <td class="due_date"><%= format_date(other_issue.due_date) %></td>
22 <td class="buttons"><%= link_to image_tag('link_break.png'),
22 <td class="buttons"><%= link_to('',
23 relation_path(relation),
23 relation_path(relation),
24 :remote => true,
24 :remote => true,
25 :method => :delete,
25 :method => :delete,
26 :data => {:confirm => l(:text_are_you_sure)},
26 :data => {:confirm => l(:text_are_you_sure)},
27 :title => l(:label_relation_delete) if User.current.allowed_to?(:manage_issue_relations, @project) %></td>
27 :title => l(:label_relation_delete),
28 :class => 'icon-only icon-link-break'
29 ) if User.current.allowed_to?(:manage_issue_relations, @project) %></td>
28 </tr>
30 </tr>
29 <% end %>
31 <% end %>
30 </table>
32 </table>
31 </form>
33 </form>
32 <% end %>
34 <% end %>
33
35
34 <%= form_for @relation, {
36 <%= form_for @relation, {
35 :as => :relation, :remote => true,
37 :as => :relation, :remote => true,
36 :url => issue_relations_path(@issue),
38 :url => issue_relations_path(@issue),
37 :method => :post,
39 :method => :post,
38 :html => {:id => 'new-relation-form', :style => 'display: none;'}
40 :html => {:id => 'new-relation-form', :style => 'display: none;'}
39 } do |f| %>
41 } do |f| %>
40 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
42 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
41 <% end %>
43 <% end %>
@@ -1,85 +1,89
1 <%= board_breadcrumb(@message) %>
1 <%= board_breadcrumb(@message) %>
2
2
3 <div class="contextual">
3 <div class="contextual">
4 <%= watcher_link(@topic, User.current) %>
4 <%= watcher_link(@topic, User.current) %>
5 <%= link_to(
5 <%= link_to(
6 l(:button_quote),
6 l(:button_quote),
7 {:action => 'quote', :id => @topic},
7 {:action => 'quote', :id => @topic},
8 :method => 'get',
8 :method => 'get',
9 :class => 'icon icon-comment',
9 :class => 'icon icon-comment',
10 :remote => true) if !@topic.locked? && authorize_for('messages', 'reply') %>
10 :remote => true) if !@topic.locked? && authorize_for('messages', 'reply') %>
11 <%= link_to(
11 <%= link_to(
12 l(:button_edit),
12 l(:button_edit),
13 {:action => 'edit', :id => @topic},
13 {:action => 'edit', :id => @topic},
14 :class => 'icon icon-edit'
14 :class => 'icon icon-edit'
15 ) if @message.editable_by?(User.current) %>
15 ) if @message.editable_by?(User.current) %>
16 <%= link_to(
16 <%= link_to(
17 l(:button_delete),
17 l(:button_delete),
18 {:action => 'destroy', :id => @topic},
18 {:action => 'destroy', :id => @topic},
19 :method => :post,
19 :method => :post,
20 :data => {:confirm => l(:text_are_you_sure)},
20 :data => {:confirm => l(:text_are_you_sure)},
21 :class => 'icon icon-del'
21 :class => 'icon icon-del'
22 ) if @message.destroyable_by?(User.current) %>
22 ) if @message.destroyable_by?(User.current) %>
23 </div>
23 </div>
24
24
25 <h2><%= avatar(@topic.author, :size => "24") %><%= @topic.subject %></h2>
25 <h2><%= avatar(@topic.author, :size => "24") %><%= @topic.subject %></h2>
26
26
27 <div class="message">
27 <div class="message">
28 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
28 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
29 <div class="wiki">
29 <div class="wiki">
30 <%= textilizable(@topic, :content) %>
30 <%= textilizable(@topic, :content) %>
31 </div>
31 </div>
32 <%= link_to_attachments @topic, :author => false %>
32 <%= link_to_attachments @topic, :author => false %>
33 </div>
33 </div>
34 <br />
34 <br />
35
35
36 <% unless @replies.empty? %>
36 <% unless @replies.empty? %>
37 <h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
37 <h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
38 <% @replies.each do |message| %>
38 <% @replies.each do |message| %>
39 <div class="message reply" id="<%= "message-#{message.id}" %>">
39 <div class="message reply" id="<%= "message-#{message.id}" %>">
40 <div class="contextual">
40 <div class="contextual">
41 <%= link_to(
41 <%= link_to(
42 image_tag('comment.png'),
42 '',
43 {:action => 'quote', :id => message},
43 {:action => 'quote', :id => message},
44 :remote => true,
44 :remote => true,
45 :method => 'get',
45 :method => 'get',
46 :title => l(:button_quote)) if !@topic.locked? && authorize_for('messages', 'reply') %>
46 :title => l(:button_quote),
47 :class => 'icon icon-comment'
48 ) if !@topic.locked? && authorize_for('messages', 'reply') %>
47 <%= link_to(
49 <%= link_to(
48 image_tag('edit.png'),
50 '',
49 {:action => 'edit', :id => message},
51 {:action => 'edit', :id => message},
50 :title => l(:button_edit)
52 :title => l(:button_edit),
53 :class => 'icon icon-edit'
51 ) if message.editable_by?(User.current) %>
54 ) if message.editable_by?(User.current) %>
52 <%= link_to(
55 <%= link_to(
53 image_tag('delete.png'),
56 '',
54 {:action => 'destroy', :id => message},
57 {:action => 'destroy', :id => message},
55 :method => :post,
58 :method => :post,
56 :data => {:confirm => l(:text_are_you_sure)},
59 :data => {:confirm => l(:text_are_you_sure)},
57 :title => l(:button_delete)
60 :title => l(:button_delete),
61 :class => 'icon icon-del'
58 ) if message.destroyable_by?(User.current) %>
62 ) if message.destroyable_by?(User.current) %>
59 </div>
63 </div>
60 <h4>
64 <h4>
61 <%= avatar(message.author, :size => "24") %>
65 <%= avatar(message.author, :size => "24") %>
62 <%= link_to message.subject, { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
66 <%= link_to message.subject, { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
63 -
67 -
64 <%= authoring message.created_on, message.author %>
68 <%= authoring message.created_on, message.author %>
65 </h4>
69 </h4>
66 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
70 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
67 <%= link_to_attachments message, :author => false %>
71 <%= link_to_attachments message, :author => false %>
68 </div>
72 </div>
69 <% end %>
73 <% end %>
70 <span class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></span>
74 <span class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></span>
71 <% end %>
75 <% end %>
72
76
73 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
77 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
74 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
78 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
75 <div id="reply" style="display:none;">
79 <div id="reply" style="display:none;">
76 <%= form_for @reply, :as => :reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
80 <%= form_for @reply, :as => :reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
77 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
81 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
78 <%= submit_tag l(:button_submit) %>
82 <%= submit_tag l(:button_submit) %>
79 <%= preview_link({:controller => 'messages', :action => 'preview', :board_id => @board}, 'message-form') %>
83 <%= preview_link({:controller => 'messages', :action => 'preview', :board_id => @board}, 'message-form') %>
80 <% end %>
84 <% end %>
81 <div id="preview" class="wiki"></div>
85 <div id="preview" class="wiki"></div>
82 </div>
86 </div>
83 <% end %>
87 <% end %>
84
88
85 <% html_title @topic.subject %>
89 <% html_title @topic.subject %>
@@ -1,58 +1,59
1 <h3>
1 <h3>
2 <%= link_to l(:label_spent_time), time_entries_path(:user_id => 'me') %>
2 <%= link_to l(:label_spent_time), time_entries_path(:user_id => 'me') %>
3 (<%= l(:label_last_n_days, 7) %>)
3 (<%= l(:label_last_n_days, 7) %>)
4 </h3>
4 </h3>
5 <%
5 <%
6 entries = timelog_items
6 entries = timelog_items
7 entries_by_day = entries.group_by(&:spent_on)
7 entries_by_day = entries.group_by(&:spent_on)
8 %>
8 %>
9
9
10 <% if User.current.allowed_to?(:log_time, nil, :global => true) %>
10 <% if User.current.allowed_to?(:log_time, nil, :global => true) %>
11 <div class="contextual">
11 <div class="contextual">
12 <%= link_to l(:button_log_time), new_time_entry_path, :class => "icon icon-add" %>
12 <%= link_to l(:button_log_time), new_time_entry_path, :class => "icon icon-add" %>
13 </div>
13 </div>
14 <% end %>
14 <% end %>
15
15
16 <div class="total-hours">
16 <div class="total-hours">
17 <p><%= l(:label_total_time) %>: <%= html_hours("%.2f" % entries.sum(&:hours).to_f) %></p>
17 <p><%= l(:label_total_time) %>: <%= html_hours("%.2f" % entries.sum(&:hours).to_f) %></p>
18 </div>
18 </div>
19
19
20 <% if entries.any? %>
20 <% if entries.any? %>
21 <table class="list time-entries">
21 <table class="list time-entries">
22 <thead><tr>
22 <thead><tr>
23 <th><%= l(:label_activity) %></th>
23 <th><%= l(:label_activity) %></th>
24 <th><%= l(:label_project) %></th>
24 <th><%= l(:label_project) %></th>
25 <th><%= l(:field_comments) %></th>
25 <th><%= l(:field_comments) %></th>
26 <th><%= l(:field_hours) %></th>
26 <th><%= l(:field_hours) %></th>
27 <th></th>
27 <th></th>
28 </tr></thead>
28 </tr></thead>
29 <tbody>
29 <tbody>
30 <% entries_by_day.keys.sort.reverse.each do |day| %>
30 <% entries_by_day.keys.sort.reverse.each do |day| %>
31 <tr class="odd">
31 <tr class="odd">
32 <td><strong><%= day == Date.today ? l(:label_today).titleize : format_date(day) %></strong></td>
32 <td><strong><%= day == Date.today ? l(:label_today).titleize : format_date(day) %></strong></td>
33 <td colspan="2"></td>
33 <td colspan="2"></td>
34 <td class="hours"><em><%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %></em></td>
34 <td class="hours"><em><%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %></em></td>
35 <td></td>
35 <td></td>
36 </tr>
36 </tr>
37 <% entries_by_day[day].each do |entry| -%>
37 <% entries_by_day[day].each do |entry| -%>
38 <tr class="time-entry" style="border-bottom: 1px solid #f5f5f5;">
38 <tr class="time-entry" style="border-bottom: 1px solid #f5f5f5;">
39 <td class="activity"><%= entry.activity %></td>
39 <td class="activity"><%= entry.activity %></td>
40 <td class="subject"><%= entry.project %> <%= h(' - ') + link_to_issue(entry.issue, :truncate => 50) if entry.issue %></td>
40 <td class="subject"><%= entry.project %> <%= h(' - ') + link_to_issue(entry.issue, :truncate => 50) if entry.issue %></td>
41 <td class="comments"><%= entry.comments %></td>
41 <td class="comments"><%= entry.comments %></td>
42 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
42 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
43 <td class="buttons">
43 <td class="buttons">
44 <% if entry.editable_by?(@user) -%>
44 <% if entry.editable_by?(@user) -%>
45 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry},
45 <%= link_to '', {:controller => 'timelog', :action => 'edit', :id => entry},
46 :title => l(:button_edit) %>
46 :title => l(:button_edit),
47 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry},
47 :class => 'icon-only icon-edit' %>
48 :data => {:confirm => l(:text_are_you_sure)},
48 <%= link_to '', {:controller => 'timelog', :action => 'destroy', :id => entry},
49 :method => :delete,
49 :data => {:confirm => l(:text_are_you_sure)}, :method => :delete,
50 :title => l(:button_delete) %>
50 :title => l(:button_delete),
51 :class => 'icon-only icon-del' %>
51 <% end -%>
52 <% end -%>
52 </td>
53 </td>
53 </tr>
54 </tr>
54 <% end -%>
55 <% end -%>
55 <% end -%>
56 <% end -%>
56 </tbody>
57 </tbody>
57 </table>
58 </table>
58 <% end %>
59 <% end %>
@@ -1,62 +1,64
1 <div class="contextual">
1 <div class="contextual">
2 <%= watcher_link(@news, User.current) %>
2 <%= watcher_link(@news, User.current) %>
3 <%= link_to(l(:button_edit),
3 <%= link_to(l(:button_edit),
4 edit_news_path(@news),
4 edit_news_path(@news),
5 :class => 'icon icon-edit',
5 :class => 'icon icon-edit',
6 :accesskey => accesskey(:edit),
6 :accesskey => accesskey(:edit),
7 :onclick => '$("#edit-news").show(); return false;') if User.current.allowed_to?(:manage_news, @project) %>
7 :onclick => '$("#edit-news").show(); return false;') if User.current.allowed_to?(:manage_news, @project) %>
8 <%= delete_link news_path(@news) if User.current.allowed_to?(:manage_news, @project) %>
8 <%= delete_link news_path(@news) if User.current.allowed_to?(:manage_news, @project) %>
9 </div>
9 </div>
10
10
11 <h2><%= avatar(@news.author, :size => "24") %><%=h @news.title %></h2>
11 <h2><%= avatar(@news.author, :size => "24") %><%=h @news.title %></h2>
12
12
13 <% if authorize_for('news', 'edit') %>
13 <% if authorize_for('news', 'edit') %>
14 <div id="edit-news" style="display:none;">
14 <div id="edit-news" style="display:none;">
15 <%= labelled_form_for :news, @news, :url => news_path(@news),
15 <%= labelled_form_for :news, @news, :url => news_path(@news),
16 :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
16 :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
17 <%= render :partial => 'form', :locals => { :f => f } %>
17 <%= render :partial => 'form', :locals => { :f => f } %>
18 <%= submit_tag l(:button_save) %>
18 <%= submit_tag l(:button_save) %>
19 <%= preview_link preview_news_path(:project_id => @project, :id => @news), 'news-form' %> |
19 <%= preview_link preview_news_path(:project_id => @project, :id => @news), 'news-form' %> |
20 <%= link_to l(:button_cancel), "#", :onclick => '$("#edit-news").hide(); return false;' %>
20 <%= link_to l(:button_cancel), "#", :onclick => '$("#edit-news").hide(); return false;' %>
21 <% end %>
21 <% end %>
22 <div id="preview" class="wiki"></div>
22 <div id="preview" class="wiki"></div>
23 </div>
23 </div>
24 <% end %>
24 <% end %>
25
25
26 <p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %>
26 <p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %>
27 <span class="author"><%= authoring @news.created_on, @news.author %></span></p>
27 <span class="author"><%= authoring @news.created_on, @news.author %></span></p>
28 <div class="wiki">
28 <div class="wiki">
29 <%= textilizable(@news, :description) %>
29 <%= textilizable(@news, :description) %>
30 </div>
30 </div>
31 <%= link_to_attachments @news %>
31 <%= link_to_attachments @news %>
32 <br />
32 <br />
33
33
34 <div id="comments" style="margin-bottom:16px;">
34 <div id="comments" style="margin-bottom:16px;">
35 <h3 class="comments"><%= l(:label_comment_plural) %></h3>
35 <h3 class="comments"><%= l(:label_comment_plural) %></h3>
36 <% @comments.each do |comment| %>
36 <% @comments.each do |comment| %>
37 <% next if comment.new_record? %>
37 <% next if comment.new_record? %>
38 <div class="contextual">
38 <div class="contextual">
39 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
39 <%= link_to_if_authorized '', {:controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
40 :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :title => l(:button_delete) %>
40 :data => {:confirm => l(:text_are_you_sure)}, :method => :delete,
41 :title => l(:button_delete),
42 :class => 'icon-only icon-del' %>
41 </div>
43 </div>
42 <h4><%= avatar(comment.author, :size => "24") %><%= authoring comment.created_on, comment.author %></h4>
44 <h4><%= avatar(comment.author, :size => "24") %><%= authoring comment.created_on, comment.author %></h4>
43 <%= textilizable(comment.comments) %>
45 <%= textilizable(comment.comments) %>
44 <% end if @comments.any? %>
46 <% end if @comments.any? %>
45 </div>
47 </div>
46
48
47 <% if @news.commentable? %>
49 <% if @news.commentable? %>
48 <p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p>
50 <p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p>
49 <%= form_tag({:controller => 'comments', :action => 'create', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %>
51 <%= form_tag({:controller => 'comments', :action => 'create', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %>
50 <div class="box">
52 <div class="box">
51 <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
53 <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
52 <%= wikitoolbar_for 'comment_comments' %>
54 <%= wikitoolbar_for 'comment_comments' %>
53 </div>
55 </div>
54 <p><%= submit_tag l(:button_add) %></p>
56 <p><%= submit_tag l(:button_add) %></p>
55 <% end %>
57 <% end %>
56 <% end %>
58 <% end %>
57
59
58 <% html_title @news.title -%>
60 <% html_title @news.title -%>
59
61
60 <% content_for :header_tags do %>
62 <% content_for :header_tags do %>
61 <%= stylesheet_link_tag 'scm' %>
63 <%= stylesheet_link_tag 'scm' %>
62 <% end %>
64 <% end %>
@@ -1,33 +1,54
1 <h2><%=l(:label_report_plural)%></h2>
1 <h2><%=l(:label_report_plural)%></h2>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <h3><%=l(:field_tracker)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), project_issues_report_details_path(@project, :detail => 'tracker') %></h3>
4 <h3>
5 <%=l(:field_tracker)%>&nbsp;
6 <%= link_to '', project_issues_report_details_path(@project, :detail => 'tracker'), :class => 'icon-only icon-zoom-in' %>
7 </h3>
5 <%= render :partial => 'simple', :locals => { :data => @issues_by_tracker, :field_name => "tracker_id", :rows => @trackers } %>
8 <%= render :partial => 'simple', :locals => { :data => @issues_by_tracker, :field_name => "tracker_id", :rows => @trackers } %>
6 <br />
9 <br />
7 <h3><%=l(:field_priority)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), project_issues_report_details_path(@project, :detail => 'priority') %></h3>
10 <h3>
11 <%=l(:field_priority)%>&nbsp;
12 <%= link_to '', project_issues_report_details_path(@project, :detail => 'priority'), :class => 'icon-only icon-zoom-in' %>
13 </h3>
8 <%= render :partial => 'simple', :locals => { :data => @issues_by_priority, :field_name => "priority_id", :rows => @priorities } %>
14 <%= render :partial => 'simple', :locals => { :data => @issues_by_priority, :field_name => "priority_id", :rows => @priorities } %>
9 <br />
15 <br />
10 <h3><%=l(:field_assigned_to)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), project_issues_report_details_path(@project, :detail => 'assigned_to') %></h3>
16 <h3>
17 <%=l(:field_assigned_to)%>&nbsp;
18 <%= link_to '', project_issues_report_details_path(@project, :detail => 'assigned_to'), :class => 'icon-only icon-zoom-in' %>
19 </h3>
11 <%= render :partial => 'simple', :locals => { :data => @issues_by_assigned_to, :field_name => "assigned_to_id", :rows => @assignees } %>
20 <%= render :partial => 'simple', :locals => { :data => @issues_by_assigned_to, :field_name => "assigned_to_id", :rows => @assignees } %>
12 <br />
21 <br />
13 <h3><%=l(:field_author)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), project_issues_report_details_path(@project, :detail => 'author') %></h3>
22 <h3>
23 <%=l(:field_author)%>&nbsp;
24 <%= link_to '', project_issues_report_details_path(@project, :detail => 'author'), :class => 'icon-only icon-zoom-in' %>
25 </h3>
14 <%= render :partial => 'simple', :locals => { :data => @issues_by_author, :field_name => "author_id", :rows => @authors } %>
26 <%= render :partial => 'simple', :locals => { :data => @issues_by_author, :field_name => "author_id", :rows => @authors } %>
15 <br />
27 <br />
16 <%= call_hook(:view_reports_issue_report_split_content_left, :project => @project) %>
28 <%= call_hook(:view_reports_issue_report_split_content_left, :project => @project) %>
17 </div>
29 </div>
18
30
19 <div class="splitcontentright">
31 <div class="splitcontentright">
20 <h3><%=l(:field_version)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), project_issues_report_details_path(@project, :detail => 'version') %></h3>
32 <h3>
33 <%=l(:field_version)%>&nbsp;
34 <%= link_to '', project_issues_report_details_path(@project, :detail => 'version'), :class => 'icon-only icon-zoom-in' %>
35 </h3>
21 <%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "fixed_version_id", :rows => @versions } %>
36 <%= render :partial => 'simple', :locals => { :data => @issues_by_version, :field_name => "fixed_version_id", :rows => @versions } %>
22 <br />
37 <br />
23 <% if @project.children.any? %>
38 <% if @project.children.any? %>
24 <h3><%=l(:field_subproject)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), project_issues_report_details_path(@project, :detail => 'subproject') %></h3>
39 <h3>
40 <%=l(:field_subproject)%>&nbsp;
41 <%= link_to '', project_issues_report_details_path(@project, :detail => 'subproject'), :class => 'icon-only icon-zoom-in' %>
42 </h3>
25 <%= render :partial => 'simple', :locals => { :data => @issues_by_subproject, :field_name => "project_id", :rows => @subprojects } %>
43 <%= render :partial => 'simple', :locals => { :data => @issues_by_subproject, :field_name => "project_id", :rows => @subprojects } %>
26 <br />
44 <br />
27 <% end %>
45 <% end %>
28 <h3><%=l(:field_category)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), project_issues_report_details_path(@project, :detail => 'category') %></h3>
46 <h3>
47 <%=l(:field_category)%>&nbsp;
48 <%= link_to '', project_issues_report_details_path(@project, :detail => 'category'), :class => 'icon-only icon-zoom-in' %>
49 </h3>
29 <%= render :partial => 'simple', :locals => { :data => @issues_by_category, :field_name => "category_id", :rows => @categories } %>
50 <%= render :partial => 'simple', :locals => { :data => @issues_by_category, :field_name => "category_id", :rows => @categories } %>
30 <br />
51 <br />
31 <%= call_hook(:view_reports_issue_report_split_content_right, :project => @project) %>
52 <%= call_hook(:view_reports_issue_report_split_content_right, :project => @project) %>
32 </div>
53 </div>
33
54
@@ -1,55 +1,59
1 <%= title [l(:label_role_plural), roles_path], l(:label_permissions_report) %>
1 <%= title [l(:label_role_plural), roles_path], l(:label_permissions_report) %>
2
2
3 <%= form_tag(permissions_roles_path, :id => 'permissions_form') do %>
3 <%= form_tag(permissions_roles_path, :id => 'permissions_form') do %>
4 <%= hidden_field_tag 'permissions[0]', '', :id => nil %>
4 <%= hidden_field_tag 'permissions[0]', '', :id => nil %>
5 <div class="autoscroll">
5 <div class="autoscroll">
6 <table class="list permissions">
6 <table class="list permissions">
7 <thead>
7 <thead>
8 <tr>
8 <tr>
9 <th><%=l(:label_permissions)%></th>
9 <th><%=l(:label_permissions)%></th>
10 <% @roles.each do |role| %>
10 <% @roles.each do |role| %>
11 <th>
11 <th>
12 <%= link_to_function('',
13 "toggleCheckboxesBySelector('input.role-#{role.id}')",
14 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
15 :class => 'icon-only icon-checked') %>
12 <%= content_tag(role.builtin? ? 'em' : 'span', role.name) %>
16 <%= content_tag(role.builtin? ? 'em' : 'span', role.name) %>
13 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')",
14 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
15 </th>
17 </th>
16 <% end %>
18 <% end %>
17 </tr>
19 </tr>
18 </thead>
20 </thead>
19 <tbody>
21 <tbody>
20 <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
22 <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
21 <% perms_by_module.keys.sort.each do |mod| %>
23 <% perms_by_module.keys.sort.each do |mod| %>
22 <% unless mod.blank? %>
24 <% unless mod.blank? %>
23 <tr class="group open">
25 <tr class="group open">
24 <td>
26 <td>
25 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
27 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
26 <%= l_or_humanize(mod, :prefix => 'project_module_') %>
28 <%= l_or_humanize(mod, :prefix => 'project_module_') %>
27 </td>
29 </td>
28 <% @roles.each do |role| %>
30 <% @roles.each do |role| %>
29 <td class="role"><%= role.name %></td>
31 <td class="role"><%= role.name %></td>
30 <% end %>
32 <% end %>
31 </tr>
33 </tr>
32 <% end %>
34 <% end %>
33 <% perms_by_module[mod].each do |permission| %>
35 <% perms_by_module[mod].each do |permission| %>
34 <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>">
36 <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>">
35 <td class="name">
37 <td class="name">
36 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')",
38 <%= link_to_function('',
37 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
39 "toggleCheckboxesBySelector('.permission-#{permission.name} input')",
40 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
41 :class => 'icon-only icon-checked') %>
38 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
42 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
39 </td>
43 </td>
40 <% @roles.each do |role| %>
44 <% @roles.each do |role| %>
41 <td>
45 <td>
42 <% if role.setable_permissions.include? permission %>
46 <% if role.setable_permissions.include? permission %>
43 <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %>
47 <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %>
44 <% end %>
48 <% end %>
45 </td>
49 </td>
46 <% end %>
50 <% end %>
47 </tr>
51 </tr>
48 <% end %>
52 <% end %>
49 <% end %>
53 <% end %>
50 </tbody>
54 </tbody>
51 </table>
55 </table>
52 </div>
56 </div>
53 <p><%= check_all_links 'permissions_form' %></p>
57 <p><%= check_all_links 'permissions_form' %></p>
54 <p><%= submit_tag l(:button_save) %></p>
58 <p><%= submit_tag l(:button_save) %></p>
55 <% end %>
59 <% end %>
@@ -1,158 +1,157
1 <%= form_tag({:action => 'edit', :tab => 'repositories'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'repositories'}) do %>
2
2
3 <fieldset class="box settings enabled_scm">
3 <fieldset class="box settings enabled_scm">
4 <legend><%= l(:setting_enabled_scm) %></legend>
4 <legend><%= l(:setting_enabled_scm) %></legend>
5 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
5 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
6 <table>
6 <table>
7 <tr>
7 <tr>
8 <th></th>
8 <th></th>
9 <th><%= l(:text_scm_command) %></th>
9 <th><%= l(:text_scm_command) %></th>
10 <th><%= l(:text_scm_command_version) %></th>
10 <th><%= l(:text_scm_command_version) %></th>
11 </tr>
11 </tr>
12 <% Redmine::Scm::Base.all.collect do |choice| %>
12 <% Redmine::Scm::Base.all.collect do |choice| %>
13 <% scm_class = "Repository::#{choice}".constantize %>
13 <% scm_class = "Repository::#{choice}".constantize %>
14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
15 <% setting = :enabled_scm %>
15 <% setting = :enabled_scm %>
16 <% enabled = Setting.send(setting).include?(value) %>
16 <% enabled = Setting.send(setting).include?(value) %>
17 <tr>
17 <tr>
18 <td class="scm_name">
18 <td class="scm_name">
19 <label>
19 <label>
20 <%= check_box_tag("settings[#{setting}][]", value, enabled, :id => nil) %>
20 <%= check_box_tag("settings[#{setting}][]", value, enabled, :id => nil) %>
21 <%= text.to_s %>
21 <%= text.to_s %>
22 </label>
22 </label>
23 </td>
23 </td>
24 <td>
24 <td>
25 <% if enabled %>
25 <% if enabled %>
26 <%=
26 <span class="icon <%= (scm_class.scm_available ? 'icon-ok' : 'icon-error') %>"></span>
27 image_tag(
28 (scm_class.scm_available ? 'true.png' : 'exclamation.png'),
29 :style => "vertical-align:bottom;"
30 )
31 %>
32 <%= scm_class.scm_command %>
27 <%= scm_class.scm_command %>
33 <% end %>
28 <% end %>
34 </td>
29 </td>
35 <td>
30 <td>
36 <%= scm_class.scm_version_string if enabled %>
31 <%= scm_class.scm_version_string if enabled %>
37 </td>
32 </td>
38 </tr>
33 </tr>
39 <% end %>
34 <% end %>
40 </table>
35 </table>
41 <p><em class="info"><%= l(:text_scm_config) %></em></p>
36 <p><em class="info"><%= l(:text_scm_config) %></em></p>
42 </fieldset>
37 </fieldset>
43
38
44 <div class="box tabular settings">
39 <div class="box tabular settings">
45 <p><%= setting_check_box :autofetch_changesets %></p>
40 <p><%= setting_check_box :autofetch_changesets %></p>
46
41
47 <p><%= setting_check_box :sys_api_enabled,
42 <p><%= setting_check_box :sys_api_enabled,
48 :onclick =>
43 :onclick =>
49 "if (this.checked) { $('#settings_sys_api_key').removeAttr('disabled'); } else { $('#settings_sys_api_key').attr('disabled', true); }" %></p>
44 "if (this.checked) { $('#settings_sys_api_key').removeAttr('disabled'); } else { $('#settings_sys_api_key').attr('disabled', true); }" %></p>
50
45
51 <p><%= setting_text_field :sys_api_key,
46 <p><%= setting_text_field :sys_api_key,
52 :size => 30,
47 :size => 30,
53 :id => 'settings_sys_api_key',
48 :id => 'settings_sys_api_key',
54 :disabled => !Setting.sys_api_enabled?,
49 :disabled => !Setting.sys_api_enabled?,
55 :label => :setting_mail_handler_api_key %>
50 :label => :setting_mail_handler_api_key %>
56 <%= link_to_function l(:label_generate_key),
51 <%= link_to_function l(:label_generate_key),
57 "if (!$('#settings_sys_api_key').attr('disabled')) { $('#settings_sys_api_key').val(randomKey(20)) }" %>
52 "if (!$('#settings_sys_api_key').attr('disabled')) { $('#settings_sys_api_key').val(randomKey(20)) }" %>
58 </p>
53 </p>
59
54
60 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
55 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
61 </div>
56 </div>
62
57
63 <fieldset class="box tabular settings">
58 <fieldset class="box tabular settings">
64 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
59 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
65 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
60 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
66 <em class="info"><%= l(:text_comma_separated) %></em></p>
61 <em class="info"><%= l(:text_comma_separated) %></em></p>
67
62
68 <p><%= setting_check_box :commit_cross_project_ref %></p>
63 <p><%= setting_check_box :commit_cross_project_ref %></p>
69
64
70 <p><%= setting_check_box :commit_logtime_enabled,
65 <p><%= setting_check_box :commit_logtime_enabled,
71 :onclick =>
66 :onclick =>
72 "if (this.checked) { $('#settings_commit_logtime_activity_id').removeAttr('disabled'); } else { $('#settings_commit_logtime_activity_id').attr('disabled', true); }"%></p>
67 "if (this.checked) { $('#settings_commit_logtime_activity_id').removeAttr('disabled'); } else { $('#settings_commit_logtime_activity_id').attr('disabled', true); }"%></p>
73
68
74 <p><%= setting_select :commit_logtime_activity_id,
69 <p><%= setting_select :commit_logtime_activity_id,
75 [[l(:label_default), 0]] +
70 [[l(:label_default), 0]] +
76 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
71 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
77 :disabled => !Setting.commit_logtime_enabled?%></p>
72 :disabled => !Setting.commit_logtime_enabled?%></p>
78 </fieldset>
73 </fieldset>
79
74
80 <table class="list" id="commit-keywords">
75 <table class="list" id="commit-keywords">
81 <thead>
76 <thead>
82 <tr>
77 <tr>
83 <th><%= l(:label_tracker) %></th>
78 <th><%= l(:label_tracker) %></th>
84 <th><%= l(:setting_commit_fix_keywords) %></th>
79 <th><%= l(:setting_commit_fix_keywords) %></th>
85 <th><%= l(:label_applied_status) %></th>
80 <th><%= l(:label_applied_status) %></th>
86 <th><%= l(:field_done_ratio) %></th>
81 <th><%= l(:field_done_ratio) %></th>
87 <th class="buttons"></th>
82 <th class="buttons"></th>
88 </tr>
83 </tr>
89 </thead>
84 </thead>
90 <tbody>
85 <tbody>
91 <% @commit_update_keywords.each do |rule| %>
86 <% @commit_update_keywords.each do |rule| %>
92 <tr class="commit-keywords">
87 <tr class="commit-keywords">
93 <td>
88 <td>
94 <%= select_tag(
89 <%= select_tag(
95 "settings[commit_update_keywords][if_tracker_id][]",
90 "settings[commit_update_keywords][if_tracker_id][]",
96 options_for_select(
91 options_for_select(
97 [[l(:label_all), ""]] +
92 [[l(:label_all), ""]] +
98 Tracker.sorted.map {|t| [t.name, t.id.to_s]},
93 Tracker.sorted.map {|t| [t.name, t.id.to_s]},
99 rule['if_tracker_id']),
94 rule['if_tracker_id']),
100 :id => nil
95 :id => nil
101 ) %>
96 ) %>
102 </td>
97 </td>
103 <td>
98 <td>
104 <%= text_field_tag("settings[commit_update_keywords][keywords][]",
99 <%= text_field_tag("settings[commit_update_keywords][keywords][]",
105 rule['keywords'], :id => nil, :size => 30) %>
100 rule['keywords'], :id => nil, :size => 30) %>
106 </td>
101 </td>
107 <td>
102 <td>
108 <%= select_tag("settings[commit_update_keywords][status_id][]",
103 <%= select_tag("settings[commit_update_keywords][status_id][]",
109 options_for_select(
104 options_for_select(
110 [["", 0]] +
105 [["", 0]] +
111 IssueStatus.sorted.
106 IssueStatus.sorted.
112 collect{|status| [status.name, status.id.to_s]},
107 collect{|status| [status.name, status.id.to_s]},
113 rule['status_id']),
108 rule['status_id']),
114 :id => nil
109 :id => nil
115 ) %>
110 ) %>
116 </td>
111 </td>
117 <td>
112 <td>
118 <%= select_tag("settings[commit_update_keywords][done_ratio][]",
113 <%= select_tag("settings[commit_update_keywords][done_ratio][]",
119 options_for_select(
114 options_for_select(
120 [["", ""]] +
115 [["", ""]] +
121 (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
116 (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
122 rule['done_ratio']),
117 rule['done_ratio']),
123 :id => nil
118 :id => nil
124 ) %>
119 ) %>
125 </td>
120 </td>
126 <td class="buttons">
121 <td class="buttons">
127 <%= link_to(image_tag('delete.png'), '#', :class => 'delete-commit-keywords') %>
122 <%= link_to('', '#',
123 :class => 'delete-commit-keywords icon-only icon-del') %>
128 </td>
124 </td>
129 </tr>
125 </tr>
130 <% end %>
126 <% end %>
131 <tr>
127 <tr>
132 <td></td>
128 <td></td>
133 <td><em class="info"><%= l(:text_comma_separated) %></em></td>
129 <td><em class="info"><%= l(:text_comma_separated) %></em></td>
134 <td></td>
130 <td></td>
135 <td></td>
131 <td></td>
136 <td class="buttons"><%= link_to(image_tag('add.png'), '#', :class => 'add-commit-keywords') %></td>
132 <td class="buttons">
133 <%= link_to('', '#',
134 :class => 'add-commit-keywords icon-only icon-add') %>
135 </td>
137 </tr>
136 </tr>
138 </tbody>
137 </tbody>
139 </table>
138 </table>
140
139
141 <p><%= submit_tag l(:button_save) %></p>
140 <p><%= submit_tag l(:button_save) %></p>
142 <% end %>
141 <% end %>
143
142
144 <%= javascript_tag do %>
143 <%= javascript_tag do %>
145 $('#commit-keywords').on('click', 'a.delete-commit-keywords', function(e){
144 $('#commit-keywords').on('click', 'a.delete-commit-keywords', function(e){
146 e.preventDefault();
145 e.preventDefault();
147 if ($('#commit-keywords tbody tr.commit-keywords').length > 1) {
146 if ($('#commit-keywords tbody tr.commit-keywords').length > 1) {
148 $(this).parents('#commit-keywords tr').remove();
147 $(this).parents('#commit-keywords tr').remove();
149 } else {
148 } else {
150 $('#commit-keywords tbody tr.commit-keywords').find('input, select').val('');
149 $('#commit-keywords tbody tr.commit-keywords').find('input, select').val('');
151 }
150 }
152 });
151 });
153 $('#commit-keywords').on('click', 'a.add-commit-keywords', function(e){
152 $('#commit-keywords').on('click', 'a.add-commit-keywords', function(e){
154 e.preventDefault();
153 e.preventDefault();
155 var row = $('#commit-keywords tr.commit-keywords:last');
154 var row = $('#commit-keywords tr.commit-keywords:last');
156 row.clone().insertAfter(row).find('input, select').val('');
155 row.clone().insertAfter(row).find('input, select').val('');
157 });
156 });
158 <% end %>
157 <% end %>
@@ -1,39 +1,41
1 <%= form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list time-entries">
4 <table class="list time-entries">
5 <thead>
5 <thead>
6 <tr>
6 <tr>
7 <th class="checkbox hide-when-print">
7 <th class="checkbox hide-when-print">
8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
10 </th>
10 </th>
11 <% @query.inline_columns.each do |column| %>
11 <% @query.inline_columns.each do |column| %>
12 <%= column_header(column) %>
12 <%= column_header(column) %>
13 <% end %>
13 <% end %>
14 <th></th>
14 <th></th>
15 </tr>
15 </tr>
16 </thead>
16 </thead>
17 <tbody>
17 <tbody>
18 <% entries.each do |entry| -%>
18 <% entries.each do |entry| -%>
19 <tr class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
19 <tr class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
20 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
20 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
21 <%= raw @query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, entry)}</td>"}.join %>
21 <%= raw @query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, entry)}</td>"}.join %>
22 <td class="buttons">
22 <td class="buttons">
23 <% if entry.editable_by?(User.current) -%>
23 <% if entry.editable_by?(User.current) -%>
24 <%= link_to image_tag('edit.png'), edit_time_entry_path(entry),
24 <%= link_to '', edit_time_entry_path(entry),
25 :title => l(:button_edit) %>
25 :title => l(:button_edit),
26 <%= link_to image_tag('delete.png'), time_entry_path(entry),
26 :class => 'icon icon-edit' %>
27 :data => {:confirm => l(:text_are_you_sure)},
27 <%= link_to '', time_entry_path(entry),
28 :method => :delete,
28 :data => {:confirm => l(:text_are_you_sure)},
29 :title => l(:button_delete) %>
29 :method => :delete,
30 :title => l(:button_delete),
31 :class => 'icon-only icon-del' %>
30 <% end -%>
32 <% end -%>
31 </td>
33 </td>
32 </tr>
34 </tr>
33 <% end -%>
35 <% end -%>
34 </tbody>
36 </tbody>
35 </table>
37 </table>
36 </div>
38 </div>
37 <% end -%>
39 <% end -%>
38
40
39 <%= context_menu time_entries_context_menu_path %>
41 <%= context_menu time_entries_context_menu_path %>
@@ -1,75 +1,78
1 <%= title [l(:label_tracker_plural), trackers_path], l(:field_summary) %>
1 <%= title [l(:label_tracker_plural), trackers_path], l(:field_summary) %>
2
2
3 <% if @trackers.any? %>
3 <% if @trackers.any? %>
4 <%= form_tag fields_trackers_path do %>
4 <%= form_tag fields_trackers_path do %>
5 <div class="autoscroll">
5 <div class="autoscroll">
6 <table class="list">
6 <table class="list">
7 <thead>
7 <thead>
8 <tr>
8 <tr>
9 <th></th>
9 <th></th>
10 <% @trackers.each do |tracker| %>
10 <% @trackers.each do |tracker| %>
11 <th>
11 <th>
12 <%= link_to_function('', "toggleCheckboxesBySelector('input.tracker-#{tracker.id}')",
13 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
14 :class => 'icon-only icon-checked') %>
12 <%= tracker.name %>
15 <%= tracker.name %>
13 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.tracker-#{tracker.id}')",
14 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
15 </th>
16 </th>
16 <% end %>
17 <% end %>
17 </tr>
18 </tr>
18 </thead>
19 </thead>
19 <tbody>
20 <tbody>
20 <tr class="group open">
21 <tr class="group open">
21 <td colspan="<%= @trackers.size + 1 %>">
22 <td colspan="<%= @trackers.size + 1 %>">
22 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
23 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
23 <%= l(:field_core_fields) %>
24 <%= l(:field_core_fields) %>
24 </td>
25 </td>
25 </tr>
26 </tr>
26 <% Tracker::CORE_FIELDS.each do |field| %>
27 <% Tracker::CORE_FIELDS.each do |field| %>
27 <tr class="<%= cycle("odd", "even") %>">
28 <tr class="<%= cycle("odd", "even") %>">
28 <td class="name">
29 <td class="name">
29 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.core-field-#{field}')",
30 <%= link_to_function('', "toggleCheckboxesBySelector('input.core-field-#{field}')",
30 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
31 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
32 :class => 'icon-only icon-checked') %>
31 <%= l("field_#{field}".sub(/_id$/, '')) %>
33 <%= l("field_#{field}".sub(/_id$/, '')) %>
32 </td>
34 </td>
33 <% @trackers.each do |tracker| %>
35 <% @trackers.each do |tracker| %>
34 <td>
36 <td>
35 <%= check_box_tag "trackers[#{tracker.id}][core_fields][]", field, tracker.core_fields.include?(field),
37 <%= check_box_tag "trackers[#{tracker.id}][core_fields][]", field, tracker.core_fields.include?(field),
36 :class => "tracker-#{tracker.id} core-field-#{field}", :id => nil %>
38 :class => "tracker-#{tracker.id} core-field-#{field}", :id => nil %>
37 </td>
39 </td>
38 <% end %>
40 <% end %>
39 </tr>
41 </tr>
40 <% end %>
42 <% end %>
41 <% if @custom_fields.any? %>
43 <% if @custom_fields.any? %>
42 <tr class="group open">
44 <tr class="group open">
43 <td colspan="<%= @trackers.size + 1 %>">
45 <td colspan="<%= @trackers.size + 1 %>">
44 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
46 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
45 <%= l(:label_custom_field_plural) %>
47 <%= l(:label_custom_field_plural) %>
46 </td>
48 </td>
47 </tr>
49 </tr>
48 <% @custom_fields.each do |field| %>
50 <% @custom_fields.each do |field| %>
49 <tr class="<%= cycle("odd", "even") %>">
51 <tr class="<%= cycle("odd", "even") %>">
50 <td class="name">
52 <td class="name">
51 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.custom-field-#{field.id}')",
53 <%= link_to_function('', "toggleCheckboxesBySelector('input.custom-field-#{field.id}')",
52 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
54 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
55 :class => 'icon-only icon-checked') %>
53 <%= field.name %>
56 <%= field.name %>
54 </td>
57 </td>
55 <% @trackers.each do |tracker| %>
58 <% @trackers.each do |tracker| %>
56 <td>
59 <td>
57 <%= check_box_tag "trackers[#{tracker.id}][custom_field_ids][]", field.id, tracker.custom_fields.include?(field),
60 <%= check_box_tag "trackers[#{tracker.id}][custom_field_ids][]", field.id, tracker.custom_fields.include?(field),
58 :class => "tracker-#{tracker.id} custom-field-#{field.id}", :id => nil %>
61 :class => "tracker-#{tracker.id} custom-field-#{field.id}", :id => nil %>
59 </td>
62 </td>
60 <% end %>
63 <% end %>
61 </tr>
64 </tr>
62 <% end %>
65 <% end %>
63 <% end %>
66 <% end %>
64 </tbody>
67 </tbody>
65 </table>
68 </table>
66 </div>
69 </div>
67 <p><%= submit_tag l(:button_save) %></p>
70 <p><%= submit_tag l(:button_save) %></p>
68 <% @trackers.each do |tracker| %>
71 <% @trackers.each do |tracker| %>
69 <%= hidden_field_tag "trackers[#{tracker.id}][core_fields][]", '' %>
72 <%= hidden_field_tag "trackers[#{tracker.id}][core_fields][]", '' %>
70 <%= hidden_field_tag "trackers[#{tracker.id}][custom_field_ids][]", '' %>
73 <%= hidden_field_tag "trackers[#{tracker.id}][custom_field_ids][]", '' %>
71 <% end %>
74 <% end %>
72 <% end %>
75 <% end %>
73 <% else %>
76 <% else %>
74 <p class="nodata"><%= l(:label_no_data) %></p>
77 <p class="nodata"><%= l(:label_no_data) %></p>
75 <% end %>
78 <% end %>
@@ -1,41 +1,44
1 <table class="list workflows transitions transitions-<%= name %>">
1 <table class="list workflows transitions transitions-<%= name %>">
2 <thead>
2 <thead>
3 <tr>
3 <tr>
4 <th>
4 <th>
5 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
5 <%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input')",
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
7 :class => 'icon-only icon-checked') %>
7 <%=l(:label_current_status)%>
8 <%=l(:label_current_status)%>
8 </th>
9 </th>
9 <th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
10 <th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
10 </tr>
11 </tr>
11 <tr>
12 <tr>
12 <td></td>
13 <td></td>
13 <% for new_status in @statuses %>
14 <% for new_status in @statuses %>
14 <td style="width:<%= 75 / @statuses.size %>%;">
15 <td style="width:<%= 75 / @statuses.size %>%;">
15 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
16 <%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
16 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
17 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
18 :class => 'icon-only icon-checked') %>
17 <%= new_status.name %>
19 <%= new_status.name %>
18 </td>
20 </td>
19 <% end %>
21 <% end %>
20 </tr>
22 </tr>
21 </thead>
23 </thead>
22 <tbody>
24 <tbody>
23 <% for old_status in [nil] + @statuses %>
25 <% for old_status in [nil] + @statuses %>
24 <% next if old_status.nil? && name != 'always' %>
26 <% next if old_status.nil? && name != 'always' %>
25 <tr class="<%= cycle("odd", "even") %>">
27 <tr class="<%= cycle("odd", "even") %>">
26 <td class="name">
28 <td class="name">
27 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.try(:id) || 0}')",
29 <%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.try(:id) || 0}')",
28 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
30 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
31 :class => 'icon-only icon-checked') %>
29
32
30 <%= old_status ? old_status.name : content_tag('em', l(:label_issue_new)) %>
33 <%= old_status ? old_status.name : content_tag('em', l(:label_issue_new)) %>
31 </td>
34 </td>
32 <% for new_status in @statuses -%>
35 <% for new_status in @statuses -%>
33 <% checked = workflows.detect {|w| w.old_status == old_status && w.new_status == new_status} %>
36 <% checked = workflows.detect {|w| w.old_status == old_status && w.new_status == new_status} %>
34 <td class="<%= checked ? 'enabled' : '' %>">
37 <td class="<%= checked ? 'enabled' : '' %>">
35 <%= transition_tag workflows, old_status, new_status, name %>
38 <%= transition_tag workflows, old_status, new_status, name %>
36 </td>
39 </td>
37 <% end -%>
40 <% end -%>
38 </tr>
41 </tr>
39 <% end %>
42 <% end %>
40 </tbody>
43 </tbody>
41 </table>
44 </table>
@@ -1,75 +1,75
1 <%= render :partial => 'action_menu' %>
1 <%= render :partial => 'action_menu' %>
2
2
3 <%= title l(:label_workflow) %>
3 <%= title l(:label_workflow) %>
4
4
5 <div class="tabs">
5 <div class="tabs">
6 <ul>
6 <ul>
7 <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
7 <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
8 <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers) %></li>
8 <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers) %></li>
9 </ul>
9 </ul>
10 </div>
10 </div>
11
11
12 <p><%=l(:text_workflow_edit)%>:</p>
12 <p><%=l(:text_workflow_edit)%>:</p>
13
13
14 <%= form_tag({}, :method => 'get') do %>
14 <%= form_tag({}, :method => 'get') do %>
15 <p>
15 <p>
16 <label><%=l(:label_role)%>:
16 <label><%=l(:label_role)%>:
17 <%= options_for_workflow_select 'role_id[]', Role.sorted.select(&:consider_workflow?), @roles, :id => 'role_id', :class => 'expandable' %>
17 <%= options_for_workflow_select 'role_id[]', Role.sorted.select(&:consider_workflow?), @roles, :id => 'role_id', :class => 'expandable' %>
18 </label>
18 </label>
19 <a href="#" data-expands="#role_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
19 <a href="#" data-expands="#role_id"><span class="toggle-multiselect"></span></a>
20
20
21 <label><%=l(:label_tracker)%>:
21 <label><%=l(:label_tracker)%>:
22 <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %>
22 <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %>
23 </label>
23 </label>
24 <a href="#" data-expands="#tracker_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
24 <a href="#" data-expands="#tracker_id"><span class="toggle-multiselect"></span></a>
25
25
26 <%= submit_tag l(:button_edit), :name => nil %>
26 <%= submit_tag l(:button_edit), :name => nil %>
27
27
28 <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
28 <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
29 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
29 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
30
30
31 </p>
31 </p>
32 <% end %>
32 <% end %>
33
33
34 <% if @trackers && @roles && @statuses.any? %>
34 <% if @trackers && @roles && @statuses.any? %>
35 <%= form_tag({}, :id => 'workflow_form' ) do %>
35 <%= form_tag({}, :id => 'workflow_form' ) do %>
36 <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
36 <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
37 <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
37 <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
38 <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
38 <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
39 <div class="autoscroll">
39 <div class="autoscroll">
40 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
40 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
41
41
42 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
42 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
43 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
43 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
44 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
44 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
45 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
45 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
46 </div>
46 </div>
47 </fieldset>
47 </fieldset>
48 <%= javascript_tag "hideFieldset($('#author_workflows'))" unless @workflows['author'].present? %>
48 <%= javascript_tag "hideFieldset($('#author_workflows'))" unless @workflows['author'].present? %>
49
49
50 <fieldset class="collapsible" style="padding: 0;">
50 <fieldset class="collapsible" style="padding: 0;">
51 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
51 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
52 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
52 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
53 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
53 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
54 </div>
54 </div>
55 </fieldset>
55 </fieldset>
56 <%= javascript_tag "hideFieldset($('#assignee_workflows'))" unless @workflows['assignee'].present? %>
56 <%= javascript_tag "hideFieldset($('#assignee_workflows'))" unless @workflows['assignee'].present? %>
57 </div>
57 </div>
58 <%= submit_tag l(:button_save) %>
58 <%= submit_tag l(:button_save) %>
59 <% end %>
59 <% end %>
60 <% end %>
60 <% end %>
61
61
62 <%= javascript_tag do %>
62 <%= javascript_tag do %>
63 $("a[data-expands]").click(function(e){
63 $("a[data-expands]").click(function(e){
64 e.preventDefault();
64 e.preventDefault();
65 var target = $($(this).attr("data-expands"));
65 var target = $($(this).attr("data-expands"));
66 if (target.attr("multiple")) {
66 if (target.attr("multiple")) {
67 target.attr("multiple", false);
67 target.attr("multiple", false);
68 target.find("option[value=all]").show();
68 target.find("option[value=all]").show();
69 } else {
69 } else {
70 target.attr("multiple", true);
70 target.attr("multiple", true);
71 target.find("option[value=all]").attr("selected", false).hide();
71 target.find("option[value=all]").attr("selected", false).hide();
72 }
72 }
73 });
73 });
74
74
75 <% end %>
75 <% end %>
@@ -1,33 +1,35
1 <%= title [l(:label_workflow), workflows_edit_path], l(:field_summary) %>
1 <%= title [l(:label_workflow), workflows_edit_path], l(:field_summary) %>
2
2
3 <% if @roles.empty? || @trackers.empty? %>
3 <% if @roles.empty? || @trackers.empty? %>
4 <p class="nodata"><%= l(:label_no_data) %></p>
4 <p class="nodata"><%= l(:label_no_data) %></p>
5 <% else %>
5 <% else %>
6 <div class="autoscroll">
6 <div class="autoscroll">
7 <table class="list">
7 <table class="list">
8 <thead>
8 <thead>
9 <tr>
9 <tr>
10 <th></th>
10 <th></th>
11 <% @roles.each do |role| %>
11 <% @roles.each do |role| %>
12 <th>
12 <th>
13 <%= content_tag(role.builtin? ? 'em' : 'span', role.name) %>
13 <%= content_tag(role.builtin? ? 'em' : 'span', role.name) %>
14 </th>
14 </th>
15 <% end %>
15 <% end %>
16 </tr>
16 </tr>
17 </thead>
17 </thead>
18 <tbody>
18 <tbody>
19 <% @trackers.each do |tracker| -%>
19 <% @trackers.each do |tracker| -%>
20 <tr class="<%= cycle('odd', 'even') %>">
20 <tr class="<%= cycle('odd', 'even') %>">
21 <td class="name"><%= tracker.name %></td>
21 <td class="name"><%= tracker.name %></td>
22 <% @roles.each do |role| -%>
22 <% @roles.each do |role| -%>
23 <% count = @workflow_counts[[tracker.id, role.id]] || 0 %>
23 <% count = @workflow_counts[[tracker.id, role.id]] || 0 %>
24 <td>
24 <td>
25 <%= link_to((count > 0 ? count : image_tag('false.png')), {:action => 'edit', :role_id => role, :tracker_id => tracker}, :title => l(:button_edit)) %>
25 <%= link_to((count > 0 ? count : content_tag(:span, nil, :class => 'icon-only icon-not-ok')),
26 {:action => 'edit', :role_id => role, :tracker_id => tracker},
27 :title => l(:button_edit)) %>
26 </td>
28 </td>
27 <% end -%>
29 <% end -%>
28 </tr>
30 </tr>
29 <% end -%>
31 <% end -%>
30 </tbody>
32 </tbody>
31 </table>
33 </table>
32 </div>
34 </div>
33 <% end %>
35 <% end %>
@@ -1,123 +1,123
1 <%= render :partial => 'action_menu' %>
1 <%= render :partial => 'action_menu' %>
2
2
3 <%= title l(:label_workflow) %>
3 <%= title l(:label_workflow) %>
4
4
5 <div class="tabs">
5 <div class="tabs">
6 <ul>
6 <ul>
7 <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers) %></li>
7 <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers) %></li>
8 <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
8 <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
9 </ul>
9 </ul>
10 </div>
10 </div>
11
11
12 <p><%=l(:text_workflow_edit)%>:</p>
12 <p><%=l(:text_workflow_edit)%>:</p>
13
13
14 <%= form_tag({}, :method => 'get') do %>
14 <%= form_tag({}, :method => 'get') do %>
15 <p>
15 <p>
16 <label><%=l(:label_role)%>:
16 <label><%=l(:label_role)%>:
17 <%= options_for_workflow_select 'role_id[]', Role.sorted.select(&:consider_workflow?), @roles, :id => 'role_id', :class => 'expandable' %>
17 <%= options_for_workflow_select 'role_id[]', Role.sorted.select(&:consider_workflow?), @roles, :id => 'role_id', :class => 'expandable' %>
18 </label>
18 </label>
19 <a href="#" data-expands="#role_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
19 <a href="#" data-expands="#role_id"><span class="toggle-multiselect"></a>
20
20
21 <label><%=l(:label_tracker)%>:
21 <label><%=l(:label_tracker)%>:
22 <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %>
22 <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %>
23 </label>
23 </label>
24 <a href="#" data-expands="#tracker_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
24 <a href="#" data-expands="#tracker_id"><span class="toggle-multiselect"></a>
25
25
26 <%= submit_tag l(:button_edit), :name => nil %>
26 <%= submit_tag l(:button_edit), :name => nil %>
27
27
28 <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
28 <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
29 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
29 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
30 </p>
30 </p>
31 <% end %>
31 <% end %>
32
32
33 <% if @trackers && @roles && @statuses.any? %>
33 <% if @trackers && @roles && @statuses.any? %>
34 <%= form_tag({}, :id => 'workflow_form' ) do %>
34 <%= form_tag({}, :id => 'workflow_form' ) do %>
35 <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
35 <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
36 <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
36 <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
37 <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
37 <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
38 <div class="autoscroll">
38 <div class="autoscroll">
39 <table class="list workflows fields_permissions">
39 <table class="list workflows fields_permissions">
40 <thead>
40 <thead>
41 <tr>
41 <tr>
42 <th>
42 <th>
43 </th>
43 </th>
44 <th colspan="<%= @statuses.length %>"><%=l(:label_issue_status)%></th>
44 <th colspan="<%= @statuses.length %>"><%=l(:label_issue_status)%></th>
45 </tr>
45 </tr>
46 <tr>
46 <tr>
47 <td></td>
47 <td></td>
48 <% for status in @statuses %>
48 <% for status in @statuses %>
49 <td style="width:<%= 75 / @statuses.size %>%;">
49 <td style="width:<%= 75 / @statuses.size %>%;">
50 <%= status.name %>
50 <%= status.name %>
51 </td>
51 </td>
52 <% end %>
52 <% end %>
53 </tr>
53 </tr>
54 </thead>
54 </thead>
55 <tbody>
55 <tbody>
56 <tr class="group open">
56 <tr class="group open">
57 <td colspan="<%= @statuses.size + 1 %>">
57 <td colspan="<%= @statuses.size + 1 %>">
58 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
58 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
59 <%= l(:field_core_fields) %>
59 <%= l(:field_core_fields) %>
60 </td>
60 </td>
61 </tr>
61 </tr>
62 <% @fields.each do |field, name| %>
62 <% @fields.each do |field, name| %>
63 <tr class="<%= cycle("odd", "even") %>">
63 <tr class="<%= cycle("odd", "even") %>">
64 <td class="name">
64 <td class="name">
65 <%= name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %>
65 <%= name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %>
66 </td>
66 </td>
67 <% for status in @statuses -%>
67 <% for status in @statuses -%>
68 <td class="<%= @permissions[status.id][field].try(:join, ' ') %>">
68 <td class="<%= @permissions[status.id][field].try(:join, ' ') %>">
69 <%= field_permission_tag(@permissions, status, field, @roles) %>
69 <%= field_permission_tag(@permissions, status, field, @roles) %>
70 <% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %>
70 <% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %>
71 </td>
71 </td>
72 <% end -%>
72 <% end -%>
73 </tr>
73 </tr>
74 <% end %>
74 <% end %>
75 <% if @custom_fields.any? %>
75 <% if @custom_fields.any? %>
76 <tr class="group open">
76 <tr class="group open">
77 <td colspan="<%= @statuses.size + 1 %>">
77 <td colspan="<%= @statuses.size + 1 %>">
78 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
78 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
79 <%= l(:label_custom_field_plural) %>
79 <%= l(:label_custom_field_plural) %>
80 </td>
80 </td>
81 </tr>
81 </tr>
82 <% @custom_fields.each do |field| %>
82 <% @custom_fields.each do |field| %>
83 <tr class="<%= cycle("odd", "even") %>">
83 <tr class="<%= cycle("odd", "even") %>">
84 <td class="name">
84 <td class="name">
85 <%= field.name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %>
85 <%= field.name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %>
86 </td>
86 </td>
87 <% for status in @statuses -%>
87 <% for status in @statuses -%>
88 <td class="<%= @permissions[status.id][field.id.to_s] %>">
88 <td class="<%= @permissions[status.id][field.id.to_s] %>">
89 <%= field_permission_tag(@permissions, status, field, @roles) %>
89 <%= field_permission_tag(@permissions, status, field, @roles) %>
90 <% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %>
90 <% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %>
91 </td>
91 </td>
92 <% end -%>
92 <% end -%>
93 </tr>
93 </tr>
94 <% end %>
94 <% end %>
95 <% end %>
95 <% end %>
96 </tbody>
96 </tbody>
97 </table>
97 </table>
98 </div>
98 </div>
99 <%= submit_tag l(:button_save) %>
99 <%= submit_tag l(:button_save) %>
100 <% end %>
100 <% end %>
101 <% end %>
101 <% end %>
102
102
103 <%= javascript_tag do %>
103 <%= javascript_tag do %>
104 $("a.repeat-value").click(function(e){
104 $("a.repeat-value").click(function(e){
105 e.preventDefault();
105 e.preventDefault();
106 var td = $(this).closest('td');
106 var td = $(this).closest('td');
107 var selected = td.find("select").find(":selected").val();
107 var selected = td.find("select").find(":selected").val();
108 td.nextAll('td').find("select").val(selected);
108 td.nextAll('td').find("select").val(selected);
109 });
109 });
110
110
111 $("a[data-expands]").click(function(e){
111 $("a[data-expands]").click(function(e){
112 e.preventDefault();
112 e.preventDefault();
113 var target = $($(this).attr("data-expands"));
113 var target = $($(this).attr("data-expands"));
114 if (target.attr("multiple")) {
114 if (target.attr("multiple")) {
115 target.attr("multiple", false);
115 target.attr("multiple", false);
116 target.find("option[value=all]").show();
116 target.find("option[value=all]").show();
117 } else {
117 } else {
118 target.attr("multiple", true);
118 target.attr("multiple", true);
119 target.find("option[value=all]").attr("selected", false).hide();
119 target.find("option[value=all]").attr("selected", false).hide();
120 }
120 }
121 });
121 });
122
122
123 <% end %>
123 <% end %>
@@ -1,1318 +1,1338
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#333; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#333; 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 pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
10 pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
11
11
12 /***** Layout *****/
12 /***** Layout *****/
13 #wrapper {background: white;overflow: hidden;}
13 #wrapper {background: white;overflow: hidden;}
14
14
15 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
16 #top-menu ul {margin: 0; padding: 0;}
16 #top-menu ul {margin: 0; padding: 0;}
17 #top-menu li {
17 #top-menu li {
18 float:left;
18 float:left;
19 list-style-type:none;
19 list-style-type:none;
20 margin: 0px 0px 0px 0px;
20 margin: 0px 0px 0px 0px;
21 padding: 0px 0px 0px 0px;
21 padding: 0px 0px 0px 0px;
22 white-space:nowrap;
22 white-space:nowrap;
23 }
23 }
24 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
25 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
26
26
27 #account {float:right;}
27 #account {float:right;}
28
28
29 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
29 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
30 #header a {color:#f8f8f8;}
30 #header a {color:#f8f8f8;}
31 #header h1 a.ancestor { font-size: 80%; }
31 #header h1 a.ancestor { font-size: 80%; }
32 #quick-search {float:right;}
32 #quick-search {float:right;}
33
33
34 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px; width: 100%;}
34 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px; width: 100%;}
35 #main-menu ul {margin: 0; padding: 0; width: 100%; white-space: nowrap;}
35 #main-menu ul {margin: 0; padding: 0; width: 100%; white-space: nowrap;}
36 #main-menu li {
36 #main-menu li {
37 float:none;
37 float:none;
38 list-style-type:none;
38 list-style-type:none;
39 margin: 0px 2px 0px 0px;
39 margin: 0px 2px 0px 0px;
40 padding: 0px 0px 0px 0px;
40 padding: 0px 0px 0px 0px;
41 white-space:nowrap;
41 white-space:nowrap;
42 display:inline-block;
42 display:inline-block;
43 }
43 }
44 #main-menu li a {
44 #main-menu li a {
45 display: block;
45 display: block;
46 color: #fff;
46 color: #fff;
47 text-decoration: none;
47 text-decoration: none;
48 font-weight: bold;
48 font-weight: bold;
49 margin: 0;
49 margin: 0;
50 padding: 4px 10px 4px 10px;
50 padding: 4px 10px 4px 10px;
51 }
51 }
52 #main-menu li a:hover {background:#759FCF; color:#fff;}
52 #main-menu li a:hover {background:#759FCF; color:#fff;}
53 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
53 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
54 #main-menu .tabs-buttons {
54 #main-menu .tabs-buttons {
55 right: 6px;
55 right: 6px;
56 background-color: transparent;
56 background-color: transparent;
57 border-bottom-color: transparent;
57 border-bottom-color: transparent;
58 }
58 }
59
59
60 #admin-menu ul {margin: 0; padding: 0;}
60 #admin-menu ul {margin: 0; padding: 0;}
61 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
61 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
62
62
63 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
63 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
64 #admin-menu a.projects { background-image: url(../images/projects.png); }
64 #admin-menu a.projects { background-image: url(../images/projects.png); }
65 #admin-menu a.users { background-image: url(../images/user.png); }
65 #admin-menu a.users { background-image: url(../images/user.png); }
66 #admin-menu a.groups { background-image: url(../images/group.png); }
66 #admin-menu a.groups { background-image: url(../images/group.png); }
67 #admin-menu a.roles { background-image: url(../images/database_key.png); }
67 #admin-menu a.roles { background-image: url(../images/database_key.png); }
68 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
68 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
69 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
69 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
70 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
70 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
71 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
71 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
72 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
72 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
73 #admin-menu a.settings { background-image: url(../images/changeset.png); }
73 #admin-menu a.settings { background-image: url(../images/changeset.png); }
74 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
74 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
75 #admin-menu a.info { background-image: url(../images/help.png); }
75 #admin-menu a.info { background-image: url(../images/help.png); }
76 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
76 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
77
77
78 #main {background-color:#EEEEEE;}
78 #main {background-color:#EEEEEE;}
79
79
80 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
80 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
81 * html #sidebar{ width: 22%; }
81 * html #sidebar{ width: 22%; }
82 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
82 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
83 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
83 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
84 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
84 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
85 #sidebar .contextual { margin-right: 1em; }
85 #sidebar .contextual { margin-right: 1em; }
86 #sidebar ul, ul.flat {margin: 0; padding: 0;}
86 #sidebar ul, ul.flat {margin: 0; padding: 0;}
87 #sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
87 #sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
88
88
89 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
89 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
90 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
90 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
91 html>body #content { min-height: 600px; }
91 html>body #content { min-height: 600px; }
92 * html body #content { height: 600px; } /* IE */
92 * html body #content { height: 600px; } /* IE */
93
93
94 #main.nosidebar #sidebar{ display: none; }
94 #main.nosidebar #sidebar{ display: none; }
95 #main.nosidebar #content{ width: auto; border-right: 0; }
95 #main.nosidebar #content{ width: auto; border-right: 0; }
96
96
97 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
97 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
98
98
99 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
99 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
100 #login-form table td {padding: 6px;}
100 #login-form table td {padding: 6px;}
101 #login-form label {font-weight: bold;}
101 #login-form label {font-weight: bold;}
102 #login-form input#username, #login-form input#password { width: 300px; }
102 #login-form input#username, #login-form input#password { width: 300px; }
103
103
104 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
104 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
105 div.modal h3.title {display:none;}
105 div.modal h3.title {display:none;}
106 div.modal p.buttons {text-align:right; margin-bottom:0;}
106 div.modal p.buttons {text-align:right; margin-bottom:0;}
107 div.modal .box p {margin: 0.3em 0;}
107 div.modal .box p {margin: 0.3em 0;}
108
108
109 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
109 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
110
110
111 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
111 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
112
112
113 .mobile-show {display: none;}
113 .mobile-show {display: none;}
114
114
115 /***** Links *****/
115 /***** Links *****/
116 a, a:link, a:visited{ color: #169; text-decoration: none; }
116 a, a:link, a:visited{ color: #169; text-decoration: none; }
117 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
117 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
118 a img{ border: 0; }
118 a img{ border: 0; }
119
119
120 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
120 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
121 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
121 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
122 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
122 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
123
123
124 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
124 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
125 #sidebar a.selected:hover {text-decoration:none;}
125 #sidebar a.selected:hover {text-decoration:none;}
126 #admin-menu a {line-height:1.7em;}
126 #admin-menu a {line-height:1.7em;}
127 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
127 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
128
128
129 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
129 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
130 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
130 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
131
131
132 a#toggle-completed-versions {color:#999;}
132 a#toggle-completed-versions {color:#999;}
133
134 a.toggle-checkboxes { margin-left: 5px; padding-left: 12px; background: url(../images/toggle_check.png) no-repeat 0% 50%; }
135
133 /***** Tables *****/
136 /***** Tables *****/
134 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
137 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
135 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
138 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
136 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
139 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
137 table.list td.id { width: 2%; text-align: center;}
140 table.list td.id { width: 2%; text-align: center;}
138 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
141 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
139 table.list td.tick {width:15%}
142 table.list td.tick {width:15%}
140 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
143 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
141 table.list td.checkbox input {padding:0px;}
144 table.list td.checkbox input {padding:0px;}
142 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
145 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
143 table.list td.buttons a { padding-right: 0.6em; }
146 table.list td.buttons a { padding-right: 0.6em; }
144 table.list td.buttons img {vertical-align:middle;}
147 table.list td.buttons img {vertical-align:middle;}
145 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
148 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
146 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
149 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
147
150
148 tr.project td.name a { white-space:nowrap; }
151 tr.project td.name a { white-space:nowrap; }
149 tr.project.closed, tr.project.archived { color: #aaa; }
152 tr.project.closed, tr.project.archived { color: #aaa; }
150 tr.project.closed a, tr.project.archived a { color: #aaa; }
153 tr.project.closed a, tr.project.archived a { color: #aaa; }
151
154
152 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
155 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
153 tr.project.idnt-1 td.name {padding-left: 0.5em;}
156 tr.project.idnt-1 td.name {padding-left: 0.5em;}
154 tr.project.idnt-2 td.name {padding-left: 2em;}
157 tr.project.idnt-2 td.name {padding-left: 2em;}
155 tr.project.idnt-3 td.name {padding-left: 3.5em;}
158 tr.project.idnt-3 td.name {padding-left: 3.5em;}
156 tr.project.idnt-4 td.name {padding-left: 5em;}
159 tr.project.idnt-4 td.name {padding-left: 5em;}
157 tr.project.idnt-5 td.name {padding-left: 6.5em;}
160 tr.project.idnt-5 td.name {padding-left: 6.5em;}
158 tr.project.idnt-6 td.name {padding-left: 8em;}
161 tr.project.idnt-6 td.name {padding-left: 8em;}
159 tr.project.idnt-7 td.name {padding-left: 9.5em;}
162 tr.project.idnt-7 td.name {padding-left: 9.5em;}
160 tr.project.idnt-8 td.name {padding-left: 11em;}
163 tr.project.idnt-8 td.name {padding-left: 11em;}
161 tr.project.idnt-9 td.name {padding-left: 12.5em;}
164 tr.project.idnt-9 td.name {padding-left: 12.5em;}
162
165
163 tr.issue { text-align: center; white-space: nowrap; }
166 tr.issue { text-align: center; white-space: nowrap; }
164 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
167 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
165 tr.issue td.relations { text-align: left; }
168 tr.issue td.relations { text-align: left; }
166 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
169 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
167 tr.issue td.relations span {white-space: nowrap;}
170 tr.issue td.relations span {white-space: nowrap;}
168 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
171 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
169 table.issues td.description pre {white-space:normal;}
172 table.issues td.description pre {white-space:normal;}
170
173
171 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
174 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
172 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
175 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
173 tr.issue.idnt-2 td.subject {padding-left: 2em;}
176 tr.issue.idnt-2 td.subject {padding-left: 2em;}
174 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
177 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
175 tr.issue.idnt-4 td.subject {padding-left: 5em;}
178 tr.issue.idnt-4 td.subject {padding-left: 5em;}
176 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
179 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
177 tr.issue.idnt-6 td.subject {padding-left: 8em;}
180 tr.issue.idnt-6 td.subject {padding-left: 8em;}
178 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
181 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
179 tr.issue.idnt-8 td.subject {padding-left: 11em;}
182 tr.issue.idnt-8 td.subject {padding-left: 11em;}
180 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
183 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
181
184
182 table.issue-report {table-layout:fixed;}
185 table.issue-report {table-layout:fixed;}
183
186
184 tr.entry { border: 1px solid #f8f8f8; }
187 tr.entry { border: 1px solid #f8f8f8; }
185 tr.entry td { white-space: nowrap; }
188 tr.entry td { white-space: nowrap; }
186 tr.entry td.filename {width:30%; text-align:left;}
189 tr.entry td.filename {width:30%; text-align:left;}
187 tr.entry td.filename_no_report {width:70%; text-align:left;}
190 tr.entry td.filename_no_report {width:70%; text-align:left;}
188 tr.entry td.size { text-align: right; font-size: 90%; }
191 tr.entry td.size { text-align: right; font-size: 90%; }
189 tr.entry td.revision, tr.entry td.author { text-align: center; }
192 tr.entry td.revision, tr.entry td.author { text-align: center; }
190 tr.entry td.age { text-align: right; }
193 tr.entry td.age { text-align: right; }
191 tr.entry.file td.filename a { margin-left: 16px; }
194 tr.entry.file td.filename a { margin-left: 16px; }
192 tr.entry.file td.filename_no_report a { margin-left: 16px; }
195 tr.entry.file td.filename_no_report a { margin-left: 16px; }
193
196
194 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
197 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
195 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
198 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
196
199
197 tr.changeset { height: 20px }
200 tr.changeset { height: 20px }
198 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
201 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
199 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
202 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
200 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
203 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
201 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
204 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
202
205
203 table.files tbody th {text-align:left;}
206 table.files tbody th {text-align:left;}
204 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
207 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
205 table.files tr.file td.digest { font-size: 80%; }
208 table.files tr.file td.digest { font-size: 80%; }
206
209
207 table.members td.roles, table.memberships td.roles { width: 45%; }
210 table.members td.roles, table.memberships td.roles { width: 45%; }
208
211
209 tr.message { height: 2.6em; }
212 tr.message { height: 2.6em; }
210 tr.message td.subject { padding-left: 20px; }
213 tr.message td.subject { padding-left: 20px; }
211 tr.message td.created_on { white-space: nowrap; }
214 tr.message td.created_on { white-space: nowrap; }
212 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
215 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
213 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
216 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
214 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
217 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
215
218
216 tr.version.closed, tr.version.closed a { color: #999; }
219 tr.version.closed, tr.version.closed a { color: #999; }
217 tr.version td.name { padding-left: 20px; }
220 tr.version td.name { padding-left: 20px; }
218 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
221 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
219 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
222 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
220
223
221 tr.user td {width:13%;white-space: nowrap;}
224 tr.user td {width:13%;white-space: nowrap;}
222 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
225 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
223 tr.user td.email { width:18%; }
226 tr.user td.email { width:18%; }
224 tr.user.locked, tr.user.registered { color: #aaa; }
227 tr.user.locked, tr.user.registered { color: #aaa; }
225 tr.user.locked a, tr.user.registered a { color: #aaa; }
228 tr.user.locked a, tr.user.registered a { color: #aaa; }
226
229
227 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
230 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
228
231
229 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
232 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
230
233
231 tr.time-entry { text-align: center; white-space: nowrap; }
234 tr.time-entry { text-align: center; white-space: nowrap; }
232 tr.time-entry td.issue, tr.time-entry td.comments, tr.time-entry td.subject, tr.time-entry td.activity { text-align: left; white-space: normal; }
235 tr.time-entry td.issue, tr.time-entry td.comments, tr.time-entry td.subject, tr.time-entry td.activity { text-align: left; white-space: normal; }
233 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
236 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
234 td.hours .hours-dec { font-size: 0.9em; }
237 td.hours .hours-dec { font-size: 0.9em; }
235
238
236 table.plugins td { vertical-align: middle; }
239 table.plugins td { vertical-align: middle; }
237 table.plugins td.configure { text-align: right; padding-right: 1em; }
240 table.plugins td.configure { text-align: right; padding-right: 1em; }
238 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
241 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
239 table.plugins span.description { display: block; font-size: 0.9em; }
242 table.plugins span.description { display: block; font-size: 0.9em; }
240 table.plugins span.url { display: block; font-size: 0.9em; }
243 table.plugins span.url { display: block; font-size: 0.9em; }
241
244
242 tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
245 tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
243 tr.group span.name {font-weight:bold;}
246 tr.group span.name {font-weight:bold;}
244 tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
247 tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
245 tr.group span.totals {color: #aaa; font-size: 80%;}
248 tr.group span.totals {color: #aaa; font-size: 80%;}
246 tr.group span.totals .value {font-weight:bold; color:#777;}
249 tr.group span.totals .value {font-weight:bold; color:#777;}
247 tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
250 tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
248 tr.group:hover a.toggle-all { display:inline;}
251 tr.group:hover a.toggle-all { display:inline;}
249 a.toggle-all:hover {text-decoration:none;}
252 a.toggle-all:hover {text-decoration:none;}
250
253
251 table.list tbody tr:hover { background-color:#ffffdd; }
254 table.list tbody tr:hover { background-color:#ffffdd; }
252 table.list tbody tr.group:hover { background-color:inherit; }
255 table.list tbody tr.group:hover { background-color:inherit; }
253 table td {padding:2px;}
256 table td {padding:2px;}
254 table p {margin:0;}
257 table p {margin:0;}
255 .odd {background-color:#f6f7f8;}
258 .odd {background-color:#f6f7f8;}
256 .even {background-color: #fff;}
259 .even {background-color: #fff;}
257
260
258 tr.builtin td.name {font-style:italic;}
261 tr.builtin td.name {font-style:italic;}
259
262
260 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
263 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
261 a.sort.asc { background-image: url(../images/sort_asc.png); }
264 a.sort.asc { background-image: url(../images/sort_asc.png); }
262 a.sort.desc { background-image: url(../images/sort_desc.png); }
265 a.sort.desc { background-image: url(../images/sort_desc.png); }
263
266
264 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
267 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
265 table.boards td.last-message {text-align:left;font-size:80%;}
268 table.boards td.last-message {text-align:left;font-size:80%;}
266
269
267 table.messages td.last_message {text-align:left;}
270 table.messages td.last_message {text-align:left;}
268
271
269 #query_form_content {font-size:90%;}
272 #query_form_content {font-size:90%;}
270
273
271 .query_sort_criteria_count {
274 .query_sort_criteria_count {
272 display: inline-block;
275 display: inline-block;
273 min-width: 1em;
276 min-width: 1em;
274 }
277 }
275
278
276 table.query-columns {
279 table.query-columns {
277 border-collapse: collapse;
280 border-collapse: collapse;
278 border: 0;
281 border: 0;
279 }
282 }
280
283
281 table.query-columns td.buttons {
284 table.query-columns td.buttons {
282 vertical-align: middle;
285 vertical-align: middle;
283 text-align: center;
286 text-align: center;
284 }
287 }
285 table.query-columns td.buttons input[type=button] {width:35px;}
288 table.query-columns td.buttons input[type=button] {width:35px;}
286 .query-totals {text-align:right; margin-top:-2.3em;}
289 .query-totals {text-align:right; margin-top:-2.3em;}
287 .query-totals>span {margin-left:0.6em;}
290 .query-totals>span {margin-left:0.6em;}
288 .query-totals .value {font-weight:bold;}
291 .query-totals .value {font-weight:bold;}
289
292
290 td.center {text-align:center;}
293 td.center {text-align:center;}
291
294
292 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
295 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
293
296
294 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
297 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
295 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
298 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
296 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
299 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
297 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
300 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
298
301
299 #watchers select {width: 95%; display: block;}
302 #watchers select {width: 95%; display: block;}
300 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
303 #watchers a.delete {opacity: 0.4; margin-left: 5px;}
301 #watchers a.delete:hover {opacity: 1;}
304 #watchers a.delete:hover {opacity: 1;}
302 #watchers img.gravatar {margin: 0 4px 2px 0;}
305 #watchers img.gravatar {margin: 0 4px 2px 0;}
303
306
304 span#watchers_inputs {overflow:auto; display:block;}
307 span#watchers_inputs {overflow:auto; display:block;}
305 span.search_for_watchers {display:block;}
308 span.search_for_watchers {display:block;}
306 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
309 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
307 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
310 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
308
311
309
312
310 .highlight { background-color: #FCFD8D;}
313 .highlight { background-color: #FCFD8D;}
311 .highlight.token-1 { background-color: #faa;}
314 .highlight.token-1 { background-color: #faa;}
312 .highlight.token-2 { background-color: #afa;}
315 .highlight.token-2 { background-color: #afa;}
313 .highlight.token-3 { background-color: #aaf;}
316 .highlight.token-3 { background-color: #aaf;}
314
317
315 .box{
318 .box{
316 padding:6px;
319 padding:6px;
317 margin-bottom: 10px;
320 margin-bottom: 10px;
318 background-color:#f6f6f6;
321 background-color:#f6f6f6;
319 color:#505050;
322 color:#505050;
320 line-height:1.5em;
323 line-height:1.5em;
321 border: 1px solid #e4e4e4;
324 border: 1px solid #e4e4e4;
322 word-wrap: break-word;
325 word-wrap: break-word;
323 border-radius: 3px;
326 border-radius: 3px;
324 }
327 }
325 .pagination .per-page span.selected {
328 .pagination .per-page span.selected {
326 font-weight: bold;
329 font-weight: bold;
327 }
330 }
328
331
329 div.square {
332 div.square {
330 border: 1px solid #999;
333 border: 1px solid #999;
331 float: left;
334 float: left;
332 margin: .3em .4em 0 .4em;
335 margin: .3em .4em 0 .4em;
333 overflow: hidden;
336 overflow: hidden;
334 width: .6em; height: .6em;
337 width: .6em; height: .6em;
335 }
338 }
336 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
339 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
337 .contextual input, .contextual select {font-size:0.9em;}
340 .contextual input, .contextual select {font-size:0.9em;}
338 .message .contextual { margin-top: 0; }
341 .message .contextual { margin-top: 0; }
339
342
340 .splitcontent {overflow:auto;}
343 .splitcontent {overflow:auto;}
341 .splitcontentleft{float:left; width:49%;}
344 .splitcontentleft{float:left; width:49%;}
342 .splitcontentright{float:right; width:49%;}
345 .splitcontentright{float:right; width:49%;}
343 form {display: inline;}
346 form {display: inline;}
344 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
347 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
345 fieldset {border: 1px solid #e4e4e4; margin:0;}
348 fieldset {border: 1px solid #e4e4e4; margin:0;}
346 legend {color: #333;}
349 legend {color: #333;}
347 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
350 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
348 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
351 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
349 blockquote blockquote { margin-left: 0;}
352 blockquote blockquote { margin-left: 0;}
350 abbr, span.field-description[title] { border-bottom: 1px dotted #aaa; cursor: help; }
353 abbr, span.field-description[title] { border-bottom: 1px dotted #aaa; cursor: help; }
351 textarea.wiki-edit {width:99%; resize:vertical;}
354 textarea.wiki-edit {width:99%; resize:vertical;}
352 li p {margin-top: 0;}
355 li p {margin-top: 0;}
353 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
356 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
354 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
357 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
355 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
358 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
356 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
359 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
357 .ltr {direction:ltr !important; unicode-bidi:bidi-override;}
360 .ltr {direction:ltr !important; unicode-bidi:bidi-override;}
358 .rtl {direction:rtl !important; unicode-bidi:bidi-override;}
361 .rtl {direction:rtl !important; unicode-bidi:bidi-override;}
359
362
360 div.issue div.subject div div { padding-left: 16px; }
363 div.issue div.subject div div { padding-left: 16px; }
361 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
364 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
362 div.issue div.subject>div>p { margin-top: 0.5em; }
365 div.issue div.subject>div>p { margin-top: 0.5em; }
363 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
366 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
364 div.issue span.private, div.journal 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;}
367 div.issue span.private, div.journal 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;}
365 div.issue .next-prev-links {color:#999;}
368 div.issue .next-prev-links {color:#999;}
366 div.issue .attributes {margin-top: 2em;}
369 div.issue .attributes {margin-top: 2em;}
367 div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
370 div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
368 div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
371 div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
369 div.issue.overdue .due-date .value { color: #c22; }
372 div.issue.overdue .due-date .value { color: #c22; }
370
373
371 #issue_tree table.issues, #relations table.issues { border: 0; }
374 #issue_tree table.issues, #relations table.issues { border: 0; }
372 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
375 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
373 #relations td.buttons {padding:0;}
376 #relations td.buttons {padding:0;}
374
377
375 fieldset.collapsible {border-width: 1px 0 0 0;}
378 fieldset.collapsible {border-width: 1px 0 0 0;}
376 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
379 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
377 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
380 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
378
381
379 fieldset#date-range p { margin: 2px 0 2px 0; }
382 fieldset#date-range p { margin: 2px 0 2px 0; }
380 fieldset#filters table { border-collapse: collapse; }
383 fieldset#filters table { border-collapse: collapse; }
381 fieldset#filters table td { padding: 0; vertical-align: middle; }
384 fieldset#filters table td { padding: 0; vertical-align: middle; }
382 fieldset#filters tr.filter { height: 2.1em; }
385 fieldset#filters tr.filter { height: 2.1em; }
383 fieldset#filters td.field { width:230px; }
386 fieldset#filters td.field { width:230px; }
384 fieldset#filters td.operator { width:180px; }
387 fieldset#filters td.operator { width:180px; }
385 fieldset#filters td.operator select {max-width:170px;}
388 fieldset#filters td.operator select {max-width:170px;}
386 fieldset#filters td.values { white-space:nowrap; }
389 fieldset#filters td.values { white-space:nowrap; }
387 fieldset#filters td.values select {min-width:130px;}
390 fieldset#filters td.values select {min-width:130px;}
388 fieldset#filters td.values input {height:1em;}
391 fieldset#filters td.values input {height:1em;}
389
392
390 #filters-table {width:60%; float:left;}
393 #filters-table {width:60%; float:left;}
391 .add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
394 .add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
392
395
393 #issue_is_private_wrap {float:right; margin-right:1em;}
396 #issue_is_private_wrap {float:right; margin-right:1em;}
394 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
397 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:16px; margin-left:0; margin-right:5px; cursor:pointer;}
395 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
398 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
396
399
397 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
400 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
398 div#issue-changesets div.changeset { padding: 4px;}
401 div#issue-changesets div.changeset { padding: 4px;}
399 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
402 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
400 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
403 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
401
404
402 .journal ul.details img {margin:0 0 -3px 4px;}
405 .journal ul.details img {margin:0 0 -3px 4px;}
403 div.journal {overflow:auto;}
406 div.journal {overflow:auto;}
404 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
407 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
405 div.journal ul.details {color:#959595; margin-bottom: 1.5em;}
408 div.journal ul.details {color:#959595; margin-bottom: 1.5em;}
406 div.journal ul.details a {color:#70A7CD;}
409 div.journal ul.details a {color:#70A7CD;}
407 div.journal ul.details a:hover {color:#D14848;}
410 div.journal ul.details a:hover {color:#D14848;}
408
411
409 div#activity dl, #search-results { margin-left: 2em; }
412 div#activity dl, #search-results { margin-left: 2em; }
410 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
413 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
411 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
414 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
412 div#activity dt.me .time { border-bottom: 1px solid #999; }
415 div#activity dt.me .time { border-bottom: 1px solid #999; }
413 div#activity dt .time { color: #777; font-size: 80%; }
416 div#activity dt .time { color: #777; font-size: 80%; }
414 div#activity dd .description, #search-results dd .description { font-style: italic; }
417 div#activity dd .description, #search-results dd .description { font-style: italic; }
415 div#activity span.project:after, #search-results span.project:after { content: " -"; }
418 div#activity span.project:after, #search-results span.project:after { content: " -"; }
416 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
419 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
417 div#activity dt.grouped {margin-left:5em;}
420 div#activity dt.grouped {margin-left:5em;}
418 div#activity dd.grouped {margin-left:9em;}
421 div#activity dd.grouped {margin-left:9em;}
419
422
420 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
423 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
421
424
422 div#search-results-counts {float:right;}
425 div#search-results-counts {float:right;}
423 div#search-results-counts ul { margin-top: 0.5em; }
426 div#search-results-counts ul { margin-top: 0.5em; }
424 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
427 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
425
428
426 dt.issue { background-image: url(../images/ticket.png); }
429 dt.issue { background-image: url(../images/ticket.png); }
427 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
430 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
428 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
431 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
429 dt.issue-note { background-image: url(../images/ticket_note.png); }
432 dt.issue-note { background-image: url(../images/ticket_note.png); }
430 dt.changeset { background-image: url(../images/changeset.png); }
433 dt.changeset { background-image: url(../images/changeset.png); }
431 dt.news { background-image: url(../images/news.png); }
434 dt.news { background-image: url(../images/news.png); }
432 dt.message { background-image: url(../images/message.png); }
435 dt.message { background-image: url(../images/message.png); }
433 dt.reply { background-image: url(../images/comments.png); }
436 dt.reply { background-image: url(../images/comments.png); }
434 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
437 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
435 dt.attachment { background-image: url(../images/attachment.png); }
438 dt.attachment { background-image: url(../images/attachment.png); }
436 dt.document { background-image: url(../images/document.png); }
439 dt.document { background-image: url(../images/document.png); }
437 dt.project { background-image: url(../images/projects.png); }
440 dt.project { background-image: url(../images/projects.png); }
438 dt.time-entry { background-image: url(../images/time.png); }
441 dt.time-entry { background-image: url(../images/time.png); }
439
442
440 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
443 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
441
444
442 div#roadmap .related-issues { margin-bottom: 1em; }
445 div#roadmap .related-issues { margin-bottom: 1em; }
443 div#roadmap .related-issues td.checkbox { display: none; }
446 div#roadmap .related-issues td.checkbox { display: none; }
444 div#roadmap .wiki h1:first-child { display: none; }
447 div#roadmap .wiki h1:first-child { display: none; }
445 div#roadmap .wiki h1 { font-size: 120%; }
448 div#roadmap .wiki h1 { font-size: 120%; }
446 div#roadmap .wiki h2 { font-size: 110%; }
449 div#roadmap .wiki h2 { font-size: 110%; }
447 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
450 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
448
451
449 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
452 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
450 div#version-summary fieldset { margin-bottom: 1em; }
453 div#version-summary fieldset { margin-bottom: 1em; }
451 div#version-summary fieldset.time-tracking table { width:100%; }
454 div#version-summary fieldset.time-tracking table { width:100%; }
452 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
455 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
453
456
454 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
457 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
455 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
458 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
456 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
459 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
457 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
460 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
458 table#time-report .hours-dec { font-size: 0.9em; }
461 table#time-report .hours-dec { font-size: 0.9em; }
459
462
460 div.wiki-page .contextual a {opacity: 0.4}
463 div.wiki-page .contextual a {opacity: 0.4}
461 div.wiki-page .contextual a:hover {opacity: 1}
464 div.wiki-page .contextual a:hover {opacity: 1}
462
465
463 form .attributes select { width: 60%; }
466 form .attributes select { width: 60%; }
467 form .attributes select + a.icon-only { vertical-align: middle; margin-left: 4px; }
464 input#issue_subject, input#document_title { width: 99%; }
468 input#issue_subject, input#document_title { width: 99%; }
465 select#issue_done_ratio { width: 95px; }
469 select#issue_done_ratio { width: 95px; }
466
470
467 ul.projects {margin:0; padding-left:1em;}
471 ul.projects {margin:0; padding-left:1em;}
468 ul.projects ul {padding-left:1.6em;}
472 ul.projects ul {padding-left:1.6em;}
469 ul.projects.root {margin:0; padding:0;}
473 ul.projects.root {margin:0; padding:0;}
470 ul.projects li {list-style-type:none;}
474 ul.projects li {list-style-type:none;}
471
475
472 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
476 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
473 #projects-index ul.projects li.root {margin-bottom: 1em;}
477 #projects-index ul.projects li.root {margin-bottom: 1em;}
474 #projects-index ul.projects li.child {margin-top: 1em;}
478 #projects-index ul.projects li.child {margin-top: 1em;}
475 #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; }
479 #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; }
476 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
480 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
477
481
478 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
482 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
479
483
480 #related-issues li img {vertical-align:middle;}
484 #related-issues li img {vertical-align:middle;}
481
485
482 ul.properties {padding:0; font-size: 0.9em; color: #777;}
486 ul.properties {padding:0; font-size: 0.9em; color: #777;}
483 ul.properties li {list-style-type:none;}
487 ul.properties li {list-style-type:none;}
484 ul.properties li span {font-style:italic;}
488 ul.properties li span {font-style:italic;}
485
489
486 .total-hours { font-size: 110%; font-weight: bold; }
490 .total-hours { font-size: 110%; font-weight: bold; }
487 .total-hours span.hours-int { font-size: 120%; }
491 .total-hours span.hours-int { font-size: 120%; }
488
492
489 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
493 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
490 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
494 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
491
495
492 #workflow_copy_form select { width: 200px; }
496 #workflow_copy_form select { width: 200px; }
493 table.transitions td.enabled {background: #bfb;}
497 table.transitions td.enabled {background: #bfb;}
494 #workflow_form table select {font-size:90%; max-width:100px;}
498 #workflow_form table select {font-size:90%; max-width:100px;}
495 table.fields_permissions td.readonly {background:#ddd;}
499 table.fields_permissions td.readonly {background:#ddd;}
496 table.fields_permissions td.required {background:#d88;}
500 table.fields_permissions td.required {background:#d88;}
497
501
498 select.expandable {vertical-align:top;}
502 select.expandable {vertical-align:top;}
499
503
500 textarea#custom_field_possible_values {width: 95%; resize:vertical}
504 textarea#custom_field_possible_values {width: 95%; resize:vertical}
501 textarea#custom_field_default_value {width: 95%; resize:vertical}
505 textarea#custom_field_default_value {width: 95%; resize:vertical}
502 .sort-handle {display:inline-block; vertical-align:middle;}
506 .sort-handle {display:inline-block; vertical-align:middle;}
503
507
504 input#content_comments {width: 99%}
508 input#content_comments {width: 99%}
505
509
506 span.pagination {margin-left:3px; color:#888;}
510 span.pagination {margin-left:3px; color:#888;}
507 .pagination ul.pages {
511 .pagination ul.pages {
508 margin: 0 5px 0 0;
512 margin: 0 5px 0 0;
509 padding: 0;
513 padding: 0;
510 display: inline;
514 display: inline;
511 }
515 }
512 .pagination ul.pages li {
516 .pagination ul.pages li {
513 display: inline-block;
517 display: inline-block;
514 padding: 0;
518 padding: 0;
515 border: 1px solid #ccc;
519 border: 1px solid #ccc;
516 margin-left: -1px;
520 margin-left: -1px;
517 line-height: 2em;
521 line-height: 2em;
518 margin-bottom: 1em;
522 margin-bottom: 1em;
519 white-space: nowrap;
523 white-space: nowrap;
520 text-align: center;
524 text-align: center;
521 }
525 }
522 .pagination ul.pages li a,
526 .pagination ul.pages li a,
523 .pagination ul.pages li span {
527 .pagination ul.pages li span {
524 padding: 3px 8px;
528 padding: 3px 8px;
525 }
529 }
526 .pagination ul.pages li:first-child {
530 .pagination ul.pages li:first-child {
527 border-top-left-radius: 4px;
531 border-top-left-radius: 4px;
528 border-bottom-left-radius: 4px;
532 border-bottom-left-radius: 4px;
529 }
533 }
530 .pagination ul.pages li:last-child {
534 .pagination ul.pages li:last-child {
531 border-top-right-radius: 4px;
535 border-top-right-radius: 4px;
532 border-bottom-right-radius: 4px;
536 border-bottom-right-radius: 4px;
533 }
537 }
534 .pagination ul.pages li.current {
538 .pagination ul.pages li.current {
535 color: white;
539 color: white;
536 background-color: #628DB6;
540 background-color: #628DB6;
537 border-color: #628DB6;
541 border-color: #628DB6;
538 }
542 }
539 .pagination ul.pages li.page:hover {
543 .pagination ul.pages li.page:hover {
540 background-color: #EEE;
544 background-color: #EEE;
541 }
545 }
542 .pagination ul.pages li.page a:hover,
546 .pagination ul.pages li.page a:hover,
543 .pagination ul.pages li.page a:active {
547 .pagination ul.pages li.page a:active {
544 color: inherit;
548 color: inherit;
545 text-decoration: inherit;
549 text-decoration: inherit;
546 }
550 }
547 span.pagination>span {white-space:nowrap;}
551 span.pagination>span {white-space:nowrap;}
548
552
549 #search-form fieldset p {margin:0.2em 0;}
553 #search-form fieldset p {margin:0.2em 0;}
550
554
551 /***** Tabular forms ******/
555 /***** Tabular forms ******/
552 .tabular p{
556 .tabular p{
553 margin: 0;
557 margin: 0;
554 padding: 3px 0 3px 0;
558 padding: 3px 0 3px 0;
555 padding-left: 180px; /* width of left column containing the label elements */
559 padding-left: 180px; /* width of left column containing the label elements */
556 min-height: 1.8em;
560 min-height: 1.8em;
557 clear:left;
561 clear:left;
558 }
562 }
559
563
560 html>body .tabular p {overflow:hidden;}
564 html>body .tabular p {overflow:hidden;}
561
565
562 .tabular input, .tabular select {max-width:95%}
566 .tabular input, .tabular select {max-width:95%}
563 .tabular textarea {width:95%; resize:vertical;}
567 .tabular textarea {width:95%; resize:vertical;}
564
568
565 .tabular label{
569 .tabular label{
566 font-weight: bold;
570 font-weight: bold;
567 float: left;
571 float: left;
568 text-align: right;
572 text-align: right;
569 /* width of left column */
573 /* width of left column */
570 margin-left: -180px;
574 margin-left: -180px;
571 /* width of labels. Should be smaller than left column to create some right margin */
575 /* width of labels. Should be smaller than left column to create some right margin */
572 width: 175px;
576 width: 175px;
573 }
577 }
574
578
575 .tabular label.floating{
579 .tabular label.floating{
576 font-weight: normal;
580 font-weight: normal;
577 margin-left: 0px;
581 margin-left: 0px;
578 text-align: left;
582 text-align: left;
579 width: 270px;
583 width: 270px;
580 }
584 }
581
585
582 .tabular label.block{
586 .tabular label.block{
583 font-weight: normal;
587 font-weight: normal;
584 margin-left: 0px !important;
588 margin-left: 0px !important;
585 text-align: left;
589 text-align: left;
586 float: none;
590 float: none;
587 display: block;
591 display: block;
588 width: auto !important;
592 width: auto !important;
589 }
593 }
590
594
591 .tabular label.inline{
595 .tabular label.inline{
592 font-weight: normal;
596 font-weight: normal;
593 float:none;
597 float:none;
594 margin-left: 5px !important;
598 margin-left: 5px !important;
595 width: auto;
599 width: auto;
596 }
600 }
597
601
598 label.no-css {
602 label.no-css {
599 font-weight: inherit;
603 font-weight: inherit;
600 float:none;
604 float:none;
601 text-align:left;
605 text-align:left;
602 margin-left:0px;
606 margin-left:0px;
603 width:auto;
607 width:auto;
604 }
608 }
605 input#time_entry_comments { width: 90%;}
609 input#time_entry_comments { width: 90%;}
606
610
607 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
611 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
608
612
609 .tabular.settings p{ padding-left: 300px; }
613 .tabular.settings p{ padding-left: 300px; }
610 .tabular.settings label{ margin-left: -300px; width: 295px; }
614 .tabular.settings label{ margin-left: -300px; width: 295px; }
611 .tabular.settings textarea { width: 99%; }
615 .tabular.settings textarea { width: 99%; }
612
616
613 .settings.enabled_scm table {width:100%}
617 .settings.enabled_scm table {width:100%}
614 .settings.enabled_scm td.scm_name{ font-weight: bold; }
618 .settings.enabled_scm td.scm_name{ font-weight: bold; }
615
619
616 fieldset.settings label { display: block; }
620 fieldset.settings label { display: block; }
617 fieldset#notified_events .parent { padding-left: 20px; }
621 fieldset#notified_events .parent { padding-left: 20px; }
618
622
619 span.required {color: #bb0000;}
623 span.required {color: #bb0000;}
620 .summary {font-style: italic;}
624 .summary {font-style: italic;}
621
625
622 .check_box_group {
626 .check_box_group {
623 display:block;
627 display:block;
624 width:95%;
628 width:95%;
625 max-height:300px;
629 max-height:300px;
626 overflow-y:auto;
630 overflow-y:auto;
627 padding:2px 4px 4px 2px;
631 padding:2px 4px 4px 2px;
628 background:#fff;
632 background:#fff;
629 border:1px solid #9EB1C2;
633 border:1px solid #9EB1C2;
630 border-radius:2px
634 border-radius:2px
631 }
635 }
632 .check_box_group label {
636 .check_box_group label {
633 font-weight: normal;
637 font-weight: normal;
634 margin-left: 0px !important;
638 margin-left: 0px !important;
635 text-align: left;
639 text-align: left;
636 float: none;
640 float: none;
637 display: block;
641 display: block;
638 width: auto;
642 width: auto;
639 }
643 }
640 .check_box_group.bool_cf {border:0; background:inherit;}
644 .check_box_group.bool_cf {border:0; background:inherit;}
641 .check_box_group.bool_cf label {display: inline;}
645 .check_box_group.bool_cf label {display: inline;}
642
646
643 #attachments_fields input.description {margin-left:4px; width:340px;}
647 #attachments_fields input.description {margin-left:4px; width:340px;}
644 #attachments_fields span {display:block; white-space:nowrap;}
648 #attachments_fields span {display:block; white-space:nowrap;}
645 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
649 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
646 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
650 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
647 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
651 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
648 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
652 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
649 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
653 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
650 a.remove-upload:hover {text-decoration:none !important;}
654 a.remove-upload:hover {text-decoration:none !important;}
651
655
652 div.fileover { background-color: lavender; }
656 div.fileover { background-color: lavender; }
653
657
654 div.attachments { margin-top: 12px; }
658 div.attachments { margin-top: 12px; }
655 div.attachments p { margin:4px 0 2px 0; }
659 div.attachments p { margin:4px 0 2px 0; }
656 div.attachments img { vertical-align: middle; }
660 div.attachments img { vertical-align: middle; }
657 div.attachments span.author { font-size: 0.9em; color: #888; }
661 div.attachments span.author { font-size: 0.9em; color: #888; }
658
662
659 div.thumbnails {margin-top:0.6em;}
663 div.thumbnails {margin-top:0.6em;}
660 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
664 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
661 div.thumbnails img {margin: 3px; vertical-align: middle;}
665 div.thumbnails img {margin: 3px; vertical-align: middle;}
662 #history div.thumbnails {margin-left: 2em;}
666 #history div.thumbnails {margin-left: 2em;}
663
667
664 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
668 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
665 .other-formats span + span:before { content: "| "; }
669 .other-formats span + span:before { content: "| "; }
666
670
667 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
671 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
668
672
669 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
673 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
670 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
674 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
671
675
672 textarea.text_cf {width:95%; resize:vertical;}
676 textarea.text_cf {width:95%; resize:vertical;}
673 input.string_cf, input.link_cf {width:95%;}
677 input.string_cf, input.link_cf {width:95%;}
674 select.bool_cf {width:auto !important;}
678 select.bool_cf {width:auto !important;}
675
679
676 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
680 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
677
681
678 #tab-content-users .splitcontentleft {width: 64%;}
682 #tab-content-users .splitcontentleft {width: 64%;}
679 #tab-content-users .splitcontentright {width: 34%;}
683 #tab-content-users .splitcontentright {width: 34%;}
680 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
684 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
681 #tab-content-users fieldset legend {font-weight: bold;}
685 #tab-content-users fieldset legend {font-weight: bold;}
682 #tab-content-users fieldset label {display: block;}
686 #tab-content-users fieldset label {display: block;}
683 #tab-content-users #principals {max-height: 400px; overflow: auto;}
687 #tab-content-users #principals {max-height: 400px; overflow: auto;}
684
688
685 #users_for_watcher {height: 200px; overflow:auto;}
689 #users_for_watcher {height: 200px; overflow:auto;}
686 #users_for_watcher label {display: block;}
690 #users_for_watcher label {display: block;}
687
691
688 table.members td.name {padding-left: 20px;}
692 table.members td.name {padding-left: 20px;}
689 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
693 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
690
694
691 input#principal_search, input#user_search {width:90%}
695 input#principal_search, input#user_search {width:90%}
692 .roles-selection label {display:inline-block; width:210px;}
696 .roles-selection label {display:inline-block; width:210px;}
693
697
694 input.autocomplete {
698 input.autocomplete {
695 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
699 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
696 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
700 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
697 }
701 }
698 input.autocomplete.ajax-loading {
702 input.autocomplete.ajax-loading {
699 background-image: url(../images/loading.gif);
703 background-image: url(../images/loading.gif);
700 }
704 }
701
705
702 .role-visibility {padding-left:2em;}
706 .role-visibility {padding-left:2em;}
703
707
704 .objects-selection {
708 .objects-selection {
705 height: 300px;
709 height: 300px;
706 overflow: auto;
710 overflow: auto;
707 margin-bottom: 1em;
711 margin-bottom: 1em;
708 }
712 }
709
713
710 .objects-selection label {
714 .objects-selection label {
711 display: block;
715 display: block;
712 }
716 }
713
717
714 .objects-selection>div {
718 .objects-selection>div {
715 column-count: auto;
719 column-count: auto;
716 column-width: 200px;
720 column-width: 200px;
717 -webkit-column-count: auto;
721 -webkit-column-count: auto;
718 -webkit-column-width: 200px;
722 -webkit-column-width: 200px;
719 -webkit-column-gap : 0.5rem;
723 -webkit-column-gap : 0.5rem;
720 -webkit-column-rule: 1px solid #ccc;
724 -webkit-column-rule: 1px solid #ccc;
721 -moz-column-count: auto;
725 -moz-column-count: auto;
722 -moz-column-width: 200px;
726 -moz-column-width: 200px;
723 -moz-column-gap : 0.5rem;
727 -moz-column-gap : 0.5rem;
724 -moz-column-rule: 1px solid #ccc;
728 -moz-column-rule: 1px solid #ccc;
725 }
729 }
726
730
727 /***** Flash & error messages ****/
731 /***** Flash & error messages ****/
728 #errorExplanation, div.flash, .nodata, .warning, .conflict {
732 #errorExplanation, div.flash, .nodata, .warning, .conflict {
729 padding: 4px 4px 4px 30px;
733 padding: 4px 4px 4px 30px;
730 margin-bottom: 12px;
734 margin-bottom: 12px;
731 font-size: 1.1em;
735 font-size: 1.1em;
732 border: 2px solid;
736 border: 2px solid;
733 border-radius: 3px;
737 border-radius: 3px;
734 }
738 }
735
739
736 div.flash {margin-top: 8px;}
740 div.flash {margin-top: 8px;}
737
741
738 div.flash.error, #errorExplanation {
742 div.flash.error, #errorExplanation {
739 background: url(../images/exclamation.png) 8px 50% no-repeat;
743 background: url(../images/exclamation.png) 8px 50% no-repeat;
740 background-color: #ffe3e3;
744 background-color: #ffe3e3;
741 border-color: #dd0000;
745 border-color: #dd0000;
742 color: #880000;
746 color: #880000;
743 }
747 }
744
748
745 div.flash.notice {
749 div.flash.notice {
746 background: url(../images/true.png) 8px 5px no-repeat;
750 background: url(../images/true.png) 8px 5px no-repeat;
747 background-color: #dfffdf;
751 background-color: #dfffdf;
748 border-color: #9fcf9f;
752 border-color: #9fcf9f;
749 color: #005f00;
753 color: #005f00;
750 }
754 }
751
755
752 div.flash.warning, .conflict {
756 div.flash.warning, .conflict {
753 background: url(../images/warning.png) 8px 5px no-repeat;
757 background: url(../images/warning.png) 8px 5px no-repeat;
754 background-color: #FFEBC1;
758 background-color: #FFEBC1;
755 border-color: #FDBF3B;
759 border-color: #FDBF3B;
756 color: #A6750C;
760 color: #A6750C;
757 text-align: left;
761 text-align: left;
758 }
762 }
759
763
760 .nodata, .warning {
764 .nodata, .warning {
761 text-align: center;
765 text-align: center;
762 background-color: #FFEBC1;
766 background-color: #FFEBC1;
763 border-color: #FDBF3B;
767 border-color: #FDBF3B;
764 color: #A6750C;
768 color: #A6750C;
765 }
769 }
766
770
767 #errorExplanation ul { font-size: 0.9em;}
771 #errorExplanation ul { font-size: 0.9em;}
768 #errorExplanation h2, #errorExplanation p { display: none; }
772 #errorExplanation h2, #errorExplanation p { display: none; }
769
773
770 .conflict-details {font-size:80%;}
774 .conflict-details {font-size:80%;}
771
775
772 /***** Ajax indicator ******/
776 /***** Ajax indicator ******/
773 #ajax-indicator {
777 #ajax-indicator {
774 position: absolute; /* fixed not supported by IE */
778 position: absolute; /* fixed not supported by IE */
775 background-color:#eee;
779 background-color:#eee;
776 border: 1px solid #bbb;
780 border: 1px solid #bbb;
777 top:35%;
781 top:35%;
778 left:40%;
782 left:40%;
779 width:20%;
783 width:20%;
780 font-weight:bold;
784 font-weight:bold;
781 text-align:center;
785 text-align:center;
782 padding:0.6em;
786 padding:0.6em;
783 z-index:100;
787 z-index:100;
784 opacity: 0.5;
788 opacity: 0.5;
785 }
789 }
786
790
787 html>body #ajax-indicator { position: fixed; }
791 html>body #ajax-indicator { position: fixed; }
788
792
789 #ajax-indicator span {
793 #ajax-indicator span {
790 background-position: 0% 40%;
794 background-position: 0% 40%;
791 background-repeat: no-repeat;
795 background-repeat: no-repeat;
792 background-image: url(../images/loading.gif);
796 background-image: url(../images/loading.gif);
793 padding-left: 26px;
797 padding-left: 26px;
794 vertical-align: bottom;
798 vertical-align: bottom;
795 }
799 }
796
800
797 /***** Calendar *****/
801 /***** Calendar *****/
798 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
802 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
799 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
803 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
800 table.cal thead th.week-number {width: auto;}
804 table.cal thead th.week-number {width: auto;}
801 table.cal tbody tr {height: 100px;}
805 table.cal tbody tr {height: 100px;}
802 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
806 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
803 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
807 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
804 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
808 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
805 table.cal td.odd p.day-num {color: #bbb;}
809 table.cal td.odd p.day-num {color: #bbb;}
806 table.cal td.today {background:#ffffdd;}
810 table.cal td.today {background:#ffffdd;}
807 table.cal td.today p.day-num {font-weight: bold;}
811 table.cal td.today p.day-num {font-weight: bold;}
808 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
812 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
809 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
813 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
810 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
814 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
811 p.cal.legend span {display:block;}
815 p.cal.legend span {display:block;}
812
816
813 /***** Tooltips ******/
817 /***** Tooltips ******/
814 .tooltip{position:relative;z-index:24;}
818 .tooltip{position:relative;z-index:24;}
815 .tooltip:hover{z-index:25;color:#000;}
819 .tooltip:hover{z-index:25;color:#000;}
816 .tooltip span.tip{display: none; text-align:left;}
820 .tooltip span.tip{display: none; text-align:left;}
817
821
818 div.tooltip:hover span.tip{
822 div.tooltip:hover span.tip{
819 display:block;
823 display:block;
820 position:absolute;
824 position:absolute;
821 top:12px; width:270px;
825 top:12px; width:270px;
822 border:1px solid #555;
826 border:1px solid #555;
823 background-color:#fff;
827 background-color:#fff;
824 padding: 4px;
828 padding: 4px;
825 font-size: 0.8em;
829 font-size: 0.8em;
826 color:#505050;
830 color:#505050;
827 }
831 }
828
832
829 img.ui-datepicker-trigger {
833 img.ui-datepicker-trigger {
830 cursor: pointer;
834 cursor: pointer;
831 vertical-align: middle;
835 vertical-align: middle;
832 margin-left: 4px;
836 margin-left: 4px;
833 }
837 }
834
838
835 /***** Progress bar *****/
839 /***** Progress bar *****/
836 table.progress {
840 table.progress {
837 border-collapse: collapse;
841 border-collapse: collapse;
838 border-spacing: 0pt;
842 border-spacing: 0pt;
839 empty-cells: show;
843 empty-cells: show;
840 text-align: center;
844 text-align: center;
841 float:left;
845 float:left;
842 margin: 1px 6px 1px 0px;
846 margin: 1px 6px 1px 0px;
843 }
847 }
844
848
845 table.progress {width:80px;}
849 table.progress {width:80px;}
846 table.progress td { height: 1em; }
850 table.progress td { height: 1em; }
847 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
851 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
848 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
852 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
849 table.progress td.todo { background: #eee none repeat scroll 0%; }
853 table.progress td.todo { background: #eee none repeat scroll 0%; }
850 p.percent {font-size: 80%; margin:0;}
854 p.percent {font-size: 80%; margin:0;}
851 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
855 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
852
856
853 .version-overview table.progress {width:40em;}
857 .version-overview table.progress {width:40em;}
854 .version-overview table.progress td { height: 1.2em; }
858 .version-overview table.progress td { height: 1.2em; }
855
859
856 /***** Tabs *****/
860 /***** Tabs *****/
857 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
861 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
858 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
862 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
859 #content .tabs ul li {
863 #content .tabs ul li {
860 float:left;
864 float:left;
861 list-style-type:none;
865 list-style-type:none;
862 white-space:nowrap;
866 white-space:nowrap;
863 margin-right:4px;
867 margin-right:4px;
864 background:#fff;
868 background:#fff;
865 position:relative;
869 position:relative;
866 margin-bottom:-1px;
870 margin-bottom:-1px;
867 }
871 }
868 #content .tabs ul li a{
872 #content .tabs ul li a{
869 display:block;
873 display:block;
870 font-size: 0.9em;
874 font-size: 0.9em;
871 text-decoration:none;
875 text-decoration:none;
872 line-height:1.3em;
876 line-height:1.3em;
873 padding:4px 6px 4px 6px;
877 padding:4px 6px 4px 6px;
874 border: 1px solid #ccc;
878 border: 1px solid #ccc;
875 border-bottom: 1px solid #bbbbbb;
879 border-bottom: 1px solid #bbbbbb;
876 background-color: #f6f6f6;
880 background-color: #f6f6f6;
877 color:#999;
881 color:#999;
878 font-weight:bold;
882 font-weight:bold;
879 border-top-left-radius:3px;
883 border-top-left-radius:3px;
880 border-top-right-radius:3px;
884 border-top-right-radius:3px;
881 }
885 }
882
886
883 #content .tabs ul li a:hover {
887 #content .tabs ul li a:hover {
884 background-color: #ffffdd;
888 background-color: #ffffdd;
885 text-decoration:none;
889 text-decoration:none;
886 }
890 }
887
891
888 #content .tabs ul li a.selected {
892 #content .tabs ul li a.selected {
889 background-color: #fff;
893 background-color: #fff;
890 border: 1px solid #bbbbbb;
894 border: 1px solid #bbbbbb;
891 border-bottom: 1px solid #fff;
895 border-bottom: 1px solid #fff;
892 color:#444;
896 color:#444;
893 }
897 }
894
898
895 #content .tabs ul li a.selected:hover {background-color: #fff;}
899 #content .tabs ul li a.selected:hover {background-color: #fff;}
896
900
897 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
901 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
898
902
899 button.tab-left, button.tab-right {
903 button.tab-left, button.tab-right {
900 font-size: 0.9em;
904 font-size: 0.9em;
901 cursor: pointer;
905 cursor: pointer;
902 height:24px;
906 height:24px;
903 border: 1px solid #ccc;
907 border: 1px solid #ccc;
904 border-bottom: 1px solid #bbbbbb;
908 border-bottom: 1px solid #bbbbbb;
905 position:absolute;
909 position:absolute;
906 padding:4px;
910 padding:4px;
907 width: 20px;
911 width: 20px;
908 bottom: -1px;
912 bottom: -1px;
909 }
913 }
910
914
911 button.tab-left {
915 button.tab-left {
912 right: 20px;
916 right: 20px;
913 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
917 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
914 border-top-left-radius:3px;
918 border-top-left-radius:3px;
915 }
919 }
916
920
917 button.tab-right {
921 button.tab-right {
918 right: 0;
922 right: 0;
919 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
923 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
920 border-top-right-radius:3px;
924 border-top-right-radius:3px;
921 }
925 }
922
926
923 /***** Diff *****/
927 /***** Diff *****/
924 .diff_out { background: #fcc; }
928 .diff_out { background: #fcc; }
925 .diff_out span { background: #faa; }
929 .diff_out span { background: #faa; }
926 .diff_in { background: #cfc; }
930 .diff_in { background: #cfc; }
927 .diff_in span { background: #afa; }
931 .diff_in span { background: #afa; }
928
932
929 .text-diff {
933 .text-diff {
930 padding: 1em;
934 padding: 1em;
931 background-color:#f6f6f6;
935 background-color:#f6f6f6;
932 color:#505050;
936 color:#505050;
933 border: 1px solid #e4e4e4;
937 border: 1px solid #e4e4e4;
934 }
938 }
935
939
936 /***** Wiki *****/
940 /***** Wiki *****/
937 div.wiki table {
941 div.wiki table {
938 border-collapse: collapse;
942 border-collapse: collapse;
939 margin-bottom: 1em;
943 margin-bottom: 1em;
940 }
944 }
941
945
942 div.wiki table, div.wiki td, div.wiki th {
946 div.wiki table, div.wiki td, div.wiki th {
943 border: 1px solid #bbb;
947 border: 1px solid #bbb;
944 padding: 4px;
948 padding: 4px;
945 }
949 }
946
950
947 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
951 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
948
952
949 div.wiki .external {
953 div.wiki .external {
950 background-position: 0% 60%;
954 background-position: 0% 60%;
951 background-repeat: no-repeat;
955 background-repeat: no-repeat;
952 padding-left: 12px;
956 padding-left: 12px;
953 background-image: url(../images/external.png);
957 background-image: url(../images/external.png);
954 }
958 }
955
959
956 div.wiki a {word-wrap: break-word;}
960 div.wiki a {word-wrap: break-word;}
957 div.wiki a.new {color: #b73535;}
961 div.wiki a.new {color: #b73535;}
958
962
959 div.wiki ul, div.wiki ol {margin-bottom:1em;}
963 div.wiki ul, div.wiki ol {margin-bottom:1em;}
960 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
964 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
961
965
962 div.wiki pre {
966 div.wiki pre {
963 margin: 1em 1em 1em 1.6em;
967 margin: 1em 1em 1em 1.6em;
964 padding: 8px;
968 padding: 8px;
965 background-color: #fafafa;
969 background-color: #fafafa;
966 border: 1px solid #e2e2e2;
970 border: 1px solid #e2e2e2;
967 border-radius: 3px;
971 border-radius: 3px;
968 width:auto;
972 width:auto;
969 overflow-x: auto;
973 overflow-x: auto;
970 overflow-y: hidden;
974 overflow-y: hidden;
971 }
975 }
972
976
973 div.wiki ul.toc {
977 div.wiki ul.toc {
974 background-color: #ffffdd;
978 background-color: #ffffdd;
975 border: 1px solid #e4e4e4;
979 border: 1px solid #e4e4e4;
976 padding: 4px;
980 padding: 4px;
977 line-height: 1.2em;
981 line-height: 1.2em;
978 margin-bottom: 12px;
982 margin-bottom: 12px;
979 margin-right: 12px;
983 margin-right: 12px;
980 margin-left: 0;
984 margin-left: 0;
981 display: table
985 display: table
982 }
986 }
983 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
987 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
984
988
985 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
989 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
986 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
990 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
987 div.wiki ul.toc ul { margin: 0; padding: 0; }
991 div.wiki ul.toc ul { margin: 0; padding: 0; }
988 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
992 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
989 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
993 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
990 div.wiki ul.toc a {
994 div.wiki ul.toc a {
991 font-size: 0.9em;
995 font-size: 0.9em;
992 font-weight: normal;
996 font-weight: normal;
993 text-decoration: none;
997 text-decoration: none;
994 color: #606060;
998 color: #606060;
995 }
999 }
996 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
1000 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
997
1001
998 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
1002 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
999 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
1003 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
1000 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
1004 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
1001
1005
1002 div.wiki img {vertical-align:middle; max-width:100%;}
1006 div.wiki img {vertical-align:middle; max-width:100%;}
1003
1007
1004 /***** My page layout *****/
1008 /***** My page layout *****/
1005 .block-receiver {
1009 .block-receiver {
1006 border:1px dashed #c0c0c0;
1010 border:1px dashed #c0c0c0;
1007 margin-bottom: 20px;
1011 margin-bottom: 20px;
1008 padding: 15px 0 15px 0;
1012 padding: 15px 0 15px 0;
1009 }
1013 }
1010
1014
1011 .mypage-box {
1015 .mypage-box {
1012 margin:0 0 20px 0;
1016 margin:0 0 20px 0;
1013 color:#505050;
1017 color:#505050;
1014 line-height:1.5em;
1018 line-height:1.5em;
1015 }
1019 }
1016
1020
1017 .handle {cursor: move;}
1021 .handle {cursor: move;}
1018
1022
1019 a.close-icon {
1023 a.close-icon {
1020 display:block;
1024 display:block;
1021 margin-top:3px;
1025 margin-top:3px;
1022 overflow:hidden;
1026 overflow:hidden;
1023 width:12px;
1027 width:12px;
1024 height:12px;
1028 height:12px;
1025 background-repeat: no-repeat;
1029 background-repeat: no-repeat;
1026 cursor:pointer;
1030 cursor:pointer;
1027 background-image:url('../images/close.png');
1031 background-image:url('../images/close.png');
1028 }
1032 }
1029 a.close-icon:hover {background-image:url('../images/close_hl.png');}
1033 a.close-icon:hover {background-image:url('../images/close_hl.png');}
1030
1034
1031 /***** Gantt chart *****/
1035 /***** Gantt chart *****/
1032 .gantt_hdr {
1036 .gantt_hdr {
1033 position:absolute;
1037 position:absolute;
1034 top:0;
1038 top:0;
1035 height:16px;
1039 height:16px;
1036 border-top: 1px solid #c0c0c0;
1040 border-top: 1px solid #c0c0c0;
1037 border-bottom: 1px solid #c0c0c0;
1041 border-bottom: 1px solid #c0c0c0;
1038 border-right: 1px solid #c0c0c0;
1042 border-right: 1px solid #c0c0c0;
1039 text-align: center;
1043 text-align: center;
1040 overflow: hidden;
1044 overflow: hidden;
1041 }
1045 }
1042
1046
1043 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
1047 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
1044
1048
1045 .gantt_subjects { font-size: 0.8em; }
1049 .gantt_subjects { font-size: 0.8em; }
1046 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
1050 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
1047
1051
1048 .task {
1052 .task {
1049 position: absolute;
1053 position: absolute;
1050 height:8px;
1054 height:8px;
1051 font-size:0.8em;
1055 font-size:0.8em;
1052 color:#888;
1056 color:#888;
1053 padding:0;
1057 padding:0;
1054 margin:0;
1058 margin:0;
1055 line-height:16px;
1059 line-height:16px;
1056 white-space:nowrap;
1060 white-space:nowrap;
1057 }
1061 }
1058
1062
1059 .task.label {width:100%;}
1063 .task.label {width:100%;}
1060 .task.label.project, .task.label.version { font-weight: bold; }
1064 .task.label.project, .task.label.version { font-weight: bold; }
1061
1065
1062 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1066 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1063 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1067 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1064 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1068 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1065
1069
1066 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1070 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1067 .task_late.parent, .task_done.parent { height: 3px;}
1071 .task_late.parent, .task_done.parent { height: 3px;}
1068 .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;}
1072 .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;}
1069 .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;}
1073 .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;}
1070
1074
1071 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1075 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1072 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1076 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1073 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1077 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1074 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1078 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1075
1079
1076 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1080 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1077 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1081 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1078 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1082 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1079 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1083 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1080
1084
1081 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1085 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1082 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1086 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1083
1087
1084 /***** Icons *****/
1088 /***** Icons *****/
1085 .icon {
1089 .icon {
1086 background-position: 0% 50%;
1090 background-position: 0% 50%;
1087 background-repeat: no-repeat;
1091 background-repeat: no-repeat;
1088 padding-left: 20px;
1092 padding-left: 20px;
1089 padding-top: 2px;
1093 padding-top: 2px;
1090 padding-bottom: 3px;
1094 padding-bottom: 3px;
1091 }
1095 }
1096 .icon-only {
1097 background-position: 0% 50%;
1098 background-repeat: no-repeat;
1099 padding-left: 16px;
1100 }
1092
1101
1093 .icon-add { background-image: url(../images/add.png); }
1102 .icon-add { background-image: url(../images/add.png); }
1094 .icon-edit { background-image: url(../images/edit.png); }
1103 .icon-edit { background-image: url(../images/edit.png); }
1095 .icon-copy { background-image: url(../images/copy.png); }
1104 .icon-copy { background-image: url(../images/copy.png); }
1096 .icon-duplicate { background-image: url(../images/duplicate.png); }
1105 .icon-duplicate { background-image: url(../images/duplicate.png); }
1097 .icon-del { background-image: url(../images/delete.png); }
1106 .icon-del { background-image: url(../images/delete.png); }
1098 .icon-move { background-image: url(../images/move.png); }
1107 .icon-move { background-image: url(../images/move.png); }
1099 .icon-save { background-image: url(../images/save.png); }
1108 .icon-save { background-image: url(../images/save.png); }
1100 .icon-cancel { background-image: url(../images/cancel.png); }
1109 .icon-cancel { background-image: url(../images/cancel.png); }
1101 .icon-multiple { background-image: url(../images/table_multiple.png); }
1110 .icon-multiple { background-image: url(../images/table_multiple.png); }
1102 .icon-folder { background-image: url(../images/folder.png); }
1111 .icon-folder { background-image: url(../images/folder.png); }
1103 .open .icon-folder { background-image: url(../images/folder_open.png); }
1112 .open .icon-folder { background-image: url(../images/folder_open.png); }
1104 .icon-package { background-image: url(../images/package.png); }
1113 .icon-package { background-image: url(../images/package.png); }
1105 .icon-user { background-image: url(../images/user.png); }
1114 .icon-user { background-image: url(../images/user.png); }
1106 .icon-projects { background-image: url(../images/projects.png); }
1115 .icon-projects { background-image: url(../images/projects.png); }
1107 .icon-help { background-image: url(../images/help.png); }
1116 .icon-help { background-image: url(../images/help.png); }
1108 .icon-attachment { background-image: url(../images/attachment.png); }
1117 .icon-attachment { background-image: url(../images/attachment.png); }
1109 .icon-history { background-image: url(../images/history.png); }
1118 .icon-history { background-image: url(../images/history.png); }
1110 .icon-time { background-image: url(../images/time.png); }
1119 .icon-time { background-image: url(../images/time.png); }
1111 .icon-time-add { background-image: url(../images/time_add.png); }
1120 .icon-time-add { background-image: url(../images/time_add.png); }
1112 .icon-stats { background-image: url(../images/stats.png); }
1121 .icon-stats { background-image: url(../images/stats.png); }
1113 .icon-warning { background-image: url(../images/warning.png); }
1122 .icon-warning { background-image: url(../images/warning.png); }
1123 .icon-error { background-image: url(../images/exclamation.png); }
1114 .icon-fav { background-image: url(../images/fav.png); }
1124 .icon-fav { background-image: url(../images/fav.png); }
1115 .icon-fav-off { background-image: url(../images/fav_off.png); }
1125 .icon-fav-off { background-image: url(../images/fav_off.png); }
1116 .icon-reload { background-image: url(../images/reload.png); }
1126 .icon-reload { background-image: url(../images/reload.png); }
1117 .icon-lock { background-image: url(../images/locked.png); }
1127 .icon-lock { background-image: url(../images/locked.png); }
1118 .icon-unlock { background-image: url(../images/unlock.png); }
1128 .icon-unlock { background-image: url(../images/unlock.png); }
1119 .icon-checked { background-image: url(../images/true.png); }
1129 .icon-checked { background-image: url(../images/toggle_check.png); }
1120 .icon-details { background-image: url(../images/zoom_in.png); }
1130 .icon-details { background-image: url(../images/zoom_in.png); }
1121 .icon-report { background-image: url(../images/report.png); }
1131 .icon-report { background-image: url(../images/report.png); }
1122 .icon-comment { background-image: url(../images/comment.png); }
1132 .icon-comment { background-image: url(../images/comment.png); }
1123 .icon-summary { background-image: url(../images/lightning.png); }
1133 .icon-summary { background-image: url(../images/lightning.png); }
1124 .icon-server-authentication { background-image: url(../images/server_key.png); }
1134 .icon-server-authentication { background-image: url(../images/server_key.png); }
1125 .icon-issue { background-image: url(../images/ticket.png); }
1135 .icon-issue { background-image: url(../images/ticket.png); }
1126 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1136 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1127 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1137 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1138 .icon-magnifier { background-image: url(../images/magnifier.png); }
1128 .icon-passwd { background-image: url(../images/textfield_key.png); }
1139 .icon-passwd { background-image: url(../images/textfield_key.png); }
1129 .icon-test { background-image: url(../images/bullet_go.png); }
1140 .icon-test { background-image: url(../images/bullet_go.png); }
1141 .icon-email { background-image: url(../images/email.png); }
1142 .icon-email-disabled { background-image: url(../images/email_disabled.png); }
1130 .icon-email-add { background-image: url(../images/email_add.png); }
1143 .icon-email-add { background-image: url(../images/email_add.png); }
1144 .icon-move-up { background-image: url(../images/1uparrow.png); }
1145 .icon-move-top { background-image: url(../images/2uparrow.png); }
1146 .icon-move-down { background-image: url(../images/1downarrow.png); }
1147 .icon-move-bottom { background-image: url(../images/2downarrow.png); }
1148 .icon-ok { background-image: url(../images/true.png); }
1149 .icon-not-ok { background-image: url(../images/false.png); }
1150 .icon-link-break { background-image: url(../images/link_break.png); }
1131
1151
1132 .icon-file { background-image: url(../images/files/default.png); }
1152 .icon-file { background-image: url(../images/files/default.png); }
1133 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1153 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1134 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1154 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1135 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1155 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1136 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1156 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1137 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1157 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1138 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1158 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1139 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1159 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1140 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1160 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1141 .icon-file.text-css { background-image: url(../images/files/css.png); }
1161 .icon-file.text-css { background-image: url(../images/files/css.png); }
1142 .icon-file.text-html { background-image: url(../images/files/html.png); }
1162 .icon-file.text-html { background-image: url(../images/files/html.png); }
1143 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1163 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1144 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1164 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1145 .icon-file.image-png { background-image: url(../images/files/image.png); }
1165 .icon-file.image-png { background-image: url(../images/files/image.png); }
1146 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1166 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1147 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1167 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1148 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1168 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1149 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1169 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1150
1170
1151 img.gravatar {
1171 img.gravatar {
1152 padding: 2px;
1172 padding: 2px;
1153 border: solid 1px #d5d5d5;
1173 border: solid 1px #d5d5d5;
1154 background: #fff;
1174 background: #fff;
1155 vertical-align: middle;
1175 vertical-align: middle;
1156 }
1176 }
1157
1177
1158 div.issue img.gravatar {
1178 div.issue img.gravatar {
1159 float: left;
1179 float: left;
1160 margin: 0 6px 0 0;
1180 margin: 0 6px 0 0;
1161 padding: 5px;
1181 padding: 5px;
1162 }
1182 }
1163
1183
1164 div.issue .attributes img.gravatar {
1184 div.issue .attributes img.gravatar {
1165 height: 14px;
1185 height: 14px;
1166 width: 14px;
1186 width: 14px;
1167 padding: 2px;
1187 padding: 2px;
1168 float: left;
1188 float: left;
1169 margin: 0 0.5em 0 0;
1189 margin: 0 0.5em 0 0;
1170 }
1190 }
1171
1191
1172 h2 img.gravatar {margin: -2px 4px -4px 0;}
1192 h2 img.gravatar {margin: -2px 4px -4px 0;}
1173 h3 img.gravatar {margin: -4px 4px -4px 0;}
1193 h3 img.gravatar {margin: -4px 4px -4px 0;}
1174 h4 img.gravatar {margin: -6px 4px -4px 0;}
1194 h4 img.gravatar {margin: -6px 4px -4px 0;}
1175 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1195 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1176 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1196 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1177 /* Used on 12px Gravatar img tags without the icon background */
1197 /* Used on 12px Gravatar img tags without the icon background */
1178 .icon-gravatar {float: left; margin-right: 4px;}
1198 .icon-gravatar {float: left; margin-right: 4px;}
1179
1199
1180 #activity dt, .journal {clear: left;}
1200 #activity dt, .journal {clear: left;}
1181
1201
1182 .journal-link {float: right;}
1202 .journal-link {float: right;}
1183
1203
1184 h2 img { vertical-align:middle; }
1204 h2 img { vertical-align:middle; }
1185
1205
1186 .hascontextmenu { cursor: context-menu; }
1206 .hascontextmenu { cursor: context-menu; }
1187
1207
1188 .sample-data {border:1px solid #ccc; border-collapse:collapse; background-color:#fff; margin:0.5em;}
1208 .sample-data {border:1px solid #ccc; border-collapse:collapse; background-color:#fff; margin:0.5em;}
1189 .sample-data td {border:1px solid #ccc; padding: 2px 4px; font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
1209 .sample-data td {border:1px solid #ccc; padding: 2px 4px; font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
1190 .sample-data tr:first-child td {font-weight:bold; text-align:center;}
1210 .sample-data tr:first-child td {font-weight:bold; text-align:center;}
1191
1211
1192 .ui-progressbar {position: relative;}
1212 .ui-progressbar {position: relative;}
1193 #progress-label {
1213 #progress-label {
1194 position: absolute; left: 50%; top: 4px;
1214 position: absolute; left: 50%; top: 4px;
1195 font-weight: bold;
1215 font-weight: bold;
1196 color: #555; text-shadow: 1px 1px 0 #fff;
1216 color: #555; text-shadow: 1px 1px 0 #fff;
1197 }
1217 }
1198
1218
1199 /* Custom JQuery styles */
1219 /* Custom JQuery styles */
1200 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1220 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1201
1221
1202
1222
1203 /************* CodeRay styles *************/
1223 /************* CodeRay styles *************/
1204 .syntaxhl div {display: inline;}
1224 .syntaxhl div {display: inline;}
1205 .syntaxhl .code pre { overflow: auto }
1225 .syntaxhl .code pre { overflow: auto }
1206
1226
1207 .syntaxhl .annotation { color:#007 }
1227 .syntaxhl .annotation { color:#007 }
1208 .syntaxhl .attribute-name { color:#b48 }
1228 .syntaxhl .attribute-name { color:#b48 }
1209 .syntaxhl .attribute-value { color:#700 }
1229 .syntaxhl .attribute-value { color:#700 }
1210 .syntaxhl .binary { color:#549 }
1230 .syntaxhl .binary { color:#549 }
1211 .syntaxhl .binary .char { color:#325 }
1231 .syntaxhl .binary .char { color:#325 }
1212 .syntaxhl .binary .delimiter { color:#325 }
1232 .syntaxhl .binary .delimiter { color:#325 }
1213 .syntaxhl .char { color:#D20 }
1233 .syntaxhl .char { color:#D20 }
1214 .syntaxhl .char .content { color:#D20 }
1234 .syntaxhl .char .content { color:#D20 }
1215 .syntaxhl .char .delimiter { color:#710 }
1235 .syntaxhl .char .delimiter { color:#710 }
1216 .syntaxhl .class { color:#258; font-weight:bold }
1236 .syntaxhl .class { color:#258; font-weight:bold }
1217 .syntaxhl .class-variable { color:#369 }
1237 .syntaxhl .class-variable { color:#369 }
1218 .syntaxhl .color { color:#0A0 }
1238 .syntaxhl .color { color:#0A0 }
1219 .syntaxhl .comment { color:#385 }
1239 .syntaxhl .comment { color:#385 }
1220 .syntaxhl .comment .char { color:#385 }
1240 .syntaxhl .comment .char { color:#385 }
1221 .syntaxhl .comment .delimiter { color:#385 }
1241 .syntaxhl .comment .delimiter { color:#385 }
1222 .syntaxhl .constant { color:#258; font-weight:bold }
1242 .syntaxhl .constant { color:#258; font-weight:bold }
1223 .syntaxhl .decorator { color:#B0B }
1243 .syntaxhl .decorator { color:#B0B }
1224 .syntaxhl .definition { color:#099; font-weight:bold }
1244 .syntaxhl .definition { color:#099; font-weight:bold }
1225 .syntaxhl .delimiter { color:black }
1245 .syntaxhl .delimiter { color:black }
1226 .syntaxhl .directive { color:#088; font-weight:bold }
1246 .syntaxhl .directive { color:#088; font-weight:bold }
1227 .syntaxhl .docstring { color:#D42; }
1247 .syntaxhl .docstring { color:#D42; }
1228 .syntaxhl .doctype { color:#34b }
1248 .syntaxhl .doctype { color:#34b }
1229 .syntaxhl .done { text-decoration: line-through; color: gray }
1249 .syntaxhl .done { text-decoration: line-through; color: gray }
1230 .syntaxhl .entity { color:#800; font-weight:bold }
1250 .syntaxhl .entity { color:#800; font-weight:bold }
1231 .syntaxhl .error { color:#F00; background-color:#FAA }
1251 .syntaxhl .error { color:#F00; background-color:#FAA }
1232 .syntaxhl .escape { color:#666 }
1252 .syntaxhl .escape { color:#666 }
1233 .syntaxhl .exception { color:#C00; font-weight:bold }
1253 .syntaxhl .exception { color:#C00; font-weight:bold }
1234 .syntaxhl .float { color:#06D }
1254 .syntaxhl .float { color:#06D }
1235 .syntaxhl .function { color:#06B; font-weight:bold }
1255 .syntaxhl .function { color:#06B; font-weight:bold }
1236 .syntaxhl .function .delimiter { color:#024; font-weight:bold }
1256 .syntaxhl .function .delimiter { color:#024; font-weight:bold }
1237 .syntaxhl .global-variable { color:#d70 }
1257 .syntaxhl .global-variable { color:#d70 }
1238 .syntaxhl .hex { color:#02b }
1258 .syntaxhl .hex { color:#02b }
1239 .syntaxhl .id { color:#33D; font-weight:bold }
1259 .syntaxhl .id { color:#33D; font-weight:bold }
1240 .syntaxhl .include { color:#B44; font-weight:bold }
1260 .syntaxhl .include { color:#B44; font-weight:bold }
1241 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1261 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1242 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1262 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1243 .syntaxhl .instance-variable { color:#33B }
1263 .syntaxhl .instance-variable { color:#33B }
1244 .syntaxhl .integer { color:#06D }
1264 .syntaxhl .integer { color:#06D }
1245 .syntaxhl .imaginary { color:#f00 }
1265 .syntaxhl .imaginary { color:#f00 }
1246 .syntaxhl .important { color:#D00 }
1266 .syntaxhl .important { color:#D00 }
1247 .syntaxhl .key { color: #606 }
1267 .syntaxhl .key { color: #606 }
1248 .syntaxhl .key .char { color: #60f }
1268 .syntaxhl .key .char { color: #60f }
1249 .syntaxhl .key .delimiter { color: #404 }
1269 .syntaxhl .key .delimiter { color: #404 }
1250 .syntaxhl .keyword { color:#939; font-weight:bold }
1270 .syntaxhl .keyword { color:#939; font-weight:bold }
1251 .syntaxhl .label { color:#970; font-weight:bold }
1271 .syntaxhl .label { color:#970; font-weight:bold }
1252 .syntaxhl .local-variable { color:#950 }
1272 .syntaxhl .local-variable { color:#950 }
1253 .syntaxhl .map .content { color:#808 }
1273 .syntaxhl .map .content { color:#808 }
1254 .syntaxhl .map .delimiter { color:#40A}
1274 .syntaxhl .map .delimiter { color:#40A}
1255 .syntaxhl .map { background-color:hsla(200,100%,50%,0.06); }
1275 .syntaxhl .map { background-color:hsla(200,100%,50%,0.06); }
1256 .syntaxhl .namespace { color:#707; font-weight:bold }
1276 .syntaxhl .namespace { color:#707; font-weight:bold }
1257 .syntaxhl .octal { color:#40E }
1277 .syntaxhl .octal { color:#40E }
1258 .syntaxhl .operator { }
1278 .syntaxhl .operator { }
1259 .syntaxhl .predefined { color:#369; font-weight:bold }
1279 .syntaxhl .predefined { color:#369; font-weight:bold }
1260 .syntaxhl .predefined-constant { color:#069 }
1280 .syntaxhl .predefined-constant { color:#069 }
1261 .syntaxhl .predefined-type { color:#0a8; font-weight:bold }
1281 .syntaxhl .predefined-type { color:#0a8; font-weight:bold }
1262 .syntaxhl .preprocessor { color:#579 }
1282 .syntaxhl .preprocessor { color:#579 }
1263 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1283 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1264 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1284 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1265 .syntaxhl .regexp .content { color:#808 }
1285 .syntaxhl .regexp .content { color:#808 }
1266 .syntaxhl .regexp .delimiter { color:#404 }
1286 .syntaxhl .regexp .delimiter { color:#404 }
1267 .syntaxhl .regexp .modifier { color:#C2C }
1287 .syntaxhl .regexp .modifier { color:#C2C }
1268 .syntaxhl .reserved { color:#080; font-weight:bold }
1288 .syntaxhl .reserved { color:#080; font-weight:bold }
1269 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1289 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1270 .syntaxhl .shell .content { color:#2B2 }
1290 .syntaxhl .shell .content { color:#2B2 }
1271 .syntaxhl .shell .delimiter { color:#161 }
1291 .syntaxhl .shell .delimiter { color:#161 }
1272 .syntaxhl .string .char { color: #46a }
1292 .syntaxhl .string .char { color: #46a }
1273 .syntaxhl .string .content { color: #46a }
1293 .syntaxhl .string .content { color: #46a }
1274 .syntaxhl .string .delimiter { color: #46a }
1294 .syntaxhl .string .delimiter { color: #46a }
1275 .syntaxhl .string .modifier { color: #46a }
1295 .syntaxhl .string .modifier { color: #46a }
1276 .syntaxhl .symbol { color:#d33 }
1296 .syntaxhl .symbol { color:#d33 }
1277 .syntaxhl .symbol .content { color:#d33 }
1297 .syntaxhl .symbol .content { color:#d33 }
1278 .syntaxhl .symbol .delimiter { color:#d33 }
1298 .syntaxhl .symbol .delimiter { color:#d33 }
1279 .syntaxhl .tag { color:#070; font-weight:bold }
1299 .syntaxhl .tag { color:#070; font-weight:bold }
1280 .syntaxhl .type { color:#339; font-weight:bold }
1300 .syntaxhl .type { color:#339; font-weight:bold }
1281 .syntaxhl .value { color: #088 }
1301 .syntaxhl .value { color: #088 }
1282 .syntaxhl .variable { color:#037 }
1302 .syntaxhl .variable { color:#037 }
1283
1303
1284 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1304 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1285 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1305 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1286 .syntaxhl .change { color: #bbf; background: #007 }
1306 .syntaxhl .change { color: #bbf; background: #007 }
1287 .syntaxhl .head { color: #f8f; background: #505 }
1307 .syntaxhl .head { color: #f8f; background: #505 }
1288 .syntaxhl .head .filename { color: white; }
1308 .syntaxhl .head .filename { color: white; }
1289
1309
1290 .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; }
1310 .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; }
1291 .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; }
1311 .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; }
1292
1312
1293 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1313 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1294 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1314 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1295 .syntaxhl .change .change { color: #88f }
1315 .syntaxhl .change .change { color: #88f }
1296 .syntaxhl .head .head { color: #f4f }
1316 .syntaxhl .head .head { color: #f4f }
1297
1317
1298 /***** Media print specific styles *****/
1318 /***** Media print specific styles *****/
1299 @media print {
1319 @media print {
1300 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1320 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1301 #main { background: #fff; }
1321 #main { background: #fff; }
1302 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1322 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1303 #wiki_add_attachment { display:none; }
1323 #wiki_add_attachment { display:none; }
1304 .hide-when-print { display: none; }
1324 .hide-when-print { display: none; }
1305 .autoscroll {overflow-x: visible;}
1325 .autoscroll {overflow-x: visible;}
1306 table.list {margin-top:0.5em;}
1326 table.list {margin-top:0.5em;}
1307 table.list th, table.list td {border: 1px solid #aaa;}
1327 table.list th, table.list td {border: 1px solid #aaa;}
1308 }
1328 }
1309
1329
1310 /* Accessibility specific styles */
1330 /* Accessibility specific styles */
1311 .hidden-for-sighted {
1331 .hidden-for-sighted {
1312 position:absolute;
1332 position:absolute;
1313 left:-10000px;
1333 left:-10000px;
1314 top:auto;
1334 top:auto;
1315 width:1px;
1335 width:1px;
1316 height:1px;
1336 height:1px;
1317 overflow:hidden;
1337 overflow:hidden;
1318 }
1338 }
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1541 +1,1541
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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 Redmine::I18n
23 include Redmine::I18n
24 include ERB::Util
24 include ERB::Util
25 include Rails.application.routes.url_helpers
25 include Rails.application.routes.url_helpers
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :email_addresses,
28 :email_addresses,
29 :repositories, :changesets,
29 :repositories, :changesets,
30 :projects_trackers,
30 :projects_trackers,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
32 :wikis, :wiki_pages, :wiki_contents,
32 :wikis, :wiki_pages, :wiki_contents,
33 :boards, :messages, :news,
33 :boards, :messages, :news,
34 :attachments, :enumerations
34 :attachments, :enumerations
35
35
36 def setup
36 def setup
37 super
37 super
38 set_tmp_attachments_directory
38 set_tmp_attachments_directory
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
40 end
40 end
41
41
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
43 User.current = User.find_by_login('admin')
43 User.current = User.find_by_login('admin')
44
44
45 @project = Issue.first.project # Used by helper
45 @project = Issue.first.project # Used by helper
46 response = link_to_if_authorized('By controller/actionr',
46 response = link_to_if_authorized('By controller/actionr',
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 assert_match /href/, response
48 assert_match /href/, response
49 end
49 end
50
50
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
52 User.current = User.find_by_login('dlopper')
52 User.current = User.find_by_login('dlopper')
53 @project = Project.find('private-child')
53 @project = Project.find('private-child')
54 issue = @project.issues.first
54 issue = @project.issues.first
55 assert !issue.visible?
55 assert !issue.visible?
56
56
57 response = link_to_if_authorized('Never displayed',
57 response = link_to_if_authorized('Never displayed',
58 {:controller => 'issues', :action => 'show', :id => issue})
58 {:controller => 'issues', :action => 'show', :id => issue})
59 assert_nil response
59 assert_nil response
60 end
60 end
61
61
62 def test_auto_links
62 def test_auto_links
63 to_test = {
63 to_test = {
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
70 '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>.',
70 '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>.',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
72 '(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>)',
72 '(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>)',
73 '(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>)',
73 '(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>)',
74 '(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>).',
74 '(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>).',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
79 '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>',
79 '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>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
86 # two exclamation marks
86 # two exclamation marks
87 '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>',
87 '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>',
88 # escaping
88 # escaping
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
90 # wrap in angle brackets
90 # wrap in angle brackets
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
92 # invalid urls
92 # invalid urls
93 'http://' => 'http://',
93 'http://' => 'http://',
94 'www.' => 'www.',
94 'www.' => 'www.',
95 'test-www.bar.com' => 'test-www.bar.com',
95 'test-www.bar.com' => 'test-www.bar.com',
96 }
96 }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 end
98 end
99
99
100 def test_auto_links_with_non_ascii_characters
100 def test_auto_links_with_non_ascii_characters
101 to_test = {
101 to_test = {
102 "http://foo.bar/#{@russian_test}" =>
102 "http://foo.bar/#{@russian_test}" =>
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
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_auto_mailto
108 def test_auto_mailto
109 to_test = {
109 to_test = {
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
112 }
112 }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
114 end
114 end
115
115
116 def test_inline_images
116 def test_inline_images
117 to_test = {
117 to_test = {
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
121 '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="" />',
121 '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="" />',
122 '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" />',
122 '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" />',
123 '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;" />',
123 '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;" />',
124 }
124 }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
126 end
126 end
127
127
128 def test_inline_images_inside_tags
128 def test_inline_images_inside_tags
129 raw = <<-RAW
129 raw = <<-RAW
130 h1. !foo.png! Heading
130 h1. !foo.png! Heading
131
131
132 Centered image:
132 Centered image:
133
133
134 p=. !bar.gif!
134 p=. !bar.gif!
135 RAW
135 RAW
136
136
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
139 end
139 end
140
140
141 def test_attached_images
141 def test_attached_images
142 to_test = {
142 to_test = {
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
147 # link image
147 # link image
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
149 }
149 }
150 attachments = Attachment.all
150 attachments = Attachment.all
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
152 end
152 end
153
153
154 def test_attached_images_with_textile_and_non_ascii_filename
154 def test_attached_images_with_textile_and_non_ascii_filename
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
156 with_settings :text_formatting => 'textile' do
156 with_settings :text_formatting => 'textile' do
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
159 end
159 end
160 end
160 end
161
161
162 def test_attached_images_with_markdown_and_non_ascii_filename
162 def test_attached_images_with_markdown_and_non_ascii_filename
163 skip unless Object.const_defined?(:Redcarpet)
163 skip unless Object.const_defined?(:Redcarpet)
164
164
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
166 with_settings :text_formatting => 'markdown' do
166 with_settings :text_formatting => 'markdown' do
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="">),
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="">),
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
169 end
169 end
170 end
170 end
171
171
172 def test_attached_images_filename_extension
172 def test_attached_images_filename_extension
173 set_tmp_attachments_directory
173 set_tmp_attachments_directory
174 a1 = Attachment.new(
174 a1 = Attachment.new(
175 :container => Issue.find(1),
175 :container => Issue.find(1),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
177 :author => User.find(1))
177 :author => User.find(1))
178 assert a1.save
178 assert a1.save
179 assert_equal "testtest.JPG", a1.filename
179 assert_equal "testtest.JPG", a1.filename
180 assert_equal "image/jpeg", a1.content_type
180 assert_equal "image/jpeg", a1.content_type
181 assert a1.image?
181 assert a1.image?
182
182
183 a2 = Attachment.new(
183 a2 = Attachment.new(
184 :container => Issue.find(1),
184 :container => Issue.find(1),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
186 :author => User.find(1))
186 :author => User.find(1))
187 assert a2.save
187 assert a2.save
188 assert_equal "testtest.jpeg", a2.filename
188 assert_equal "testtest.jpeg", a2.filename
189 assert_equal "image/jpeg", a2.content_type
189 assert_equal "image/jpeg", a2.content_type
190 assert a2.image?
190 assert a2.image?
191
191
192 a3 = Attachment.new(
192 a3 = Attachment.new(
193 :container => Issue.find(1),
193 :container => Issue.find(1),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
195 :author => User.find(1))
195 :author => User.find(1))
196 assert a3.save
196 assert a3.save
197 assert_equal "testtest.JPE", a3.filename
197 assert_equal "testtest.JPE", a3.filename
198 assert_equal "image/jpeg", a3.content_type
198 assert_equal "image/jpeg", a3.content_type
199 assert a3.image?
199 assert a3.image?
200
200
201 a4 = Attachment.new(
201 a4 = Attachment.new(
202 :container => Issue.find(1),
202 :container => Issue.find(1),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
204 :author => User.find(1))
204 :author => User.find(1))
205 assert a4.save
205 assert a4.save
206 assert_equal "Testtest.BMP", a4.filename
206 assert_equal "Testtest.BMP", a4.filename
207 assert_equal "image/x-ms-bmp", a4.content_type
207 assert_equal "image/x-ms-bmp", a4.content_type
208 assert a4.image?
208 assert a4.image?
209
209
210 to_test = {
210 to_test = {
211 'Inline image: !testtest.jpg!' =>
211 'Inline image: !testtest.jpg!' =>
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
213 'Inline image: !testtest.jpeg!' =>
213 'Inline image: !testtest.jpeg!' =>
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
215 'Inline image: !testtest.jpe!' =>
215 'Inline image: !testtest.jpe!' =>
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
217 'Inline image: !testtest.bmp!' =>
217 'Inline image: !testtest.bmp!' =>
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
219 }
219 }
220
220
221 attachments = [a1, a2, a3, a4]
221 attachments = [a1, a2, a3, a4]
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
223 end
223 end
224
224
225 def test_attached_images_should_read_later
225 def test_attached_images_should_read_later
226 set_fixtures_attachments_directory
226 set_fixtures_attachments_directory
227 a1 = Attachment.find(16)
227 a1 = Attachment.find(16)
228 assert_equal "testfile.png", a1.filename
228 assert_equal "testfile.png", a1.filename
229 assert a1.readable?
229 assert a1.readable?
230 assert (! a1.visible?(User.anonymous))
230 assert (! a1.visible?(User.anonymous))
231 assert a1.visible?(User.find(2))
231 assert a1.visible?(User.find(2))
232 a2 = Attachment.find(17)
232 a2 = Attachment.find(17)
233 assert_equal "testfile.PNG", a2.filename
233 assert_equal "testfile.PNG", a2.filename
234 assert a2.readable?
234 assert a2.readable?
235 assert (! a2.visible?(User.anonymous))
235 assert (! a2.visible?(User.anonymous))
236 assert a2.visible?(User.find(2))
236 assert a2.visible?(User.find(2))
237 assert a1.created_on < a2.created_on
237 assert a1.created_on < a2.created_on
238
238
239 to_test = {
239 to_test = {
240 'Inline image: !testfile.png!' =>
240 'Inline image: !testfile.png!' =>
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
242 'Inline image: !Testfile.PNG!' =>
242 'Inline image: !Testfile.PNG!' =>
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
244 }
244 }
245 attachments = [a1, a2]
245 attachments = [a1, a2]
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
247 set_tmp_attachments_directory
247 set_tmp_attachments_directory
248 end
248 end
249
249
250 def test_textile_external_links
250 def test_textile_external_links
251 to_test = {
251 to_test = {
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
255 '"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>',
255 '"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>',
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
257 # no multiline link text
257 # no multiline link text
258 "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",
258 "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",
259 # mailto link
259 # mailto link
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
261 # two exclamation marks
261 # two exclamation marks
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
263 # escaping
263 # escaping
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
265 }
265 }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
267 end
267 end
268
268
269 def test_textile_external_links_with_non_ascii_characters
269 def test_textile_external_links_with_non_ascii_characters
270 to_test = {
270 to_test = {
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
273 }
273 }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 end
275 end
276
276
277 def test_redmine_links
277 def test_redmine_links
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
284
284
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
289
289
290 changeset_link2 = link_to('691322a8eb01e11fd7',
290 changeset_link2 = link_to('691322a8eb01e11fd7',
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
293
293
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
295 :class => 'document')
295 :class => 'document')
296
296
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
298 :class => 'version')
298 :class => 'version')
299
299
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
301
301
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
303
303
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
305
305
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
307
307
308 source_url = '/projects/ecookbook/repository/entry/some/file'
308 source_url = '/projects/ecookbook/repository/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
313
313
314 export_url = '/projects/ecookbook/repository/raw/some/file'
314 export_url = '/projects/ecookbook/repository/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
319
319
320 to_test = {
320 to_test = {
321 # tickets
321 # tickets
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
323 # ticket notes
323 # ticket notes
324 '#3-14' => note_link,
324 '#3-14' => note_link,
325 '#3#note-14' => note_link2,
325 '#3#note-14' => note_link2,
326 # should not ignore leading zero
326 # should not ignore leading zero
327 '#03' => '#03',
327 '#03' => '#03',
328 # changesets
328 # changesets
329 'r1' => revision_link,
329 'r1' => revision_link,
330 'r1.' => "#{revision_link}.",
330 'r1.' => "#{revision_link}.",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
333 'commit:691322a8eb01e11fd7' => changeset_link2,
333 'commit:691322a8eb01e11fd7' => changeset_link2,
334 # documents
334 # documents
335 'document#1' => document_link,
335 'document#1' => document_link,
336 'document:"Test document"' => document_link,
336 'document:"Test document"' => document_link,
337 # versions
337 # versions
338 'version#2' => version_link,
338 'version#2' => version_link,
339 'version:1.0' => version_link,
339 'version:1.0' => version_link,
340 'version:"1.0"' => version_link,
340 'version:"1.0"' => version_link,
341 # source
341 # source
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
355 # export
355 # export
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
361 # forum
361 # forum
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
364 # message
364 # message
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
367 # news
367 # news
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
370 # project
370 # project
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
374 # not found
374 # not found
375 '#0123456789' => '#0123456789',
375 '#0123456789' => '#0123456789',
376 # invalid expressions
376 # invalid expressions
377 'source:' => 'source:',
377 'source:' => 'source:',
378 # url hash
378 # url hash
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
380 }
380 }
381 @project = Project.find(1)
381 @project = Project.find(1)
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
383 end
383 end
384
384
385 def test_should_not_parse_redmine_links_inside_link
385 def test_should_not_parse_redmine_links_inside_link
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
388 textilizable(raw, :project => Project.find(1))
388 textilizable(raw, :project => Project.find(1))
389 end
389 end
390
390
391 def test_redmine_links_with_a_different_project_before_current_project
391 def test_redmine_links_with_a_different_project_before_current_project
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
394 @project = Project.find(3)
394 @project = Project.find(3)
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
397 assert_equal "<p>#{result1} #{result2}</p>",
397 assert_equal "<p>#{result1} #{result2}</p>",
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
399 end
399 end
400
400
401 def test_escaped_redmine_links_should_not_be_parsed
401 def test_escaped_redmine_links_should_not_be_parsed
402 to_test = [
402 to_test = [
403 '#3.',
403 '#3.',
404 '#3-14.',
404 '#3-14.',
405 '#3#-note14.',
405 '#3#-note14.',
406 'r1',
406 'r1',
407 'document#1',
407 'document#1',
408 'document:"Test document"',
408 'document:"Test document"',
409 'version#2',
409 'version#2',
410 'version:1.0',
410 'version:1.0',
411 'version:"1.0"',
411 'version:"1.0"',
412 'source:/some/file'
412 'source:/some/file'
413 ]
413 ]
414 @project = Project.find(1)
414 @project = Project.find(1)
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
416 end
416 end
417
417
418 def test_cross_project_redmine_links
418 def test_cross_project_redmine_links
419 source_link = link_to('ecookbook:source:/some/file',
419 source_link = link_to('ecookbook:source:/some/file',
420 {:controller => 'repositories', :action => 'entry',
420 {:controller => 'repositories', :action => 'entry',
421 :id => 'ecookbook', :path => ['some', 'file']},
421 :id => 'ecookbook', :path => ['some', 'file']},
422 :class => 'source')
422 :class => 'source')
423 changeset_link = link_to('ecookbook:r2',
423 changeset_link = link_to('ecookbook:r2',
424 {:controller => 'repositories', :action => 'revision',
424 {:controller => 'repositories', :action => 'revision',
425 :id => 'ecookbook', :rev => 2},
425 :id => 'ecookbook', :rev => 2},
426 :class => 'changeset',
426 :class => 'changeset',
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
428 to_test = {
428 to_test = {
429 # documents
429 # documents
430 'document:"Test document"' => 'document:"Test document"',
430 'document:"Test document"' => 'document:"Test document"',
431 'ecookbook:document:"Test document"' =>
431 'ecookbook:document:"Test document"' =>
432 link_to("Test document", "/documents/1", :class => "document"),
432 link_to("Test document", "/documents/1", :class => "document"),
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
434 # versions
434 # versions
435 'version:"1.0"' => 'version:"1.0"',
435 'version:"1.0"' => 'version:"1.0"',
436 'ecookbook:version:"1.0"' =>
436 'ecookbook:version:"1.0"' =>
437 link_to("1.0", "/versions/2", :class => "version"),
437 link_to("1.0", "/versions/2", :class => "version"),
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
439 # changeset
439 # changeset
440 'r2' => 'r2',
440 'r2' => 'r2',
441 'ecookbook:r2' => changeset_link,
441 'ecookbook:r2' => changeset_link,
442 'invalid:r2' => 'invalid:r2',
442 'invalid:r2' => 'invalid:r2',
443 # source
443 # source
444 'source:/some/file' => 'source:/some/file',
444 'source:/some/file' => 'source:/some/file',
445 'ecookbook:source:/some/file' => source_link,
445 'ecookbook:source:/some/file' => source_link,
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
447 }
447 }
448 @project = Project.find(3)
448 @project = Project.find(3)
449 to_test.each do |text, result|
449 to_test.each do |text, result|
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
451 end
451 end
452 end
452 end
453
453
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
457
457
458 @project = v.project
458 @project = v.project
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
460 end
460 end
461
461
462 def test_link_to_issue_subject
462 def test_link_to_issue_subject
463 issue = Issue.generate!(:subject => "01234567890123456789")
463 issue = Issue.generate!(:subject => "01234567890123456789")
464 str = link_to_issue(issue, :truncate => 10)
464 str = link_to_issue(issue, :truncate => 10)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
466 assert_equal "#{result}: 0123456...", str
466 assert_equal "#{result}: 0123456...", str
467
467
468 issue = Issue.generate!(:subject => "<&>")
468 issue = Issue.generate!(:subject => "<&>")
469 str = link_to_issue(issue)
469 str = link_to_issue(issue)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
471 assert_equal "#{result}: &lt;&amp;&gt;", str
471 assert_equal "#{result}: &lt;&amp;&gt;", str
472
472
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
474 str = link_to_issue(issue, :truncate => 10)
474 str = link_to_issue(issue, :truncate => 10)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
477 end
477 end
478
478
479 def test_link_to_issue_title
479 def test_link_to_issue_title
480 long_str = "0123456789" * 5
480 long_str = "0123456789" * 5
481
481
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
483 str = link_to_issue(issue, :subject => false)
483 str = link_to_issue(issue, :subject => false)
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
485 :class => issue.css_classes,
485 :class => issue.css_classes,
486 :title => "#{long_str}0123456...")
486 :title => "#{long_str}0123456...")
487 assert_equal result, str
487 assert_equal result, str
488
488
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
490 str = link_to_issue(issue, :subject => false)
490 str = link_to_issue(issue, :subject => false)
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
492 :class => issue.css_classes,
492 :class => issue.css_classes,
493 :title => "<&>#{long_str}0123...")
493 :title => "<&>#{long_str}0123...")
494 assert_equal result, str
494 assert_equal result, str
495 end
495 end
496
496
497 def test_multiple_repositories_redmine_links
497 def test_multiple_repositories_redmine_links
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
502
502
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
506 :class => 'changeset', :title => '')
506 :class => 'changeset', :title => '')
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
508 :class => 'changeset', :title => '')
508 :class => 'changeset', :title => '')
509
509
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
512
512
513 to_test = {
513 to_test = {
514 'r2' => changeset_link,
514 'r2' => changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
516 'invalid|r123' => 'invalid|r123',
516 'invalid|r123' => 'invalid|r123',
517 'commit:hg1|abcd' => hg_changeset_link,
517 'commit:hg1|abcd' => hg_changeset_link,
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
519 # source
519 # source
520 'source:some/file' => source_link,
520 'source:some/file' => source_link,
521 'source:hg1|some/file' => hg_source_link,
521 'source:hg1|some/file' => hg_source_link,
522 'source:invalid|some/file' => 'source:invalid|some/file',
522 'source:invalid|some/file' => 'source:invalid|some/file',
523 }
523 }
524
524
525 @project = Project.find(1)
525 @project = Project.find(1)
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
527 end
527 end
528
528
529 def test_cross_project_multiple_repositories_redmine_links
529 def test_cross_project_multiple_repositories_redmine_links
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
534
534
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
538 :class => 'changeset', :title => '')
538 :class => 'changeset', :title => '')
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
540 :class => 'changeset', :title => '')
540 :class => 'changeset', :title => '')
541
541
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
544
544
545 to_test = {
545 to_test = {
546 'ecookbook:r2' => changeset_link,
546 'ecookbook:r2' => changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
552 # source
552 # source
553 'ecookbook:source:some/file' => source_link,
553 'ecookbook:source:some/file' => source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
557 }
557 }
558
558
559 @project = Project.find(3)
559 @project = Project.find(3)
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
561 end
561 end
562
562
563 def test_redmine_links_git_commit
563 def test_redmine_links_git_commit
564 changeset_link = link_to('abcd',
564 changeset_link = link_to('abcd',
565 {
565 {
566 :controller => 'repositories',
566 :controller => 'repositories',
567 :action => 'revision',
567 :action => 'revision',
568 :id => 'subproject1',
568 :id => 'subproject1',
569 :rev => 'abcd',
569 :rev => 'abcd',
570 },
570 },
571 :class => 'changeset', :title => 'test commit')
571 :class => 'changeset', :title => 'test commit')
572 to_test = {
572 to_test = {
573 'commit:abcd' => changeset_link,
573 'commit:abcd' => changeset_link,
574 }
574 }
575 @project = Project.find(3)
575 @project = Project.find(3)
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
577 assert r
577 assert r
578 c = Changeset.new(:repository => r,
578 c = Changeset.new(:repository => r,
579 :committed_on => Time.now,
579 :committed_on => Time.now,
580 :revision => 'abcd',
580 :revision => 'abcd',
581 :scmid => 'abcd',
581 :scmid => 'abcd',
582 :comments => 'test commit')
582 :comments => 'test commit')
583 assert( c.save )
583 assert( c.save )
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
585 end
585 end
586
586
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
588 def test_redmine_links_darcs_commit
588 def test_redmine_links_darcs_commit
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
590 {
590 {
591 :controller => 'repositories',
591 :controller => 'repositories',
592 :action => 'revision',
592 :action => 'revision',
593 :id => 'subproject1',
593 :id => 'subproject1',
594 :rev => '123',
594 :rev => '123',
595 },
595 },
596 :class => 'changeset', :title => 'test commit')
596 :class => 'changeset', :title => 'test commit')
597 to_test = {
597 to_test = {
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
599 }
599 }
600 @project = Project.find(3)
600 @project = Project.find(3)
601 r = Repository::Darcs.create!(
601 r = Repository::Darcs.create!(
602 :project => @project, :url => '/tmp/test/darcs',
602 :project => @project, :url => '/tmp/test/darcs',
603 :log_encoding => 'UTF-8')
603 :log_encoding => 'UTF-8')
604 assert r
604 assert r
605 c = Changeset.new(:repository => r,
605 c = Changeset.new(:repository => r,
606 :committed_on => Time.now,
606 :committed_on => Time.now,
607 :revision => '123',
607 :revision => '123',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
609 :comments => 'test commit')
609 :comments => 'test commit')
610 assert( c.save )
610 assert( c.save )
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
612 end
612 end
613
613
614 def test_redmine_links_mercurial_commit
614 def test_redmine_links_mercurial_commit
615 changeset_link_rev = link_to('r123',
615 changeset_link_rev = link_to('r123',
616 {
616 {
617 :controller => 'repositories',
617 :controller => 'repositories',
618 :action => 'revision',
618 :action => 'revision',
619 :id => 'subproject1',
619 :id => 'subproject1',
620 :rev => '123' ,
620 :rev => '123' ,
621 },
621 },
622 :class => 'changeset', :title => 'test commit')
622 :class => 'changeset', :title => 'test commit')
623 changeset_link_commit = link_to('abcd',
623 changeset_link_commit = link_to('abcd',
624 {
624 {
625 :controller => 'repositories',
625 :controller => 'repositories',
626 :action => 'revision',
626 :action => 'revision',
627 :id => 'subproject1',
627 :id => 'subproject1',
628 :rev => 'abcd' ,
628 :rev => 'abcd' ,
629 },
629 },
630 :class => 'changeset', :title => 'test commit')
630 :class => 'changeset', :title => 'test commit')
631 to_test = {
631 to_test = {
632 'r123' => changeset_link_rev,
632 'r123' => changeset_link_rev,
633 'commit:abcd' => changeset_link_commit,
633 'commit:abcd' => changeset_link_commit,
634 }
634 }
635 @project = Project.find(3)
635 @project = Project.find(3)
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
637 assert r
637 assert r
638 c = Changeset.new(:repository => r,
638 c = Changeset.new(:repository => r,
639 :committed_on => Time.now,
639 :committed_on => Time.now,
640 :revision => '123',
640 :revision => '123',
641 :scmid => 'abcd',
641 :scmid => 'abcd',
642 :comments => 'test commit')
642 :comments => 'test commit')
643 assert( c.save )
643 assert( c.save )
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
645 end
645 end
646
646
647 def test_attachment_links
647 def test_attachment_links
648 text = 'attachment:error281.txt'
648 text = 'attachment:error281.txt'
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
650 :class => "attachment")
650 :class => "attachment")
651 assert_equal "<p>#{result}</p>",
651 assert_equal "<p>#{result}</p>",
652 textilizable(text,
652 textilizable(text,
653 :attachments => Issue.find(3).attachments),
653 :attachments => Issue.find(3).attachments),
654 "#{text} failed"
654 "#{text} failed"
655 end
655 end
656
656
657 def test_attachment_link_should_link_to_latest_attachment
657 def test_attachment_link_should_link_to_latest_attachment
658 set_tmp_attachments_directory
658 set_tmp_attachments_directory
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
660 a2 = Attachment.generate!(:filename => "test.txt")
660 a2 = Attachment.generate!(:filename => "test.txt")
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
662 :class => "attachment")
662 :class => "attachment")
663 assert_equal "<p>#{result}</p>",
663 assert_equal "<p>#{result}</p>",
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
665 end
665 end
666
666
667 def test_wiki_links
667 def test_wiki_links
668 russian_eacape = CGI.escape(@russian_test)
668 russian_eacape = CGI.escape(@russian_test)
669 to_test = {
669 to_test = {
670 '[[CookBook documentation]]' =>
670 '[[CookBook documentation]]' =>
671 link_to("CookBook documentation",
671 link_to("CookBook documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
673 :class => "wiki-page"),
673 :class => "wiki-page"),
674 '[[Another page|Page]]' =>
674 '[[Another page|Page]]' =>
675 link_to("Page",
675 link_to("Page",
676 "/projects/ecookbook/wiki/Another_page",
676 "/projects/ecookbook/wiki/Another_page",
677 :class => "wiki-page"),
677 :class => "wiki-page"),
678 # title content should be formatted
678 # title content should be formatted
679 '[[Another page|With _styled_ *title*]]' =>
679 '[[Another page|With _styled_ *title*]]' =>
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
681 "/projects/ecookbook/wiki/Another_page",
681 "/projects/ecookbook/wiki/Another_page",
682 :class => "wiki-page"),
682 :class => "wiki-page"),
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
685 "/projects/ecookbook/wiki/Another_page",
685 "/projects/ecookbook/wiki/Another_page",
686 :class => "wiki-page"),
686 :class => "wiki-page"),
687 # link with anchor
687 # link with anchor
688 '[[CookBook documentation#One-section]]' =>
688 '[[CookBook documentation#One-section]]' =>
689 link_to("CookBook documentation",
689 link_to("CookBook documentation",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
691 :class => "wiki-page"),
691 :class => "wiki-page"),
692 '[[Another page#anchor|Page]]' =>
692 '[[Another page#anchor|Page]]' =>
693 link_to("Page",
693 link_to("Page",
694 "/projects/ecookbook/wiki/Another_page#anchor",
694 "/projects/ecookbook/wiki/Another_page#anchor",
695 :class => "wiki-page"),
695 :class => "wiki-page"),
696 # UTF8 anchor
696 # UTF8 anchor
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
698 link_to(@russian_test,
698 link_to(@russian_test,
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
700 :class => "wiki-page"),
700 :class => "wiki-page"),
701 # page that doesn't exist
701 # page that doesn't exist
702 '[[Unknown page]]' =>
702 '[[Unknown page]]' =>
703 link_to("Unknown page",
703 link_to("Unknown page",
704 "/projects/ecookbook/wiki/Unknown_page",
704 "/projects/ecookbook/wiki/Unknown_page",
705 :class => "wiki-page new"),
705 :class => "wiki-page new"),
706 '[[Unknown page|404]]' =>
706 '[[Unknown page|404]]' =>
707 link_to("404",
707 link_to("404",
708 "/projects/ecookbook/wiki/Unknown_page",
708 "/projects/ecookbook/wiki/Unknown_page",
709 :class => "wiki-page new"),
709 :class => "wiki-page new"),
710 # link to another project wiki
710 # link to another project wiki
711 '[[onlinestore:]]' =>
711 '[[onlinestore:]]' =>
712 link_to("onlinestore",
712 link_to("onlinestore",
713 "/projects/onlinestore/wiki",
713 "/projects/onlinestore/wiki",
714 :class => "wiki-page"),
714 :class => "wiki-page"),
715 '[[onlinestore:|Wiki]]' =>
715 '[[onlinestore:|Wiki]]' =>
716 link_to("Wiki",
716 link_to("Wiki",
717 "/projects/onlinestore/wiki",
717 "/projects/onlinestore/wiki",
718 :class => "wiki-page"),
718 :class => "wiki-page"),
719 '[[onlinestore:Start page]]' =>
719 '[[onlinestore:Start page]]' =>
720 link_to("Start page",
720 link_to("Start page",
721 "/projects/onlinestore/wiki/Start_page",
721 "/projects/onlinestore/wiki/Start_page",
722 :class => "wiki-page"),
722 :class => "wiki-page"),
723 '[[onlinestore:Start page|Text]]' =>
723 '[[onlinestore:Start page|Text]]' =>
724 link_to("Text",
724 link_to("Text",
725 "/projects/onlinestore/wiki/Start_page",
725 "/projects/onlinestore/wiki/Start_page",
726 :class => "wiki-page"),
726 :class => "wiki-page"),
727 '[[onlinestore:Unknown page]]' =>
727 '[[onlinestore:Unknown page]]' =>
728 link_to("Unknown page",
728 link_to("Unknown page",
729 "/projects/onlinestore/wiki/Unknown_page",
729 "/projects/onlinestore/wiki/Unknown_page",
730 :class => "wiki-page new"),
730 :class => "wiki-page new"),
731 # struck through link
731 # struck through link
732 '-[[Another page|Page]]-' =>
732 '-[[Another page|Page]]-' =>
733 "<del>".html_safe +
733 "<del>".html_safe +
734 link_to("Page",
734 link_to("Page",
735 "/projects/ecookbook/wiki/Another_page",
735 "/projects/ecookbook/wiki/Another_page",
736 :class => "wiki-page").html_safe +
736 :class => "wiki-page").html_safe +
737 "</del>".html_safe,
737 "</del>".html_safe,
738 '-[[Another page|Page]] link-' =>
738 '-[[Another page|Page]] link-' =>
739 "<del>".html_safe +
739 "<del>".html_safe +
740 link_to("Page",
740 link_to("Page",
741 "/projects/ecookbook/wiki/Another_page",
741 "/projects/ecookbook/wiki/Another_page",
742 :class => "wiki-page").html_safe +
742 :class => "wiki-page").html_safe +
743 " link</del>".html_safe,
743 " link</del>".html_safe,
744 # escaping
744 # escaping
745 '![[Another page|Page]]' => '[[Another page|Page]]',
745 '![[Another page|Page]]' => '[[Another page|Page]]',
746 # project does not exist
746 # project does not exist
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
749 }
749 }
750 @project = Project.find(1)
750 @project = Project.find(1)
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
752 end
752 end
753
753
754 def test_wiki_links_within_local_file_generation_context
754 def test_wiki_links_within_local_file_generation_context
755 to_test = {
755 to_test = {
756 # link to a page
756 # link to a page
757 '[[CookBook documentation]]' =>
757 '[[CookBook documentation]]' =>
758 link_to("CookBook documentation", "CookBook_documentation.html",
758 link_to("CookBook documentation", "CookBook_documentation.html",
759 :class => "wiki-page"),
759 :class => "wiki-page"),
760 '[[CookBook documentation|documentation]]' =>
760 '[[CookBook documentation|documentation]]' =>
761 link_to("documentation", "CookBook_documentation.html",
761 link_to("documentation", "CookBook_documentation.html",
762 :class => "wiki-page"),
762 :class => "wiki-page"),
763 '[[CookBook documentation#One-section]]' =>
763 '[[CookBook documentation#One-section]]' =>
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
765 :class => "wiki-page"),
765 :class => "wiki-page"),
766 '[[CookBook documentation#One-section|documentation]]' =>
766 '[[CookBook documentation#One-section|documentation]]' =>
767 link_to("documentation", "CookBook_documentation.html#One-section",
767 link_to("documentation", "CookBook_documentation.html#One-section",
768 :class => "wiki-page"),
768 :class => "wiki-page"),
769 # page that doesn't exist
769 # page that doesn't exist
770 '[[Unknown page]]' =>
770 '[[Unknown page]]' =>
771 link_to("Unknown page", "Unknown_page.html",
771 link_to("Unknown page", "Unknown_page.html",
772 :class => "wiki-page new"),
772 :class => "wiki-page new"),
773 '[[Unknown page|404]]' =>
773 '[[Unknown page|404]]' =>
774 link_to("404", "Unknown_page.html",
774 link_to("404", "Unknown_page.html",
775 :class => "wiki-page new"),
775 :class => "wiki-page new"),
776 '[[Unknown page#anchor]]' =>
776 '[[Unknown page#anchor]]' =>
777 link_to("Unknown page", "Unknown_page.html#anchor",
777 link_to("Unknown page", "Unknown_page.html#anchor",
778 :class => "wiki-page new"),
778 :class => "wiki-page new"),
779 '[[Unknown page#anchor|404]]' =>
779 '[[Unknown page#anchor|404]]' =>
780 link_to("404", "Unknown_page.html#anchor",
780 link_to("404", "Unknown_page.html#anchor",
781 :class => "wiki-page new"),
781 :class => "wiki-page new"),
782 }
782 }
783 @project = Project.find(1)
783 @project = Project.find(1)
784 to_test.each do |text, result|
784 to_test.each do |text, result|
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
786 end
786 end
787 end
787 end
788
788
789 def test_wiki_links_within_wiki_page_context
789 def test_wiki_links_within_wiki_page_context
790 page = WikiPage.find_by_title('Another_page' )
790 page = WikiPage.find_by_title('Another_page' )
791 to_test = {
791 to_test = {
792 '[[CookBook documentation]]' =>
792 '[[CookBook documentation]]' =>
793 link_to("CookBook documentation",
793 link_to("CookBook documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
795 :class => "wiki-page"),
795 :class => "wiki-page"),
796 '[[CookBook documentation|documentation]]' =>
796 '[[CookBook documentation|documentation]]' =>
797 link_to("documentation",
797 link_to("documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
799 :class => "wiki-page"),
799 :class => "wiki-page"),
800 '[[CookBook documentation#One-section]]' =>
800 '[[CookBook documentation#One-section]]' =>
801 link_to("CookBook documentation",
801 link_to("CookBook documentation",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
803 :class => "wiki-page"),
803 :class => "wiki-page"),
804 '[[CookBook documentation#One-section|documentation]]' =>
804 '[[CookBook documentation#One-section|documentation]]' =>
805 link_to("documentation",
805 link_to("documentation",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
807 :class => "wiki-page"),
807 :class => "wiki-page"),
808 # link to the current page
808 # link to the current page
809 '[[Another page]]' =>
809 '[[Another page]]' =>
810 link_to("Another page",
810 link_to("Another page",
811 "/projects/ecookbook/wiki/Another_page",
811 "/projects/ecookbook/wiki/Another_page",
812 :class => "wiki-page"),
812 :class => "wiki-page"),
813 '[[Another page|Page]]' =>
813 '[[Another page|Page]]' =>
814 link_to("Page",
814 link_to("Page",
815 "/projects/ecookbook/wiki/Another_page",
815 "/projects/ecookbook/wiki/Another_page",
816 :class => "wiki-page"),
816 :class => "wiki-page"),
817 '[[Another page#anchor]]' =>
817 '[[Another page#anchor]]' =>
818 link_to("Another page",
818 link_to("Another page",
819 "#anchor",
819 "#anchor",
820 :class => "wiki-page"),
820 :class => "wiki-page"),
821 '[[Another page#anchor|Page]]' =>
821 '[[Another page#anchor|Page]]' =>
822 link_to("Page",
822 link_to("Page",
823 "#anchor",
823 "#anchor",
824 :class => "wiki-page"),
824 :class => "wiki-page"),
825 # page that doesn't exist
825 # page that doesn't exist
826 '[[Unknown page]]' =>
826 '[[Unknown page]]' =>
827 link_to("Unknown page",
827 link_to("Unknown page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
829 :class => "wiki-page new"),
829 :class => "wiki-page new"),
830 '[[Unknown page|404]]' =>
830 '[[Unknown page|404]]' =>
831 link_to("404",
831 link_to("404",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
833 :class => "wiki-page new"),
833 :class => "wiki-page new"),
834 '[[Unknown page#anchor]]' =>
834 '[[Unknown page#anchor]]' =>
835 link_to("Unknown page",
835 link_to("Unknown page",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
837 :class => "wiki-page new"),
837 :class => "wiki-page new"),
838 '[[Unknown page#anchor|404]]' =>
838 '[[Unknown page#anchor|404]]' =>
839 link_to("404",
839 link_to("404",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
841 :class => "wiki-page new"),
841 :class => "wiki-page new"),
842 }
842 }
843 @project = Project.find(1)
843 @project = Project.find(1)
844 to_test.each do |text, result|
844 to_test.each do |text, result|
845 assert_equal "<p>#{result}</p>",
845 assert_equal "<p>#{result}</p>",
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
847 end
847 end
848 end
848 end
849
849
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
851 to_test = {
851 to_test = {
852 # link to a page
852 # link to a page
853 '[[CookBook documentation]]' =>
853 '[[CookBook documentation]]' =>
854 link_to("CookBook documentation",
854 link_to("CookBook documentation",
855 "#CookBook_documentation",
855 "#CookBook_documentation",
856 :class => "wiki-page"),
856 :class => "wiki-page"),
857 '[[CookBook documentation|documentation]]' =>
857 '[[CookBook documentation|documentation]]' =>
858 link_to("documentation",
858 link_to("documentation",
859 "#CookBook_documentation",
859 "#CookBook_documentation",
860 :class => "wiki-page"),
860 :class => "wiki-page"),
861 '[[CookBook documentation#One-section]]' =>
861 '[[CookBook documentation#One-section]]' =>
862 link_to("CookBook documentation",
862 link_to("CookBook documentation",
863 "#CookBook_documentation_One-section",
863 "#CookBook_documentation_One-section",
864 :class => "wiki-page"),
864 :class => "wiki-page"),
865 '[[CookBook documentation#One-section|documentation]]' =>
865 '[[CookBook documentation#One-section|documentation]]' =>
866 link_to("documentation",
866 link_to("documentation",
867 "#CookBook_documentation_One-section",
867 "#CookBook_documentation_One-section",
868 :class => "wiki-page"),
868 :class => "wiki-page"),
869 # page that doesn't exist
869 # page that doesn't exist
870 '[[Unknown page]]' =>
870 '[[Unknown page]]' =>
871 link_to("Unknown page",
871 link_to("Unknown page",
872 "#Unknown_page",
872 "#Unknown_page",
873 :class => "wiki-page new"),
873 :class => "wiki-page new"),
874 '[[Unknown page|404]]' =>
874 '[[Unknown page|404]]' =>
875 link_to("404",
875 link_to("404",
876 "#Unknown_page",
876 "#Unknown_page",
877 :class => "wiki-page new"),
877 :class => "wiki-page new"),
878 '[[Unknown page#anchor]]' =>
878 '[[Unknown page#anchor]]' =>
879 link_to("Unknown page",
879 link_to("Unknown page",
880 "#Unknown_page_anchor",
880 "#Unknown_page_anchor",
881 :class => "wiki-page new"),
881 :class => "wiki-page new"),
882 '[[Unknown page#anchor|404]]' =>
882 '[[Unknown page#anchor|404]]' =>
883 link_to("404",
883 link_to("404",
884 "#Unknown_page_anchor",
884 "#Unknown_page_anchor",
885 :class => "wiki-page new"),
885 :class => "wiki-page new"),
886 }
886 }
887 @project = Project.find(1)
887 @project = Project.find(1)
888 to_test.each do |text, result|
888 to_test.each do |text, result|
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
890 end
890 end
891 end
891 end
892
892
893 def test_html_tags
893 def test_html_tags
894 to_test = {
894 to_test = {
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
898 # do not escape pre/code tags
898 # do not escape pre/code tags
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
904 # remove attributes except class
904 # remove attributes except class
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
910 # xss
910 # xss
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
913 }
913 }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
915 end
915 end
916
916
917 def test_allowed_html_tags
917 def test_allowed_html_tags
918 to_test = {
918 to_test = {
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
922 }
922 }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
924 end
924 end
925
925
926 def test_pre_tags
926 def test_pre_tags
927 raw = <<-RAW
927 raw = <<-RAW
928 Before
928 Before
929
929
930 <pre>
930 <pre>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
932 </pre>
932 </pre>
933
933
934 After
934 After
935 RAW
935 RAW
936
936
937 expected = <<-EXPECTED
937 expected = <<-EXPECTED
938 <p>Before</p>
938 <p>Before</p>
939 <pre>
939 <pre>
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
941 </pre>
941 </pre>
942 <p>After</p>
942 <p>After</p>
943 EXPECTED
943 EXPECTED
944
944
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
946 end
946 end
947
947
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
949 raw = <<-RAW
949 raw = <<-RAW
950 [[CookBook documentation]]
950 [[CookBook documentation]]
951
951
952 #1
952 #1
953
953
954 <pre>
954 <pre>
955 [[CookBook documentation]]
955 [[CookBook documentation]]
956
956
957 #1
957 #1
958 </pre>
958 </pre>
959 RAW
959 RAW
960
960
961 result1 = link_to("CookBook documentation",
961 result1 = link_to("CookBook documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
963 :class => "wiki-page")
963 :class => "wiki-page")
964 result2 = link_to('#1',
964 result2 = link_to('#1',
965 "/issues/1",
965 "/issues/1",
966 :class => Issue.find(1).css_classes,
966 :class => Issue.find(1).css_classes,
967 :title => "Bug: Cannot print recipes (New)")
967 :title => "Bug: Cannot print recipes (New)")
968
968
969 expected = <<-EXPECTED
969 expected = <<-EXPECTED
970 <p>#{result1}</p>
970 <p>#{result1}</p>
971 <p>#{result2}</p>
971 <p>#{result2}</p>
972 <pre>
972 <pre>
973 [[CookBook documentation]]
973 [[CookBook documentation]]
974
974
975 #1
975 #1
976 </pre>
976 </pre>
977 EXPECTED
977 EXPECTED
978
978
979 @project = Project.find(1)
979 @project = Project.find(1)
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
981 end
981 end
982
982
983 def test_non_closing_pre_blocks_should_be_closed
983 def test_non_closing_pre_blocks_should_be_closed
984 raw = <<-RAW
984 raw = <<-RAW
985 <pre><code>
985 <pre><code>
986 RAW
986 RAW
987
987
988 expected = <<-EXPECTED
988 expected = <<-EXPECTED
989 <pre><code>
989 <pre><code>
990 </code></pre>
990 </code></pre>
991 EXPECTED
991 EXPECTED
992
992
993 @project = Project.find(1)
993 @project = Project.find(1)
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
995 end
995 end
996
996
997 def test_unbalanced_closing_pre_tag_should_not_error
997 def test_unbalanced_closing_pre_tag_should_not_error
998 assert_nothing_raised do
998 assert_nothing_raised do
999 textilizable("unbalanced</pre>")
999 textilizable("unbalanced</pre>")
1000 end
1000 end
1001 end
1001 end
1002
1002
1003 def test_syntax_highlight
1003 def test_syntax_highlight
1004 raw = <<-RAW
1004 raw = <<-RAW
1005 <pre><code class="ruby">
1005 <pre><code class="ruby">
1006 # Some ruby code here
1006 # Some ruby code here
1007 </code></pre>
1007 </code></pre>
1008 RAW
1008 RAW
1009
1009
1010 expected = <<-EXPECTED
1010 expected = <<-EXPECTED
1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1012 </code></pre>
1012 </code></pre>
1013 EXPECTED
1013 EXPECTED
1014
1014
1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1016 end
1016 end
1017
1017
1018 def test_to_path_param
1018 def test_to_path_param
1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1022 assert_equal nil, to_path_param('/')
1022 assert_equal nil, to_path_param('/')
1023 end
1023 end
1024
1024
1025 def test_wiki_links_in_tables
1025 def test_wiki_links_in_tables
1026 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1026 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1027 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1027 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1028 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1028 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1030 result = "<tr><td>#{link1}</td>" +
1030 result = "<tr><td>#{link1}</td>" +
1031 "<td>#{link2}</td>" +
1031 "<td>#{link2}</td>" +
1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1033 @project = Project.find(1)
1033 @project = Project.find(1)
1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1035 end
1035 end
1036
1036
1037 def test_text_formatting
1037 def test_text_formatting
1038 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1038 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1040 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1040 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1041 '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>',
1041 '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>',
1042 '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',
1042 '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',
1043 }
1043 }
1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1045 end
1045 end
1046
1046
1047 def test_wiki_horizontal_rule
1047 def test_wiki_horizontal_rule
1048 assert_equal '<hr />', textilizable('---')
1048 assert_equal '<hr />', textilizable('---')
1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1050 end
1050 end
1051
1051
1052 def test_footnotes
1052 def test_footnotes
1053 raw = <<-RAW
1053 raw = <<-RAW
1054 This is some text[1].
1054 This is some text[1].
1055
1055
1056 fn1. This is the foot note
1056 fn1. This is the foot note
1057 RAW
1057 RAW
1058
1058
1059 expected = <<-EXPECTED
1059 expected = <<-EXPECTED
1060 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1060 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1062 EXPECTED
1062 EXPECTED
1063
1063
1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1065 end
1065 end
1066
1066
1067 def test_headings
1067 def test_headings
1068 raw = 'h1. Some heading'
1068 raw = 'h1. Some heading'
1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1070
1070
1071 assert_equal expected, textilizable(raw)
1071 assert_equal expected, textilizable(raw)
1072 end
1072 end
1073
1073
1074 def test_headings_with_special_chars
1074 def test_headings_with_special_chars
1075 # This test makes sure that the generated anchor names match the expected
1075 # This test makes sure that the generated anchor names match the expected
1076 # ones even if the heading text contains unconventional characters
1076 # ones even if the heading text contains unconventional characters
1077 raw = 'h1. Some heading related to version 0.5'
1077 raw = 'h1. Some heading related to version 0.5'
1078 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1078 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1080
1080
1081 assert_equal expected, textilizable(raw)
1081 assert_equal expected, textilizable(raw)
1082 end
1082 end
1083
1083
1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1087
1087
1088 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1088 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1089
1089
1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1091 end
1091 end
1092
1092
1093 def test_table_of_content
1093 def test_table_of_content
1094 raw = <<-RAW
1094 raw = <<-RAW
1095 {{toc}}
1095 {{toc}}
1096
1096
1097 h1. Title
1097 h1. Title
1098
1098
1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1100
1100
1101 h2. Subtitle with a [[Wiki]] link
1101 h2. Subtitle with a [[Wiki]] link
1102
1102
1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1104
1104
1105 h2. Subtitle with [[Wiki|another Wiki]] link
1105 h2. Subtitle with [[Wiki|another Wiki]] link
1106
1106
1107 h2. Subtitle with %{color:red}red text%
1107 h2. Subtitle with %{color:red}red text%
1108
1108
1109 <pre>
1109 <pre>
1110 some code
1110 some code
1111 </pre>
1111 </pre>
1112
1112
1113 h3. Subtitle with *some* _modifiers_
1113 h3. Subtitle with *some* _modifiers_
1114
1114
1115 h3. Subtitle with @inline code@
1115 h3. Subtitle with @inline code@
1116
1116
1117 h1. Another title
1117 h1. Another title
1118
1118
1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1120
1120
1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1122
1122
1123 RAW
1123 RAW
1124
1124
1125 expected = '<ul class="toc">' +
1125 expected = '<ul class="toc">' +
1126 '<li><a href="#Title">Title</a>' +
1126 '<li><a href="#Title">Title</a>' +
1127 '<ul>' +
1127 '<ul>' +
1128 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1128 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1129 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1129 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1131 '<ul>' +
1131 '<ul>' +
1132 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1132 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1134 '</ul>' +
1134 '</ul>' +
1135 '</li>' +
1135 '</li>' +
1136 '</ul>' +
1136 '</ul>' +
1137 '</li>' +
1137 '</li>' +
1138 '<li><a href="#Another-title">Another title</a>' +
1138 '<li><a href="#Another-title">Another title</a>' +
1139 '<ul>' +
1139 '<ul>' +
1140 '<li>' +
1140 '<li>' +
1141 '<ul>' +
1141 '<ul>' +
1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1143 '</ul>' +
1143 '</ul>' +
1144 '</li>' +
1144 '</li>' +
1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1146 '</ul>' +
1146 '</ul>' +
1147 '</li>' +
1147 '</li>' +
1148 '</ul>'
1148 '</ul>'
1149
1149
1150 @project = Project.find(1)
1150 @project = Project.find(1)
1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1152 end
1152 end
1153
1153
1154 def test_table_of_content_should_generate_unique_anchors
1154 def test_table_of_content_should_generate_unique_anchors
1155 raw = <<-RAW
1155 raw = <<-RAW
1156 {{toc}}
1156 {{toc}}
1157
1157
1158 h1. Title
1158 h1. Title
1159
1159
1160 h2. Subtitle
1160 h2. Subtitle
1161
1161
1162 h2. Subtitle
1162 h2. Subtitle
1163 RAW
1163 RAW
1164
1164
1165 expected = '<ul class="toc">' +
1165 expected = '<ul class="toc">' +
1166 '<li><a href="#Title">Title</a>' +
1166 '<li><a href="#Title">Title</a>' +
1167 '<ul>' +
1167 '<ul>' +
1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1169 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1169 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1170 '</ul>'
1170 '</ul>'
1171 '</li>' +
1171 '</li>' +
1172 '</ul>'
1172 '</ul>'
1173
1173
1174 @project = Project.find(1)
1174 @project = Project.find(1)
1175 result = textilizable(raw).gsub("\n", "")
1175 result = textilizable(raw).gsub("\n", "")
1176 assert_include expected, result
1176 assert_include expected, result
1177 assert_include '<a name="Subtitle">', result
1177 assert_include '<a name="Subtitle">', result
1178 assert_include '<a name="Subtitle-2">', result
1178 assert_include '<a name="Subtitle-2">', result
1179 end
1179 end
1180
1180
1181 def test_table_of_content_should_contain_included_page_headings
1181 def test_table_of_content_should_contain_included_page_headings
1182 raw = <<-RAW
1182 raw = <<-RAW
1183 {{toc}}
1183 {{toc}}
1184
1184
1185 h1. Included
1185 h1. Included
1186
1186
1187 {{include(Child_1)}}
1187 {{include(Child_1)}}
1188 RAW
1188 RAW
1189
1189
1190 expected = '<ul class="toc">' +
1190 expected = '<ul class="toc">' +
1191 '<li><a href="#Included">Included</a></li>' +
1191 '<li><a href="#Included">Included</a></li>' +
1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1193 '</ul>'
1193 '</ul>'
1194
1194
1195 @project = Project.find(1)
1195 @project = Project.find(1)
1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1197 end
1197 end
1198
1198
1199 def test_toc_with_textile_formatting_should_be_parsed
1199 def test_toc_with_textile_formatting_should_be_parsed
1200 with_settings :text_formatting => 'textile' do
1200 with_settings :text_formatting => 'textile' do
1201 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1201 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1202 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1202 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1204 end
1204 end
1205 end
1205 end
1206
1206
1207 if Object.const_defined?(:Redcarpet)
1207 if Object.const_defined?(:Redcarpet)
1208 def test_toc_with_markdown_formatting_should_be_parsed
1208 def test_toc_with_markdown_formatting_should_be_parsed
1209 with_settings :text_formatting => 'markdown' do
1209 with_settings :text_formatting => 'markdown' do
1210 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1210 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1211 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1211 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1213 end
1213 end
1214 end
1214 end
1215 end
1215 end
1216
1216
1217 def test_section_edit_links
1217 def test_section_edit_links
1218 raw = <<-RAW
1218 raw = <<-RAW
1219 h1. Title
1219 h1. Title
1220
1220
1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1222
1222
1223 h2. Subtitle with a [[Wiki]] link
1223 h2. Subtitle with a [[Wiki]] link
1224
1224
1225 h2. Subtitle with *some* _modifiers_
1225 h2. Subtitle with *some* _modifiers_
1226
1226
1227 h2. Subtitle with @inline code@
1227 h2. Subtitle with @inline code@
1228
1228
1229 <pre>
1229 <pre>
1230 some code
1230 some code
1231
1231
1232 h2. heading inside pre
1232 h2. heading inside pre
1233
1233
1234 <h2>html heading inside pre</h2>
1234 <h2>html heading inside pre</h2>
1235 </pre>
1235 </pre>
1236
1236
1237 h2. Subtitle after pre tag
1237 h2. Subtitle after pre tag
1238 RAW
1238 RAW
1239
1239
1240 @project = Project.find(1)
1240 @project = Project.find(1)
1241 set_language_if_valid 'en'
1241 set_language_if_valid 'en'
1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1243
1243
1244 # heading that contains inline code
1244 # heading that contains inline code
1245 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-4">' +
1245 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-4">' +
1246 '<a href="/projects/1/wiki/Test/edit\?section=4"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1246 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=4"></a></div>' +
1247 '<a name="Subtitle-with-inline-code"></a>' +
1247 '<a name="Subtitle-with-inline-code"></a>' +
1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1249 result
1249 result
1250
1250
1251 # last heading
1251 # last heading
1252 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-5">' +
1252 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-5">' +
1253 '<a href="/projects/1/wiki/Test/edit\?section=5"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1253 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=5"></a></div>' +
1254 '<a name="Subtitle-after-pre-tag"></a>' +
1254 '<a name="Subtitle-after-pre-tag"></a>' +
1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1256 result
1256 result
1257 end
1257 end
1258
1258
1259 def test_default_formatter
1259 def test_default_formatter
1260 with_settings :text_formatting => 'unknown' do
1260 with_settings :text_formatting => 'unknown' do
1261 text = 'a *link*: http://www.example.net/'
1261 text = 'a *link*: http://www.example.net/'
1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1263 end
1263 end
1264 end
1264 end
1265
1265
1266 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1266 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1267 text = '<a>http://example.com</a>'
1267 text = '<a>http://example.com</a>'
1268 expected = text.dup
1268 expected = text.dup
1269 parse_redmine_links(text, nil, nil, nil, true, {})
1269 parse_redmine_links(text, nil, nil, nil, true, {})
1270 assert_equal expected, text
1270 assert_equal expected, text
1271 end
1271 end
1272
1272
1273 def test_due_date_distance_in_words
1273 def test_due_date_distance_in_words
1274 to_test = { Date.today => 'Due in 0 days',
1274 to_test = { Date.today => 'Due in 0 days',
1275 Date.today + 1 => 'Due in 1 day',
1275 Date.today + 1 => 'Due in 1 day',
1276 Date.today + 100 => 'Due in about 3 months',
1276 Date.today + 100 => 'Due in about 3 months',
1277 Date.today + 20000 => 'Due in over 54 years',
1277 Date.today + 20000 => 'Due in over 54 years',
1278 Date.today - 1 => '1 day late',
1278 Date.today - 1 => '1 day late',
1279 Date.today - 100 => 'about 3 months late',
1279 Date.today - 100 => 'about 3 months late',
1280 Date.today - 20000 => 'over 54 years late',
1280 Date.today - 20000 => 'over 54 years late',
1281 }
1281 }
1282 ::I18n.locale = :en
1282 ::I18n.locale = :en
1283 to_test.each do |date, expected|
1283 to_test.each do |date, expected|
1284 assert_equal expected, due_date_distance_in_words(date)
1284 assert_equal expected, due_date_distance_in_words(date)
1285 end
1285 end
1286 end
1286 end
1287
1287
1288 def test_avatar_enabled
1288 def test_avatar_enabled
1289 with_settings :gravatar_enabled => '1' do
1289 with_settings :gravatar_enabled => '1' do
1290 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1290 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1291 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1291 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1292 # Default size is 50
1292 # Default size is 50
1293 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1293 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1294 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1294 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1295 # Non-avatar options should be considered html options
1295 # Non-avatar options should be considered html options
1296 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1296 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1297 # The default class of the img tag should be gravatar
1297 # The default class of the img tag should be gravatar
1298 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1298 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1299 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1299 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1300 assert_nil avatar('jsmith')
1300 assert_nil avatar('jsmith')
1301 assert_nil avatar(nil)
1301 assert_nil avatar(nil)
1302 end
1302 end
1303 end
1303 end
1304
1304
1305 def test_avatar_disabled
1305 def test_avatar_disabled
1306 with_settings :gravatar_enabled => '0' do
1306 with_settings :gravatar_enabled => '0' do
1307 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1307 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1308 end
1308 end
1309 end
1309 end
1310
1310
1311 def test_link_to_user
1311 def test_link_to_user
1312 user = User.find(2)
1312 user = User.find(2)
1313 result = link_to("John Smith", "/users/2", :class => "user active")
1313 result = link_to("John Smith", "/users/2", :class => "user active")
1314 assert_equal result, link_to_user(user)
1314 assert_equal result, link_to_user(user)
1315 end
1315 end
1316
1316
1317 def test_link_to_user_should_not_link_to_locked_user
1317 def test_link_to_user_should_not_link_to_locked_user
1318 with_current_user nil do
1318 with_current_user nil do
1319 user = User.find(5)
1319 user = User.find(5)
1320 assert user.locked?
1320 assert user.locked?
1321 assert_equal 'Dave2 Lopper2', link_to_user(user)
1321 assert_equal 'Dave2 Lopper2', link_to_user(user)
1322 end
1322 end
1323 end
1323 end
1324
1324
1325 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1325 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1326 with_current_user User.find(1) do
1326 with_current_user User.find(1) do
1327 user = User.find(5)
1327 user = User.find(5)
1328 assert user.locked?
1328 assert user.locked?
1329 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1329 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1330 assert_equal result, link_to_user(user)
1330 assert_equal result, link_to_user(user)
1331 end
1331 end
1332 end
1332 end
1333
1333
1334 def test_link_to_user_should_not_link_to_anonymous
1334 def test_link_to_user_should_not_link_to_anonymous
1335 user = User.anonymous
1335 user = User.anonymous
1336 assert user.anonymous?
1336 assert user.anonymous?
1337 t = link_to_user(user)
1337 t = link_to_user(user)
1338 assert_equal ::I18n.t(:label_user_anonymous), t
1338 assert_equal ::I18n.t(:label_user_anonymous), t
1339 end
1339 end
1340
1340
1341 def test_link_to_attachment
1341 def test_link_to_attachment
1342 a = Attachment.find(3)
1342 a = Attachment.find(3)
1343 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1343 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1344 link_to_attachment(a)
1344 link_to_attachment(a)
1345 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1345 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1346 link_to_attachment(a, :text => 'Text')
1346 link_to_attachment(a, :text => 'Text')
1347 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1347 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1348 assert_equal result,
1348 assert_equal result,
1349 link_to_attachment(a, :class => 'foo')
1349 link_to_attachment(a, :class => 'foo')
1350 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1350 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1351 link_to_attachment(a, :download => true)
1351 link_to_attachment(a, :download => true)
1352 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1352 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1353 link_to_attachment(a, :only_path => false)
1353 link_to_attachment(a, :only_path => false)
1354 end
1354 end
1355
1355
1356 def test_thumbnail_tag
1356 def test_thumbnail_tag
1357 a = Attachment.find(3)
1357 a = Attachment.find(3)
1358 assert_select_in thumbnail_tag(a),
1358 assert_select_in thumbnail_tag(a),
1359 'a[href=?][title=?] img[alt="3"][src=?]',
1359 'a[href=?][title=?] img[alt="3"][src=?]',
1360 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1360 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1361 end
1361 end
1362
1362
1363 def test_link_to_project
1363 def test_link_to_project
1364 project = Project.find(1)
1364 project = Project.find(1)
1365 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1365 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1366 link_to_project(project)
1366 link_to_project(project)
1367 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1367 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1368 link_to_project(project, {:only_path => false, :jump => 'blah'})
1368 link_to_project(project, {:only_path => false, :jump => 'blah'})
1369 end
1369 end
1370
1370
1371 def test_link_to_project_settings
1371 def test_link_to_project_settings
1372 project = Project.find(1)
1372 project = Project.find(1)
1373 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1373 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1374
1374
1375 project.status = Project::STATUS_CLOSED
1375 project.status = Project::STATUS_CLOSED
1376 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1376 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1377
1377
1378 project.status = Project::STATUS_ARCHIVED
1378 project.status = Project::STATUS_ARCHIVED
1379 assert_equal 'eCookbook', link_to_project_settings(project)
1379 assert_equal 'eCookbook', link_to_project_settings(project)
1380 end
1380 end
1381
1381
1382 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1382 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1383 # numeric identifier are no longer allowed
1383 # numeric identifier are no longer allowed
1384 Project.where(:id => 1).update_all(:identifier => 25)
1384 Project.where(:id => 1).update_all(:identifier => 25)
1385 assert_equal '<a href="/projects/1">eCookbook</a>',
1385 assert_equal '<a href="/projects/1">eCookbook</a>',
1386 link_to_project(Project.find(1))
1386 link_to_project(Project.find(1))
1387 end
1387 end
1388
1388
1389 def test_principals_options_for_select_with_users
1389 def test_principals_options_for_select_with_users
1390 User.current = nil
1390 User.current = nil
1391 users = [User.find(2), User.find(4)]
1391 users = [User.find(2), User.find(4)]
1392 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1392 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1393 principals_options_for_select(users)
1393 principals_options_for_select(users)
1394 end
1394 end
1395
1395
1396 def test_principals_options_for_select_with_selected
1396 def test_principals_options_for_select_with_selected
1397 User.current = nil
1397 User.current = nil
1398 users = [User.find(2), User.find(4)]
1398 users = [User.find(2), User.find(4)]
1399 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1399 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1400 principals_options_for_select(users, User.find(4))
1400 principals_options_for_select(users, User.find(4))
1401 end
1401 end
1402
1402
1403 def test_principals_options_for_select_with_users_and_groups
1403 def test_principals_options_for_select_with_users_and_groups
1404 User.current = nil
1404 User.current = nil
1405 set_language_if_valid 'en'
1405 set_language_if_valid 'en'
1406 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1406 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1407 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1407 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1408 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1408 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1409 principals_options_for_select(users)
1409 principals_options_for_select(users)
1410 end
1410 end
1411
1411
1412 def test_principals_options_for_select_with_empty_collection
1412 def test_principals_options_for_select_with_empty_collection
1413 assert_equal '', principals_options_for_select([])
1413 assert_equal '', principals_options_for_select([])
1414 end
1414 end
1415
1415
1416 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1416 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1417 set_language_if_valid 'en'
1417 set_language_if_valid 'en'
1418 users = [User.find(2), User.find(4)]
1418 users = [User.find(2), User.find(4)]
1419 User.current = User.find(4)
1419 User.current = User.find(4)
1420 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1420 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1421 end
1421 end
1422
1422
1423 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1423 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1424 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1424 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1425 end
1425 end
1426
1426
1427 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1427 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1428 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1428 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1429 end
1429 end
1430
1430
1431 def test_image_tag_should_pick_the_default_image
1431 def test_image_tag_should_pick_the_default_image
1432 assert_match 'src="/images/image.png"', image_tag("image.png")
1432 assert_match 'src="/images/image.png"', image_tag("image.png")
1433 end
1433 end
1434
1434
1435 def test_image_tag_should_pick_the_theme_image_if_it_exists
1435 def test_image_tag_should_pick_the_theme_image_if_it_exists
1436 theme = Redmine::Themes.themes.last
1436 theme = Redmine::Themes.themes.last
1437 theme.images << 'image.png'
1437 theme.images << 'image.png'
1438
1438
1439 with_settings :ui_theme => theme.id do
1439 with_settings :ui_theme => theme.id do
1440 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1440 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1441 assert_match %|src="/images/other.png"|, image_tag("other.png")
1441 assert_match %|src="/images/other.png"|, image_tag("other.png")
1442 end
1442 end
1443 ensure
1443 ensure
1444 theme.images.delete 'image.png'
1444 theme.images.delete 'image.png'
1445 end
1445 end
1446
1446
1447 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1447 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1448 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1448 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1449 end
1449 end
1450
1450
1451 def test_javascript_include_tag_should_pick_the_default_javascript
1451 def test_javascript_include_tag_should_pick_the_default_javascript
1452 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1452 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1453 end
1453 end
1454
1454
1455 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1455 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1456 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1456 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1457 end
1457 end
1458
1458
1459 def test_raw_json_should_escape_closing_tags
1459 def test_raw_json_should_escape_closing_tags
1460 s = raw_json(["<foo>bar</foo>"])
1460 s = raw_json(["<foo>bar</foo>"])
1461 assert_include '\/foo', s
1461 assert_include '\/foo', s
1462 end
1462 end
1463
1463
1464 def test_raw_json_should_be_html_safe
1464 def test_raw_json_should_be_html_safe
1465 s = raw_json(["foo"])
1465 s = raw_json(["foo"])
1466 assert s.html_safe?
1466 assert s.html_safe?
1467 end
1467 end
1468
1468
1469 def test_html_title_should_app_title_if_not_set
1469 def test_html_title_should_app_title_if_not_set
1470 assert_equal 'Redmine', html_title
1470 assert_equal 'Redmine', html_title
1471 end
1471 end
1472
1472
1473 def test_html_title_should_join_items
1473 def test_html_title_should_join_items
1474 html_title 'Foo', 'Bar'
1474 html_title 'Foo', 'Bar'
1475 assert_equal 'Foo - Bar - Redmine', html_title
1475 assert_equal 'Foo - Bar - Redmine', html_title
1476 end
1476 end
1477
1477
1478 def test_html_title_should_append_current_project_name
1478 def test_html_title_should_append_current_project_name
1479 @project = Project.find(1)
1479 @project = Project.find(1)
1480 html_title 'Foo', 'Bar'
1480 html_title 'Foo', 'Bar'
1481 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1481 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1482 end
1482 end
1483
1483
1484 def test_title_should_return_a_h2_tag
1484 def test_title_should_return_a_h2_tag
1485 assert_equal '<h2>Foo</h2>', title('Foo')
1485 assert_equal '<h2>Foo</h2>', title('Foo')
1486 end
1486 end
1487
1487
1488 def test_title_should_set_html_title
1488 def test_title_should_set_html_title
1489 title('Foo')
1489 title('Foo')
1490 assert_equal 'Foo - Redmine', html_title
1490 assert_equal 'Foo - Redmine', html_title
1491 end
1491 end
1492
1492
1493 def test_title_should_turn_arrays_into_links
1493 def test_title_should_turn_arrays_into_links
1494 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1494 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1495 assert_equal 'Foo - Redmine', html_title
1495 assert_equal 'Foo - Redmine', html_title
1496 end
1496 end
1497
1497
1498 def test_title_should_join_items
1498 def test_title_should_join_items
1499 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1499 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1500 assert_equal 'Bar - Foo - Redmine', html_title
1500 assert_equal 'Bar - Foo - Redmine', html_title
1501 end
1501 end
1502
1502
1503 def test_favicon_path
1503 def test_favicon_path
1504 assert_match %r{^/favicon\.ico}, favicon_path
1504 assert_match %r{^/favicon\.ico}, favicon_path
1505 end
1505 end
1506
1506
1507 def test_favicon_path_with_suburi
1507 def test_favicon_path_with_suburi
1508 Redmine::Utils.relative_url_root = '/foo'
1508 Redmine::Utils.relative_url_root = '/foo'
1509 assert_match %r{^/foo/favicon\.ico}, favicon_path
1509 assert_match %r{^/foo/favicon\.ico}, favicon_path
1510 ensure
1510 ensure
1511 Redmine::Utils.relative_url_root = ''
1511 Redmine::Utils.relative_url_root = ''
1512 end
1512 end
1513
1513
1514 def test_favicon_url
1514 def test_favicon_url
1515 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1515 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1516 end
1516 end
1517
1517
1518 def test_favicon_url_with_suburi
1518 def test_favicon_url_with_suburi
1519 Redmine::Utils.relative_url_root = '/foo'
1519 Redmine::Utils.relative_url_root = '/foo'
1520 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1520 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1521 ensure
1521 ensure
1522 Redmine::Utils.relative_url_root = ''
1522 Redmine::Utils.relative_url_root = ''
1523 end
1523 end
1524
1524
1525 def test_truncate_single_line
1525 def test_truncate_single_line
1526 str = "01234"
1526 str = "01234"
1527 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1527 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1528 assert_equal "01234 0...", result
1528 assert_equal "01234 0...", result
1529 assert !result.html_safe?
1529 assert !result.html_safe?
1530 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1530 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1531 assert_equal "01234<&#> 012...", result
1531 assert_equal "01234<&#> 012...", result
1532 assert !result.html_safe?
1532 assert !result.html_safe?
1533 end
1533 end
1534
1534
1535 def test_truncate_single_line_non_ascii
1535 def test_truncate_single_line_non_ascii
1536 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1536 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1537 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1537 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1538 assert_equal "#{ja} #{ja}...", result
1538 assert_equal "#{ja} #{ja}...", result
1539 assert !result.html_safe?
1539 assert !result.html_safe?
1540 end
1540 end
1541 end
1541 end
General Comments 0
You need to be logged in to leave comments. Login now