##// END OF EJS Templates
Merged r14108 (#19348)....
Jean-Philippe Lang -
r13727:767064c23a1c
parent child
Show More

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

@@ -1,1321 +1,1321
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
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = issue.subject.truncate(60)
75 title = issue.subject.truncate(60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if truncate_length = options[:truncate]
78 if truncate_length = options[:truncate]
79 subject = subject.truncate(truncate_length)
79 subject = subject.truncate(truncate_length)
80 end
80 end
81 end
81 end
82 only_path = options[:only_path].nil? ? true : options[:only_path]
82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to(text, issue_url(issue, :only_path => only_path),
83 s = link_to(text, issue_url(issue, :only_path => only_path),
84 :class => issue.css_classes, :title => title)
84 :class => issue.css_classes, :title => title)
85 s << h(": #{subject}") if subject
85 s << h(": #{subject}") if subject
86 s = h("#{issue.project} - ") + s if options[:project]
86 s = h("#{issue.project} - ") + s if options[:project]
87 s
87 s
88 end
88 end
89
89
90 # Generates a link to an attachment.
90 # Generates a link to an attachment.
91 # Options:
91 # Options:
92 # * :text - Link text (default to attachment filename)
92 # * :text - Link text (default to attachment filename)
93 # * :download - Force download (default: false)
93 # * :download - Force download (default: false)
94 def link_to_attachment(attachment, options={})
94 def link_to_attachment(attachment, options={})
95 text = options.delete(:text) || attachment.filename
95 text = options.delete(:text) || attachment.filename
96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
96 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97 html_options = options.slice!(:only_path)
97 html_options = options.slice!(:only_path)
98 options[:only_path] = true unless options.key?(:only_path)
98 options[:only_path] = true unless options.key?(:only_path)
99 url = send(route_method, attachment, attachment.filename, options)
99 url = send(route_method, attachment, attachment.filename, options)
100 link_to text, url, html_options
100 link_to text, url, html_options
101 end
101 end
102
102
103 # Generates a link to a SCM revision
103 # Generates a link to a SCM revision
104 # Options:
104 # Options:
105 # * :text - Link text (default to the formatted revision)
105 # * :text - Link text (default to the formatted revision)
106 def link_to_revision(revision, repository, options={})
106 def link_to_revision(revision, repository, options={})
107 if repository.is_a?(Project)
107 if repository.is_a?(Project)
108 repository = repository.repository
108 repository = repository.repository
109 end
109 end
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
112 link_to(
113 h(text),
113 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision)),
115 :title => l(:label_revision_id, format_revision(revision)),
116 :accesskey => options[:accesskey]
116 :accesskey => options[:accesskey]
117 )
117 )
118 end
118 end
119
119
120 # Generates a link to a message
120 # Generates a link to a message
121 def link_to_message(message, options={}, html_options = nil)
121 def link_to_message(message, options={}, html_options = nil)
122 link_to(
122 link_to(
123 message.subject.truncate(60),
123 message.subject.truncate(60),
124 board_message_url(message.board_id, message.parent_id || message.id, {
124 board_message_url(message.board_id, message.parent_id || message.id, {
125 :r => (message.parent_id && message.id),
125 :r => (message.parent_id && message.id),
126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
126 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
127 :only_path => true
127 :only_path => true
128 }.merge(options)),
128 }.merge(options)),
129 html_options
129 html_options
130 )
130 )
131 end
131 end
132
132
133 # Generates a link to a project if active
133 # Generates a link to a project if active
134 # Examples:
134 # Examples:
135 #
135 #
136 # link_to_project(project) # => link to the specified project overview
136 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
137 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
138 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 #
139 #
140 def link_to_project(project, options={}, html_options = nil)
140 def link_to_project(project, options={}, html_options = nil)
141 if project.archived?
141 if project.archived?
142 h(project.name)
142 h(project.name)
143 else
143 else
144 link_to project.name,
144 link_to project.name,
145 project_url(project, {:only_path => true}.merge(options)),
145 project_url(project, {:only_path => true}.merge(options)),
146 html_options
146 html_options
147 end
147 end
148 end
148 end
149
149
150 # Generates a link to a project settings if active
150 # Generates a link to a project settings if active
151 def link_to_project_settings(project, options={}, html_options=nil)
151 def link_to_project_settings(project, options={}, html_options=nil)
152 if project.active?
152 if project.active?
153 link_to project.name, settings_project_path(project, options), html_options
153 link_to project.name, settings_project_path(project, options), html_options
154 elsif project.archived?
154 elsif project.archived?
155 h(project.name)
155 h(project.name)
156 else
156 else
157 link_to project.name, project_path(project, options), html_options
157 link_to project.name, project_path(project, options), html_options
158 end
158 end
159 end
159 end
160
160
161 # Generates a link to a version
161 # Generates a link to a version
162 def link_to_version(version, options = {})
162 def link_to_version(version, options = {})
163 return '' unless version && version.is_a?(Version)
163 return '' unless version && version.is_a?(Version)
164 options = {:title => format_date(version.effective_date)}.merge(options)
164 options = {:title => format_date(version.effective_date)}.merge(options)
165 link_to_if version.visible?, format_version_name(version), version_path(version), options
165 link_to_if version.visible?, format_version_name(version), version_path(version), options
166 end
166 end
167
167
168 # Helper that formats object for html or text rendering
168 # Helper that formats object for html or text rendering
169 def format_object(object, html=true, &block)
169 def format_object(object, html=true, &block)
170 if block_given?
170 if block_given?
171 object = yield object
171 object = yield object
172 end
172 end
173 case object.class.name
173 case object.class.name
174 when 'Array'
174 when 'Array'
175 object.map {|o| format_object(o, html)}.join(', ').html_safe
175 object.map {|o| format_object(o, html)}.join(', ').html_safe
176 when 'Time'
176 when 'Time'
177 format_time(object)
177 format_time(object)
178 when 'Date'
178 when 'Date'
179 format_date(object)
179 format_date(object)
180 when 'Fixnum'
180 when 'Fixnum'
181 object.to_s
181 object.to_s
182 when 'Float'
182 when 'Float'
183 sprintf "%.2f", object
183 sprintf "%.2f", object
184 when 'User'
184 when 'User'
185 html ? link_to_user(object) : object.to_s
185 html ? link_to_user(object) : object.to_s
186 when 'Project'
186 when 'Project'
187 html ? link_to_project(object) : object.to_s
187 html ? link_to_project(object) : object.to_s
188 when 'Version'
188 when 'Version'
189 html ? link_to_version(object) : object.to_s
189 html ? link_to_version(object) : object.to_s
190 when 'TrueClass'
190 when 'TrueClass'
191 l(:general_text_Yes)
191 l(:general_text_Yes)
192 when 'FalseClass'
192 when 'FalseClass'
193 l(:general_text_No)
193 l(:general_text_No)
194 when 'Issue'
194 when 'Issue'
195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
195 object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 when 'CustomValue', 'CustomFieldValue'
196 when 'CustomValue', 'CustomFieldValue'
197 if object.custom_field
197 if object.custom_field
198 f = object.custom_field.format.formatted_custom_value(self, object, html)
198 f = object.custom_field.format.formatted_custom_value(self, object, html)
199 if f.nil? || f.is_a?(String)
199 if f.nil? || f.is_a?(String)
200 f
200 f
201 else
201 else
202 format_object(f, html, &block)
202 format_object(f, html, &block)
203 end
203 end
204 else
204 else
205 object.value.to_s
205 object.value.to_s
206 end
206 end
207 else
207 else
208 html ? h(object) : object.to_s
208 html ? h(object) : object.to_s
209 end
209 end
210 end
210 end
211
211
212 def wiki_page_path(page, options={})
212 def wiki_page_path(page, options={})
213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
213 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
214 end
214 end
215
215
216 def thumbnail_tag(attachment)
216 def thumbnail_tag(attachment)
217 link_to image_tag(thumbnail_path(attachment)),
217 link_to image_tag(thumbnail_path(attachment)),
218 named_attachment_path(attachment, attachment.filename),
218 named_attachment_path(attachment, attachment.filename),
219 :title => attachment.filename
219 :title => attachment.filename
220 end
220 end
221
221
222 def toggle_link(name, id, options={})
222 def toggle_link(name, id, options={})
223 onclick = "$('##{id}').toggle(); "
223 onclick = "$('##{id}').toggle(); "
224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
224 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
225 onclick << "return false;"
225 onclick << "return false;"
226 link_to(name, "#", :onclick => onclick)
226 link_to(name, "#", :onclick => onclick)
227 end
227 end
228
228
229 def format_activity_title(text)
229 def format_activity_title(text)
230 h(truncate_single_line_raw(text, 100))
230 h(truncate_single_line_raw(text, 100))
231 end
231 end
232
232
233 def format_activity_day(date)
233 def format_activity_day(date)
234 date == User.current.today ? l(:label_today).titleize : format_date(date)
234 date == User.current.today ? l(:label_today).titleize : format_date(date)
235 end
235 end
236
236
237 def format_activity_description(text)
237 def format_activity_description(text)
238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
238 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
239 ).gsub(/[\r\n]+/, "<br />").html_safe
239 ).gsub(/[\r\n]+/, "<br />").html_safe
240 end
240 end
241
241
242 def format_version_name(version)
242 def format_version_name(version)
243 if !version.shared? || version.project == @project
243 if version.project == @project
244 h(version)
244 h(version)
245 else
245 else
246 h("#{version.project} - #{version}")
246 h("#{version.project} - #{version}")
247 end
247 end
248 end
248 end
249
249
250 def due_date_distance_in_words(date)
250 def due_date_distance_in_words(date)
251 if date
251 if date
252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
252 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
253 end
253 end
254 end
254 end
255
255
256 # Renders a tree of projects as a nested set of unordered lists
256 # Renders a tree of projects as a nested set of unordered lists
257 # The given collection may be a subset of the whole project tree
257 # The given collection may be a subset of the whole project tree
258 # (eg. some intermediate nodes are private and can not be seen)
258 # (eg. some intermediate nodes are private and can not be seen)
259 def render_project_nested_lists(projects, &block)
259 def render_project_nested_lists(projects, &block)
260 s = ''
260 s = ''
261 if projects.any?
261 if projects.any?
262 ancestors = []
262 ancestors = []
263 original_project = @project
263 original_project = @project
264 projects.sort_by(&:lft).each do |project|
264 projects.sort_by(&:lft).each do |project|
265 # set the project environment to please macros.
265 # set the project environment to please macros.
266 @project = project
266 @project = project
267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
267 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
268 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
269 else
269 else
270 ancestors.pop
270 ancestors.pop
271 s << "</li>"
271 s << "</li>"
272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
272 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 ancestors.pop
273 ancestors.pop
274 s << "</ul></li>\n"
274 s << "</ul></li>\n"
275 end
275 end
276 end
276 end
277 classes = (ancestors.empty? ? 'root' : 'child')
277 classes = (ancestors.empty? ? 'root' : 'child')
278 s << "<li class='#{classes}'><div class='#{classes}'>"
278 s << "<li class='#{classes}'><div class='#{classes}'>"
279 s << h(block_given? ? capture(project, &block) : project.name)
279 s << h(block_given? ? capture(project, &block) : project.name)
280 s << "</div>\n"
280 s << "</div>\n"
281 ancestors << project
281 ancestors << project
282 end
282 end
283 s << ("</li></ul>\n" * ancestors.size)
283 s << ("</li></ul>\n" * ancestors.size)
284 @project = original_project
284 @project = original_project
285 end
285 end
286 s.html_safe
286 s.html_safe
287 end
287 end
288
288
289 def render_page_hierarchy(pages, node=nil, options={})
289 def render_page_hierarchy(pages, node=nil, options={})
290 content = ''
290 content = ''
291 if pages[node]
291 if pages[node]
292 content << "<ul class=\"pages-hierarchy\">\n"
292 content << "<ul class=\"pages-hierarchy\">\n"
293 pages[node].each do |page|
293 pages[node].each do |page|
294 content << "<li>"
294 content << "<li>"
295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
295 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
296 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
297 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
298 content << "</li>\n"
298 content << "</li>\n"
299 end
299 end
300 content << "</ul>\n"
300 content << "</ul>\n"
301 end
301 end
302 content.html_safe
302 content.html_safe
303 end
303 end
304
304
305 # Renders flash messages
305 # Renders flash messages
306 def render_flash_messages
306 def render_flash_messages
307 s = ''
307 s = ''
308 flash.each do |k,v|
308 flash.each do |k,v|
309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
309 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
310 end
310 end
311 s.html_safe
311 s.html_safe
312 end
312 end
313
313
314 # Renders tabs and their content
314 # Renders tabs and their content
315 def render_tabs(tabs, selected=params[:tab])
315 def render_tabs(tabs, selected=params[:tab])
316 if tabs.any?
316 if tabs.any?
317 unless tabs.detect {|tab| tab[:name] == selected}
317 unless tabs.detect {|tab| tab[:name] == selected}
318 selected = nil
318 selected = nil
319 end
319 end
320 selected ||= tabs.first[:name]
320 selected ||= tabs.first[:name]
321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
321 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
322 else
322 else
323 content_tag 'p', l(:label_no_data), :class => "nodata"
323 content_tag 'p', l(:label_no_data), :class => "nodata"
324 end
324 end
325 end
325 end
326
326
327 # Renders the project quick-jump box
327 # Renders the project quick-jump box
328 def render_project_jump_box
328 def render_project_jump_box
329 return unless User.current.logged?
329 return unless User.current.logged?
330 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
330 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
331 if projects.any?
331 if projects.any?
332 options =
332 options =
333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
333 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
334 '<option value="" disabled="disabled">---</option>').html_safe
334 '<option value="" disabled="disabled">---</option>').html_safe
335
335
336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
336 options << project_tree_options_for_select(projects, :selected => @project) do |p|
337 { :value => project_path(:id => p, :jump => current_menu_item) }
337 { :value => project_path(:id => p, :jump => current_menu_item) }
338 end
338 end
339
339
340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
340 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 end
341 end
342 end
342 end
343
343
344 def project_tree_options_for_select(projects, options = {})
344 def project_tree_options_for_select(projects, options = {})
345 s = ''.html_safe
345 s = ''.html_safe
346 if blank_text = options[:include_blank]
346 if blank_text = options[:include_blank]
347 if blank_text == true
347 if blank_text == true
348 blank_text = '&nbsp;'.html_safe
348 blank_text = '&nbsp;'.html_safe
349 end
349 end
350 s << content_tag('option', blank_text, :value => '')
350 s << content_tag('option', blank_text, :value => '')
351 end
351 end
352 project_tree(projects) do |project, level|
352 project_tree(projects) do |project, level|
353 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
353 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
354 tag_options = {:value => project.id}
354 tag_options = {:value => project.id}
355 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
355 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
356 tag_options[:selected] = 'selected'
356 tag_options[:selected] = 'selected'
357 else
357 else
358 tag_options[:selected] = nil
358 tag_options[:selected] = nil
359 end
359 end
360 tag_options.merge!(yield(project)) if block_given?
360 tag_options.merge!(yield(project)) if block_given?
361 s << content_tag('option', name_prefix + h(project), tag_options)
361 s << content_tag('option', name_prefix + h(project), tag_options)
362 end
362 end
363 s.html_safe
363 s.html_safe
364 end
364 end
365
365
366 # Yields the given block for each project with its level in the tree
366 # Yields the given block for each project with its level in the tree
367 #
367 #
368 # Wrapper for Project#project_tree
368 # Wrapper for Project#project_tree
369 def project_tree(projects, &block)
369 def project_tree(projects, &block)
370 Project.project_tree(projects, &block)
370 Project.project_tree(projects, &block)
371 end
371 end
372
372
373 def principals_check_box_tags(name, principals)
373 def principals_check_box_tags(name, principals)
374 s = ''
374 s = ''
375 principals.each do |principal|
375 principals.each do |principal|
376 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
376 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
377 end
377 end
378 s.html_safe
378 s.html_safe
379 end
379 end
380
380
381 # Returns a string for users/groups option tags
381 # Returns a string for users/groups option tags
382 def principals_options_for_select(collection, selected=nil)
382 def principals_options_for_select(collection, selected=nil)
383 s = ''
383 s = ''
384 if collection.include?(User.current)
384 if collection.include?(User.current)
385 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
385 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
386 end
386 end
387 groups = ''
387 groups = ''
388 collection.sort.each do |element|
388 collection.sort.each do |element|
389 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
389 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
390 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
390 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
391 end
391 end
392 unless groups.empty?
392 unless groups.empty?
393 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
393 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
394 end
394 end
395 s.html_safe
395 s.html_safe
396 end
396 end
397
397
398 def option_tag(name, text, value, selected=nil, options={})
398 def option_tag(name, text, value, selected=nil, options={})
399 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
399 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
400 end
400 end
401
401
402 def truncate_single_line_raw(string, length)
402 def truncate_single_line_raw(string, length)
403 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
403 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
404 end
404 end
405
405
406 # Truncates at line break after 250 characters or options[:length]
406 # Truncates at line break after 250 characters or options[:length]
407 def truncate_lines(string, options={})
407 def truncate_lines(string, options={})
408 length = options[:length] || 250
408 length = options[:length] || 250
409 if string.to_s =~ /\A(.{#{length}}.*?)$/m
409 if string.to_s =~ /\A(.{#{length}}.*?)$/m
410 "#{$1}..."
410 "#{$1}..."
411 else
411 else
412 string
412 string
413 end
413 end
414 end
414 end
415
415
416 def anchor(text)
416 def anchor(text)
417 text.to_s.gsub(' ', '_')
417 text.to_s.gsub(' ', '_')
418 end
418 end
419
419
420 def html_hours(text)
420 def html_hours(text)
421 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
421 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
422 end
422 end
423
423
424 def authoring(created, author, options={})
424 def authoring(created, author, options={})
425 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
425 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
426 end
426 end
427
427
428 def time_tag(time)
428 def time_tag(time)
429 text = distance_of_time_in_words(Time.now, time)
429 text = distance_of_time_in_words(Time.now, time)
430 if @project
430 if @project
431 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
431 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
432 else
432 else
433 content_tag('abbr', text, :title => format_time(time))
433 content_tag('abbr', text, :title => format_time(time))
434 end
434 end
435 end
435 end
436
436
437 def syntax_highlight_lines(name, content)
437 def syntax_highlight_lines(name, content)
438 lines = []
438 lines = []
439 syntax_highlight(name, content).each_line { |line| lines << line }
439 syntax_highlight(name, content).each_line { |line| lines << line }
440 lines
440 lines
441 end
441 end
442
442
443 def syntax_highlight(name, content)
443 def syntax_highlight(name, content)
444 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
444 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
445 end
445 end
446
446
447 def to_path_param(path)
447 def to_path_param(path)
448 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
448 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
449 str.blank? ? nil : str
449 str.blank? ? nil : str
450 end
450 end
451
451
452 def reorder_links(name, url, method = :post)
452 def reorder_links(name, url, method = :post)
453 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
453 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
454 url.merge({"#{name}[move_to]" => 'highest'}),
454 url.merge({"#{name}[move_to]" => 'highest'}),
455 :method => method, :title => l(:label_sort_highest)) +
455 :method => method, :title => l(:label_sort_highest)) +
456 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
456 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
457 url.merge({"#{name}[move_to]" => 'higher'}),
457 url.merge({"#{name}[move_to]" => 'higher'}),
458 :method => method, :title => l(:label_sort_higher)) +
458 :method => method, :title => l(:label_sort_higher)) +
459 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
459 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
460 url.merge({"#{name}[move_to]" => 'lower'}),
460 url.merge({"#{name}[move_to]" => 'lower'}),
461 :method => method, :title => l(:label_sort_lower)) +
461 :method => method, :title => l(:label_sort_lower)) +
462 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
462 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
463 url.merge({"#{name}[move_to]" => 'lowest'}),
463 url.merge({"#{name}[move_to]" => 'lowest'}),
464 :method => method, :title => l(:label_sort_lowest))
464 :method => method, :title => l(:label_sort_lowest))
465 end
465 end
466
466
467 def breadcrumb(*args)
467 def breadcrumb(*args)
468 elements = args.flatten
468 elements = args.flatten
469 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
469 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
470 end
470 end
471
471
472 def other_formats_links(&block)
472 def other_formats_links(&block)
473 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
473 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
474 yield Redmine::Views::OtherFormatsBuilder.new(self)
474 yield Redmine::Views::OtherFormatsBuilder.new(self)
475 concat('</p>'.html_safe)
475 concat('</p>'.html_safe)
476 end
476 end
477
477
478 def page_header_title
478 def page_header_title
479 if @project.nil? || @project.new_record?
479 if @project.nil? || @project.new_record?
480 h(Setting.app_title)
480 h(Setting.app_title)
481 else
481 else
482 b = []
482 b = []
483 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
483 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
484 if ancestors.any?
484 if ancestors.any?
485 root = ancestors.shift
485 root = ancestors.shift
486 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
486 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
487 if ancestors.size > 2
487 if ancestors.size > 2
488 b << "\xe2\x80\xa6"
488 b << "\xe2\x80\xa6"
489 ancestors = ancestors[-2, 2]
489 ancestors = ancestors[-2, 2]
490 end
490 end
491 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
491 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
492 end
492 end
493 b << h(@project)
493 b << h(@project)
494 b.join(" \xc2\xbb ").html_safe
494 b.join(" \xc2\xbb ").html_safe
495 end
495 end
496 end
496 end
497
497
498 # Returns a h2 tag and sets the html title with the given arguments
498 # Returns a h2 tag and sets the html title with the given arguments
499 def title(*args)
499 def title(*args)
500 strings = args.map do |arg|
500 strings = args.map do |arg|
501 if arg.is_a?(Array) && arg.size >= 2
501 if arg.is_a?(Array) && arg.size >= 2
502 link_to(*arg)
502 link_to(*arg)
503 else
503 else
504 h(arg.to_s)
504 h(arg.to_s)
505 end
505 end
506 end
506 end
507 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
507 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
508 content_tag('h2', strings.join(' &#187; ').html_safe)
508 content_tag('h2', strings.join(' &#187; ').html_safe)
509 end
509 end
510
510
511 # Sets the html title
511 # Sets the html title
512 # Returns the html title when called without arguments
512 # Returns the html title when called without arguments
513 # Current project name and app_title and automatically appended
513 # Current project name and app_title and automatically appended
514 # Exemples:
514 # Exemples:
515 # html_title 'Foo', 'Bar'
515 # html_title 'Foo', 'Bar'
516 # html_title # => 'Foo - Bar - My Project - Redmine'
516 # html_title # => 'Foo - Bar - My Project - Redmine'
517 def html_title(*args)
517 def html_title(*args)
518 if args.empty?
518 if args.empty?
519 title = @html_title || []
519 title = @html_title || []
520 title << @project.name if @project
520 title << @project.name if @project
521 title << Setting.app_title unless Setting.app_title == title.last
521 title << Setting.app_title unless Setting.app_title == title.last
522 title.reject(&:blank?).join(' - ')
522 title.reject(&:blank?).join(' - ')
523 else
523 else
524 @html_title ||= []
524 @html_title ||= []
525 @html_title += args
525 @html_title += args
526 end
526 end
527 end
527 end
528
528
529 # Returns the theme, controller name, and action as css classes for the
529 # Returns the theme, controller name, and action as css classes for the
530 # HTML body.
530 # HTML body.
531 def body_css_classes
531 def body_css_classes
532 css = []
532 css = []
533 if theme = Redmine::Themes.theme(Setting.ui_theme)
533 if theme = Redmine::Themes.theme(Setting.ui_theme)
534 css << 'theme-' + theme.name
534 css << 'theme-' + theme.name
535 end
535 end
536
536
537 css << 'project-' + @project.identifier if @project && @project.identifier.present?
537 css << 'project-' + @project.identifier if @project && @project.identifier.present?
538 css << 'controller-' + controller_name
538 css << 'controller-' + controller_name
539 css << 'action-' + action_name
539 css << 'action-' + action_name
540 css.join(' ')
540 css.join(' ')
541 end
541 end
542
542
543 def accesskey(s)
543 def accesskey(s)
544 @used_accesskeys ||= []
544 @used_accesskeys ||= []
545 key = Redmine::AccessKeys.key_for(s)
545 key = Redmine::AccessKeys.key_for(s)
546 return nil if @used_accesskeys.include?(key)
546 return nil if @used_accesskeys.include?(key)
547 @used_accesskeys << key
547 @used_accesskeys << key
548 key
548 key
549 end
549 end
550
550
551 # Formats text according to system settings.
551 # Formats text according to system settings.
552 # 2 ways to call this method:
552 # 2 ways to call this method:
553 # * with a String: textilizable(text, options)
553 # * with a String: textilizable(text, options)
554 # * with an object and one of its attribute: textilizable(issue, :description, options)
554 # * with an object and one of its attribute: textilizable(issue, :description, options)
555 def textilizable(*args)
555 def textilizable(*args)
556 options = args.last.is_a?(Hash) ? args.pop : {}
556 options = args.last.is_a?(Hash) ? args.pop : {}
557 case args.size
557 case args.size
558 when 1
558 when 1
559 obj = options[:object]
559 obj = options[:object]
560 text = args.shift
560 text = args.shift
561 when 2
561 when 2
562 obj = args.shift
562 obj = args.shift
563 attr = args.shift
563 attr = args.shift
564 text = obj.send(attr).to_s
564 text = obj.send(attr).to_s
565 else
565 else
566 raise ArgumentError, 'invalid arguments to textilizable'
566 raise ArgumentError, 'invalid arguments to textilizable'
567 end
567 end
568 return '' if text.blank?
568 return '' if text.blank?
569 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
569 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
570 @only_path = only_path = options.delete(:only_path) == false ? false : true
570 @only_path = only_path = options.delete(:only_path) == false ? false : true
571
571
572 text = text.dup
572 text = text.dup
573 macros = catch_macros(text)
573 macros = catch_macros(text)
574 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
574 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
575
575
576 @parsed_headings = []
576 @parsed_headings = []
577 @heading_anchors = {}
577 @heading_anchors = {}
578 @current_section = 0 if options[:edit_section_links]
578 @current_section = 0 if options[:edit_section_links]
579
579
580 parse_sections(text, project, obj, attr, only_path, options)
580 parse_sections(text, project, obj, attr, only_path, options)
581 text = parse_non_pre_blocks(text, obj, macros) do |text|
581 text = parse_non_pre_blocks(text, obj, macros) do |text|
582 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
582 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
583 send method_name, text, project, obj, attr, only_path, options
583 send method_name, text, project, obj, attr, only_path, options
584 end
584 end
585 end
585 end
586 parse_headings(text, project, obj, attr, only_path, options)
586 parse_headings(text, project, obj, attr, only_path, options)
587
587
588 if @parsed_headings.any?
588 if @parsed_headings.any?
589 replace_toc(text, @parsed_headings)
589 replace_toc(text, @parsed_headings)
590 end
590 end
591
591
592 text.html_safe
592 text.html_safe
593 end
593 end
594
594
595 def parse_non_pre_blocks(text, obj, macros)
595 def parse_non_pre_blocks(text, obj, macros)
596 s = StringScanner.new(text)
596 s = StringScanner.new(text)
597 tags = []
597 tags = []
598 parsed = ''
598 parsed = ''
599 while !s.eos?
599 while !s.eos?
600 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
600 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
601 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
601 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
602 if tags.empty?
602 if tags.empty?
603 yield text
603 yield text
604 inject_macros(text, obj, macros) if macros.any?
604 inject_macros(text, obj, macros) if macros.any?
605 else
605 else
606 inject_macros(text, obj, macros, false) if macros.any?
606 inject_macros(text, obj, macros, false) if macros.any?
607 end
607 end
608 parsed << text
608 parsed << text
609 if tag
609 if tag
610 if closing
610 if closing
611 if tags.last == tag.downcase
611 if tags.last == tag.downcase
612 tags.pop
612 tags.pop
613 end
613 end
614 else
614 else
615 tags << tag.downcase
615 tags << tag.downcase
616 end
616 end
617 parsed << full_tag
617 parsed << full_tag
618 end
618 end
619 end
619 end
620 # Close any non closing tags
620 # Close any non closing tags
621 while tag = tags.pop
621 while tag = tags.pop
622 parsed << "</#{tag}>"
622 parsed << "</#{tag}>"
623 end
623 end
624 parsed
624 parsed
625 end
625 end
626
626
627 def parse_inline_attachments(text, project, obj, attr, only_path, options)
627 def parse_inline_attachments(text, project, obj, attr, only_path, options)
628 return if options[:inline_attachments] == false
628 return if options[:inline_attachments] == false
629
629
630 # when using an image link, try to use an attachment, if possible
630 # when using an image link, try to use an attachment, if possible
631 attachments = options[:attachments] || []
631 attachments = options[:attachments] || []
632 attachments += obj.attachments if obj.respond_to?(:attachments)
632 attachments += obj.attachments if obj.respond_to?(:attachments)
633 if attachments.present?
633 if attachments.present?
634 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
634 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
635 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
635 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
636 # search for the picture in attachments
636 # search for the picture in attachments
637 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
637 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
638 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
638 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
639 desc = found.description.to_s.gsub('"', '')
639 desc = found.description.to_s.gsub('"', '')
640 if !desc.blank? && alttext.blank?
640 if !desc.blank? && alttext.blank?
641 alt = " title=\"#{desc}\" alt=\"#{desc}\""
641 alt = " title=\"#{desc}\" alt=\"#{desc}\""
642 end
642 end
643 "src=\"#{image_url}\"#{alt}"
643 "src=\"#{image_url}\"#{alt}"
644 else
644 else
645 m
645 m
646 end
646 end
647 end
647 end
648 end
648 end
649 end
649 end
650
650
651 # Wiki links
651 # Wiki links
652 #
652 #
653 # Examples:
653 # Examples:
654 # [[mypage]]
654 # [[mypage]]
655 # [[mypage|mytext]]
655 # [[mypage|mytext]]
656 # wiki links can refer other project wikis, using project name or identifier:
656 # wiki links can refer other project wikis, using project name or identifier:
657 # [[project:]] -> wiki starting page
657 # [[project:]] -> wiki starting page
658 # [[project:|mytext]]
658 # [[project:|mytext]]
659 # [[project:mypage]]
659 # [[project:mypage]]
660 # [[project:mypage|mytext]]
660 # [[project:mypage|mytext]]
661 def parse_wiki_links(text, project, obj, attr, only_path, options)
661 def parse_wiki_links(text, project, obj, attr, only_path, options)
662 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
662 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
663 link_project = project
663 link_project = project
664 esc, all, page, title = $1, $2, $3, $5
664 esc, all, page, title = $1, $2, $3, $5
665 if esc.nil?
665 if esc.nil?
666 if page =~ /^([^\:]+)\:(.*)$/
666 if page =~ /^([^\:]+)\:(.*)$/
667 identifier, page = $1, $2
667 identifier, page = $1, $2
668 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
668 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
669 title ||= identifier if page.blank?
669 title ||= identifier if page.blank?
670 end
670 end
671
671
672 if link_project && link_project.wiki
672 if link_project && link_project.wiki
673 # extract anchor
673 # extract anchor
674 anchor = nil
674 anchor = nil
675 if page =~ /^(.+?)\#(.+)$/
675 if page =~ /^(.+?)\#(.+)$/
676 page, anchor = $1, $2
676 page, anchor = $1, $2
677 end
677 end
678 anchor = sanitize_anchor_name(anchor) if anchor.present?
678 anchor = sanitize_anchor_name(anchor) if anchor.present?
679 # check if page exists
679 # check if page exists
680 wiki_page = link_project.wiki.find_page(page)
680 wiki_page = link_project.wiki.find_page(page)
681 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
681 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
682 "##{anchor}"
682 "##{anchor}"
683 else
683 else
684 case options[:wiki_links]
684 case options[:wiki_links]
685 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
685 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
686 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
686 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
687 else
687 else
688 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
688 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
689 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
689 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
690 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
690 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
691 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
691 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
692 end
692 end
693 end
693 end
694 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
694 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
695 else
695 else
696 # project or wiki doesn't exist
696 # project or wiki doesn't exist
697 all
697 all
698 end
698 end
699 else
699 else
700 all
700 all
701 end
701 end
702 end
702 end
703 end
703 end
704
704
705 # Redmine links
705 # Redmine links
706 #
706 #
707 # Examples:
707 # Examples:
708 # Issues:
708 # Issues:
709 # #52 -> Link to issue #52
709 # #52 -> Link to issue #52
710 # Changesets:
710 # Changesets:
711 # r52 -> Link to revision 52
711 # r52 -> Link to revision 52
712 # commit:a85130f -> Link to scmid starting with a85130f
712 # commit:a85130f -> Link to scmid starting with a85130f
713 # Documents:
713 # Documents:
714 # document#17 -> Link to document with id 17
714 # document#17 -> Link to document with id 17
715 # document:Greetings -> Link to the document with title "Greetings"
715 # document:Greetings -> Link to the document with title "Greetings"
716 # document:"Some document" -> Link to the document with title "Some document"
716 # document:"Some document" -> Link to the document with title "Some document"
717 # Versions:
717 # Versions:
718 # version#3 -> Link to version with id 3
718 # version#3 -> Link to version with id 3
719 # version:1.0.0 -> Link to version named "1.0.0"
719 # version:1.0.0 -> Link to version named "1.0.0"
720 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
720 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
721 # Attachments:
721 # Attachments:
722 # attachment:file.zip -> Link to the attachment of the current object named file.zip
722 # attachment:file.zip -> Link to the attachment of the current object named file.zip
723 # Source files:
723 # Source files:
724 # source:some/file -> Link to the file located at /some/file in the project's repository
724 # source:some/file -> Link to the file located at /some/file in the project's repository
725 # source:some/file@52 -> Link to the file's revision 52
725 # source:some/file@52 -> Link to the file's revision 52
726 # source:some/file#L120 -> Link to line 120 of the file
726 # source:some/file#L120 -> Link to line 120 of the file
727 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
727 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
728 # export:some/file -> Force the download of the file
728 # export:some/file -> Force the download of the file
729 # Forum messages:
729 # Forum messages:
730 # message#1218 -> Link to message with id 1218
730 # message#1218 -> Link to message with id 1218
731 # Projects:
731 # Projects:
732 # project:someproject -> Link to project named "someproject"
732 # project:someproject -> Link to project named "someproject"
733 # project#3 -> Link to project with id 3
733 # project#3 -> Link to project with id 3
734 #
734 #
735 # Links can refer other objects from other projects, using project identifier:
735 # Links can refer other objects from other projects, using project identifier:
736 # identifier:r52
736 # identifier:r52
737 # identifier:document:"Some document"
737 # identifier:document:"Some document"
738 # identifier:version:1.0.0
738 # identifier:version:1.0.0
739 # identifier:source:some/file
739 # identifier:source:some/file
740 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
740 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
741 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|
741 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|
742 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
742 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
743 if tag_content
743 if tag_content
744 $&
744 $&
745 else
745 else
746 link = nil
746 link = nil
747 project = default_project
747 project = default_project
748 if project_identifier
748 if project_identifier
749 project = Project.visible.find_by_identifier(project_identifier)
749 project = Project.visible.find_by_identifier(project_identifier)
750 end
750 end
751 if esc.nil?
751 if esc.nil?
752 if prefix.nil? && sep == 'r'
752 if prefix.nil? && sep == 'r'
753 if project
753 if project
754 repository = nil
754 repository = nil
755 if repo_identifier
755 if repo_identifier
756 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
756 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
757 else
757 else
758 repository = project.repository
758 repository = project.repository
759 end
759 end
760 # project.changesets.visible raises an SQL error because of a double join on repositories
760 # project.changesets.visible raises an SQL error because of a double join on repositories
761 if repository &&
761 if repository &&
762 (changeset = Changeset.visible.
762 (changeset = Changeset.visible.
763 find_by_repository_id_and_revision(repository.id, identifier))
763 find_by_repository_id_and_revision(repository.id, identifier))
764 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
764 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
765 {:only_path => only_path, :controller => 'repositories',
765 {:only_path => only_path, :controller => 'repositories',
766 :action => 'revision', :id => project,
766 :action => 'revision', :id => project,
767 :repository_id => repository.identifier_param,
767 :repository_id => repository.identifier_param,
768 :rev => changeset.revision},
768 :rev => changeset.revision},
769 :class => 'changeset',
769 :class => 'changeset',
770 :title => truncate_single_line_raw(changeset.comments, 100))
770 :title => truncate_single_line_raw(changeset.comments, 100))
771 end
771 end
772 end
772 end
773 elsif sep == '#'
773 elsif sep == '#'
774 oid = identifier.to_i
774 oid = identifier.to_i
775 case prefix
775 case prefix
776 when nil
776 when nil
777 if oid.to_s == identifier &&
777 if oid.to_s == identifier &&
778 issue = Issue.visible.find_by_id(oid)
778 issue = Issue.visible.find_by_id(oid)
779 anchor = comment_id ? "note-#{comment_id}" : nil
779 anchor = comment_id ? "note-#{comment_id}" : nil
780 link = link_to("##{oid}#{comment_suffix}",
780 link = link_to("##{oid}#{comment_suffix}",
781 issue_url(issue, :only_path => only_path, :anchor => anchor),
781 issue_url(issue, :only_path => only_path, :anchor => anchor),
782 :class => issue.css_classes,
782 :class => issue.css_classes,
783 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
783 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
784 end
784 end
785 when 'document'
785 when 'document'
786 if document = Document.visible.find_by_id(oid)
786 if document = Document.visible.find_by_id(oid)
787 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
787 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
788 end
788 end
789 when 'version'
789 when 'version'
790 if version = Version.visible.find_by_id(oid)
790 if version = Version.visible.find_by_id(oid)
791 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
791 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
792 end
792 end
793 when 'message'
793 when 'message'
794 if message = Message.visible.find_by_id(oid)
794 if message = Message.visible.find_by_id(oid)
795 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
795 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
796 end
796 end
797 when 'forum'
797 when 'forum'
798 if board = Board.visible.find_by_id(oid)
798 if board = Board.visible.find_by_id(oid)
799 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
799 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
800 end
800 end
801 when 'news'
801 when 'news'
802 if news = News.visible.find_by_id(oid)
802 if news = News.visible.find_by_id(oid)
803 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
803 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
804 end
804 end
805 when 'project'
805 when 'project'
806 if p = Project.visible.find_by_id(oid)
806 if p = Project.visible.find_by_id(oid)
807 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
807 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
808 end
808 end
809 end
809 end
810 elsif sep == ':'
810 elsif sep == ':'
811 # removes the double quotes if any
811 # removes the double quotes if any
812 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
812 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
813 name = CGI.unescapeHTML(name)
813 name = CGI.unescapeHTML(name)
814 case prefix
814 case prefix
815 when 'document'
815 when 'document'
816 if project && document = project.documents.visible.find_by_title(name)
816 if project && document = project.documents.visible.find_by_title(name)
817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
818 end
818 end
819 when 'version'
819 when 'version'
820 if project && version = project.versions.visible.find_by_name(name)
820 if project && version = project.versions.visible.find_by_name(name)
821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
822 end
822 end
823 when 'forum'
823 when 'forum'
824 if project && board = project.boards.visible.find_by_name(name)
824 if project && board = project.boards.visible.find_by_name(name)
825 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
825 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
826 end
826 end
827 when 'news'
827 when 'news'
828 if project && news = project.news.visible.find_by_title(name)
828 if project && news = project.news.visible.find_by_title(name)
829 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
829 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
830 end
830 end
831 when 'commit', 'source', 'export'
831 when 'commit', 'source', 'export'
832 if project
832 if project
833 repository = nil
833 repository = nil
834 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
834 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
835 repo_prefix, repo_identifier, name = $1, $2, $3
835 repo_prefix, repo_identifier, name = $1, $2, $3
836 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
836 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
837 else
837 else
838 repository = project.repository
838 repository = project.repository
839 end
839 end
840 if prefix == 'commit'
840 if prefix == 'commit'
841 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
841 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
842 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},
842 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},
843 :class => 'changeset',
843 :class => 'changeset',
844 :title => truncate_single_line_raw(changeset.comments, 100)
844 :title => truncate_single_line_raw(changeset.comments, 100)
845 end
845 end
846 else
846 else
847 if repository && User.current.allowed_to?(:browse_repository, project)
847 if repository && User.current.allowed_to?(:browse_repository, project)
848 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
848 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
849 path, rev, anchor = $1, $3, $5
849 path, rev, anchor = $1, $3, $5
850 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,
850 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,
851 :path => to_path_param(path),
851 :path => to_path_param(path),
852 :rev => rev,
852 :rev => rev,
853 :anchor => anchor},
853 :anchor => anchor},
854 :class => (prefix == 'export' ? 'source download' : 'source')
854 :class => (prefix == 'export' ? 'source download' : 'source')
855 end
855 end
856 end
856 end
857 repo_prefix = nil
857 repo_prefix = nil
858 end
858 end
859 when 'attachment'
859 when 'attachment'
860 attachments = options[:attachments] || []
860 attachments = options[:attachments] || []
861 attachments += obj.attachments if obj.respond_to?(:attachments)
861 attachments += obj.attachments if obj.respond_to?(:attachments)
862 if attachments && attachment = Attachment.latest_attach(attachments, name)
862 if attachments && attachment = Attachment.latest_attach(attachments, name)
863 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
863 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
864 end
864 end
865 when 'project'
865 when 'project'
866 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
866 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
867 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
867 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
868 end
868 end
869 end
869 end
870 end
870 end
871 end
871 end
872 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
872 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
873 end
873 end
874 end
874 end
875 end
875 end
876
876
877 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
877 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
878
878
879 def parse_sections(text, project, obj, attr, only_path, options)
879 def parse_sections(text, project, obj, attr, only_path, options)
880 return unless options[:edit_section_links]
880 return unless options[:edit_section_links]
881 text.gsub!(HEADING_RE) do
881 text.gsub!(HEADING_RE) do
882 heading = $1
882 heading = $1
883 @current_section += 1
883 @current_section += 1
884 if @current_section > 1
884 if @current_section > 1
885 content_tag('div',
885 content_tag('div',
886 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
886 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
887 :class => 'contextual',
887 :class => 'contextual',
888 :title => l(:button_edit_section),
888 :title => l(:button_edit_section),
889 :id => "section-#{@current_section}") + heading.html_safe
889 :id => "section-#{@current_section}") + heading.html_safe
890 else
890 else
891 heading
891 heading
892 end
892 end
893 end
893 end
894 end
894 end
895
895
896 # Headings and TOC
896 # Headings and TOC
897 # Adds ids and links to headings unless options[:headings] is set to false
897 # Adds ids and links to headings unless options[:headings] is set to false
898 def parse_headings(text, project, obj, attr, only_path, options)
898 def parse_headings(text, project, obj, attr, only_path, options)
899 return if options[:headings] == false
899 return if options[:headings] == false
900
900
901 text.gsub!(HEADING_RE) do
901 text.gsub!(HEADING_RE) do
902 level, attrs, content = $2.to_i, $3, $4
902 level, attrs, content = $2.to_i, $3, $4
903 item = strip_tags(content).strip
903 item = strip_tags(content).strip
904 anchor = sanitize_anchor_name(item)
904 anchor = sanitize_anchor_name(item)
905 # used for single-file wiki export
905 # used for single-file wiki export
906 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
906 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
907 @heading_anchors[anchor] ||= 0
907 @heading_anchors[anchor] ||= 0
908 idx = (@heading_anchors[anchor] += 1)
908 idx = (@heading_anchors[anchor] += 1)
909 if idx > 1
909 if idx > 1
910 anchor = "#{anchor}-#{idx}"
910 anchor = "#{anchor}-#{idx}"
911 end
911 end
912 @parsed_headings << [level, anchor, item]
912 @parsed_headings << [level, anchor, item]
913 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
913 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
914 end
914 end
915 end
915 end
916
916
917 MACROS_RE = /(
917 MACROS_RE = /(
918 (!)? # escaping
918 (!)? # escaping
919 (
919 (
920 \{\{ # opening tag
920 \{\{ # opening tag
921 ([\w]+) # macro name
921 ([\w]+) # macro name
922 (\(([^\n\r]*?)\))? # optional arguments
922 (\(([^\n\r]*?)\))? # optional arguments
923 ([\n\r].*?[\n\r])? # optional block of text
923 ([\n\r].*?[\n\r])? # optional block of text
924 \}\} # closing tag
924 \}\} # closing tag
925 )
925 )
926 )/mx unless const_defined?(:MACROS_RE)
926 )/mx unless const_defined?(:MACROS_RE)
927
927
928 MACRO_SUB_RE = /(
928 MACRO_SUB_RE = /(
929 \{\{
929 \{\{
930 macro\((\d+)\)
930 macro\((\d+)\)
931 \}\}
931 \}\}
932 )/x unless const_defined?(:MACRO_SUB_RE)
932 )/x unless const_defined?(:MACRO_SUB_RE)
933
933
934 # Extracts macros from text
934 # Extracts macros from text
935 def catch_macros(text)
935 def catch_macros(text)
936 macros = {}
936 macros = {}
937 text.gsub!(MACROS_RE) do
937 text.gsub!(MACROS_RE) do
938 all, macro = $1, $4.downcase
938 all, macro = $1, $4.downcase
939 if macro_exists?(macro) || all =~ MACRO_SUB_RE
939 if macro_exists?(macro) || all =~ MACRO_SUB_RE
940 index = macros.size
940 index = macros.size
941 macros[index] = all
941 macros[index] = all
942 "{{macro(#{index})}}"
942 "{{macro(#{index})}}"
943 else
943 else
944 all
944 all
945 end
945 end
946 end
946 end
947 macros
947 macros
948 end
948 end
949
949
950 # Executes and replaces macros in text
950 # Executes and replaces macros in text
951 def inject_macros(text, obj, macros, execute=true)
951 def inject_macros(text, obj, macros, execute=true)
952 text.gsub!(MACRO_SUB_RE) do
952 text.gsub!(MACRO_SUB_RE) do
953 all, index = $1, $2.to_i
953 all, index = $1, $2.to_i
954 orig = macros.delete(index)
954 orig = macros.delete(index)
955 if execute && orig && orig =~ MACROS_RE
955 if execute && orig && orig =~ MACROS_RE
956 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
956 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
957 if esc.nil?
957 if esc.nil?
958 h(exec_macro(macro, obj, args, block) || all)
958 h(exec_macro(macro, obj, args, block) || all)
959 else
959 else
960 h(all)
960 h(all)
961 end
961 end
962 elsif orig
962 elsif orig
963 h(orig)
963 h(orig)
964 else
964 else
965 h(all)
965 h(all)
966 end
966 end
967 end
967 end
968 end
968 end
969
969
970 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
970 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
971
971
972 # Renders the TOC with given headings
972 # Renders the TOC with given headings
973 def replace_toc(text, headings)
973 def replace_toc(text, headings)
974 text.gsub!(TOC_RE) do
974 text.gsub!(TOC_RE) do
975 left_align, right_align = $2, $3
975 left_align, right_align = $2, $3
976 # Keep only the 4 first levels
976 # Keep only the 4 first levels
977 headings = headings.select{|level, anchor, item| level <= 4}
977 headings = headings.select{|level, anchor, item| level <= 4}
978 if headings.empty?
978 if headings.empty?
979 ''
979 ''
980 else
980 else
981 div_class = 'toc'
981 div_class = 'toc'
982 div_class << ' right' if right_align
982 div_class << ' right' if right_align
983 div_class << ' left' if left_align
983 div_class << ' left' if left_align
984 out = "<ul class=\"#{div_class}\"><li>"
984 out = "<ul class=\"#{div_class}\"><li>"
985 root = headings.map(&:first).min
985 root = headings.map(&:first).min
986 current = root
986 current = root
987 started = false
987 started = false
988 headings.each do |level, anchor, item|
988 headings.each do |level, anchor, item|
989 if level > current
989 if level > current
990 out << '<ul><li>' * (level - current)
990 out << '<ul><li>' * (level - current)
991 elsif level < current
991 elsif level < current
992 out << "</li></ul>\n" * (current - level) + "</li><li>"
992 out << "</li></ul>\n" * (current - level) + "</li><li>"
993 elsif started
993 elsif started
994 out << '</li><li>'
994 out << '</li><li>'
995 end
995 end
996 out << "<a href=\"##{anchor}\">#{item}</a>"
996 out << "<a href=\"##{anchor}\">#{item}</a>"
997 current = level
997 current = level
998 started = true
998 started = true
999 end
999 end
1000 out << '</li></ul>' * (current - root)
1000 out << '</li></ul>' * (current - root)
1001 out << '</li></ul>'
1001 out << '</li></ul>'
1002 end
1002 end
1003 end
1003 end
1004 end
1004 end
1005
1005
1006 # Same as Rails' simple_format helper without using paragraphs
1006 # Same as Rails' simple_format helper without using paragraphs
1007 def simple_format_without_paragraph(text)
1007 def simple_format_without_paragraph(text)
1008 text.to_s.
1008 text.to_s.
1009 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1009 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1010 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1010 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1011 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1011 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1012 html_safe
1012 html_safe
1013 end
1013 end
1014
1014
1015 def lang_options_for_select(blank=true)
1015 def lang_options_for_select(blank=true)
1016 (blank ? [["(auto)", ""]] : []) + languages_options
1016 (blank ? [["(auto)", ""]] : []) + languages_options
1017 end
1017 end
1018
1018
1019 def labelled_form_for(*args, &proc)
1019 def labelled_form_for(*args, &proc)
1020 args << {} unless args.last.is_a?(Hash)
1020 args << {} unless args.last.is_a?(Hash)
1021 options = args.last
1021 options = args.last
1022 if args.first.is_a?(Symbol)
1022 if args.first.is_a?(Symbol)
1023 options.merge!(:as => args.shift)
1023 options.merge!(:as => args.shift)
1024 end
1024 end
1025 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1025 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1026 form_for(*args, &proc)
1026 form_for(*args, &proc)
1027 end
1027 end
1028
1028
1029 def labelled_fields_for(*args, &proc)
1029 def labelled_fields_for(*args, &proc)
1030 args << {} unless args.last.is_a?(Hash)
1030 args << {} unless args.last.is_a?(Hash)
1031 options = args.last
1031 options = args.last
1032 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1032 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1033 fields_for(*args, &proc)
1033 fields_for(*args, &proc)
1034 end
1034 end
1035
1035
1036 def error_messages_for(*objects)
1036 def error_messages_for(*objects)
1037 html = ""
1037 html = ""
1038 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1038 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1039 errors = objects.map {|o| o.errors.full_messages}.flatten
1039 errors = objects.map {|o| o.errors.full_messages}.flatten
1040 if errors.any?
1040 if errors.any?
1041 html << "<div id='errorExplanation'><ul>\n"
1041 html << "<div id='errorExplanation'><ul>\n"
1042 errors.each do |error|
1042 errors.each do |error|
1043 html << "<li>#{h error}</li>\n"
1043 html << "<li>#{h error}</li>\n"
1044 end
1044 end
1045 html << "</ul></div>\n"
1045 html << "</ul></div>\n"
1046 end
1046 end
1047 html.html_safe
1047 html.html_safe
1048 end
1048 end
1049
1049
1050 def delete_link(url, options={})
1050 def delete_link(url, options={})
1051 options = {
1051 options = {
1052 :method => :delete,
1052 :method => :delete,
1053 :data => {:confirm => l(:text_are_you_sure)},
1053 :data => {:confirm => l(:text_are_you_sure)},
1054 :class => 'icon icon-del'
1054 :class => 'icon icon-del'
1055 }.merge(options)
1055 }.merge(options)
1056
1056
1057 link_to l(:button_delete), url, options
1057 link_to l(:button_delete), url, options
1058 end
1058 end
1059
1059
1060 def preview_link(url, form, target='preview', options={})
1060 def preview_link(url, form, target='preview', options={})
1061 content_tag 'a', l(:label_preview), {
1061 content_tag 'a', l(:label_preview), {
1062 :href => "#",
1062 :href => "#",
1063 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1063 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1064 :accesskey => accesskey(:preview)
1064 :accesskey => accesskey(:preview)
1065 }.merge(options)
1065 }.merge(options)
1066 end
1066 end
1067
1067
1068 def link_to_function(name, function, html_options={})
1068 def link_to_function(name, function, html_options={})
1069 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1069 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1070 end
1070 end
1071
1071
1072 # Helper to render JSON in views
1072 # Helper to render JSON in views
1073 def raw_json(arg)
1073 def raw_json(arg)
1074 arg.to_json.to_s.gsub('/', '\/').html_safe
1074 arg.to_json.to_s.gsub('/', '\/').html_safe
1075 end
1075 end
1076
1076
1077 def back_url
1077 def back_url
1078 url = params[:back_url]
1078 url = params[:back_url]
1079 if url.nil? && referer = request.env['HTTP_REFERER']
1079 if url.nil? && referer = request.env['HTTP_REFERER']
1080 url = CGI.unescape(referer.to_s)
1080 url = CGI.unescape(referer.to_s)
1081 end
1081 end
1082 url
1082 url
1083 end
1083 end
1084
1084
1085 def back_url_hidden_field_tag
1085 def back_url_hidden_field_tag
1086 url = back_url
1086 url = back_url
1087 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1087 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1088 end
1088 end
1089
1089
1090 def check_all_links(form_name)
1090 def check_all_links(form_name)
1091 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1091 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1092 " | ".html_safe +
1092 " | ".html_safe +
1093 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1093 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1094 end
1094 end
1095
1095
1096 def toggle_checkboxes_link(selector)
1096 def toggle_checkboxes_link(selector)
1097 link_to_function image_tag('toggle_check.png'),
1097 link_to_function image_tag('toggle_check.png'),
1098 "toggleCheckboxesBySelector('#{selector}')",
1098 "toggleCheckboxesBySelector('#{selector}')",
1099 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1099 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1100 end
1100 end
1101
1101
1102 def progress_bar(pcts, options={})
1102 def progress_bar(pcts, options={})
1103 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1103 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1104 pcts = pcts.collect(&:round)
1104 pcts = pcts.collect(&:round)
1105 pcts[1] = pcts[1] - pcts[0]
1105 pcts[1] = pcts[1] - pcts[0]
1106 pcts << (100 - pcts[1] - pcts[0])
1106 pcts << (100 - pcts[1] - pcts[0])
1107 width = options[:width] || '100px;'
1107 width = options[:width] || '100px;'
1108 legend = options[:legend] || ''
1108 legend = options[:legend] || ''
1109 content_tag('table',
1109 content_tag('table',
1110 content_tag('tr',
1110 content_tag('tr',
1111 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1111 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1112 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1112 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1113 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1113 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1114 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1114 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1115 content_tag('p', legend, :class => 'percent').html_safe
1115 content_tag('p', legend, :class => 'percent').html_safe
1116 end
1116 end
1117
1117
1118 def checked_image(checked=true)
1118 def checked_image(checked=true)
1119 if checked
1119 if checked
1120 image_tag 'toggle_check.png'
1120 image_tag 'toggle_check.png'
1121 end
1121 end
1122 end
1122 end
1123
1123
1124 def context_menu(url)
1124 def context_menu(url)
1125 unless @context_menu_included
1125 unless @context_menu_included
1126 content_for :header_tags do
1126 content_for :header_tags do
1127 javascript_include_tag('context_menu') +
1127 javascript_include_tag('context_menu') +
1128 stylesheet_link_tag('context_menu')
1128 stylesheet_link_tag('context_menu')
1129 end
1129 end
1130 if l(:direction) == 'rtl'
1130 if l(:direction) == 'rtl'
1131 content_for :header_tags do
1131 content_for :header_tags do
1132 stylesheet_link_tag('context_menu_rtl')
1132 stylesheet_link_tag('context_menu_rtl')
1133 end
1133 end
1134 end
1134 end
1135 @context_menu_included = true
1135 @context_menu_included = true
1136 end
1136 end
1137 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1137 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1138 end
1138 end
1139
1139
1140 def calendar_for(field_id)
1140 def calendar_for(field_id)
1141 include_calendar_headers_tags
1141 include_calendar_headers_tags
1142 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1142 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1143 end
1143 end
1144
1144
1145 def include_calendar_headers_tags
1145 def include_calendar_headers_tags
1146 unless @calendar_headers_tags_included
1146 unless @calendar_headers_tags_included
1147 tags = ''.html_safe
1147 tags = ''.html_safe
1148 @calendar_headers_tags_included = true
1148 @calendar_headers_tags_included = true
1149 content_for :header_tags do
1149 content_for :header_tags do
1150 start_of_week = Setting.start_of_week
1150 start_of_week = Setting.start_of_week
1151 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1151 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1152 # Redmine uses 1..7 (monday..sunday) in settings and locales
1152 # Redmine uses 1..7 (monday..sunday) in settings and locales
1153 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1153 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1154 start_of_week = start_of_week.to_i % 7
1154 start_of_week = start_of_week.to_i % 7
1155 tags << javascript_tag(
1155 tags << javascript_tag(
1156 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1156 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1157 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1157 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1158 path_to_image('/images/calendar.png') +
1158 path_to_image('/images/calendar.png') +
1159 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1159 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1160 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1160 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1161 "beforeShow: beforeShowDatePicker};")
1161 "beforeShow: beforeShowDatePicker};")
1162 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1162 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1163 unless jquery_locale == 'en'
1163 unless jquery_locale == 'en'
1164 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1164 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1165 end
1165 end
1166 tags
1166 tags
1167 end
1167 end
1168 end
1168 end
1169 end
1169 end
1170
1170
1171 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1171 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1172 # Examples:
1172 # Examples:
1173 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1173 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1174 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1174 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1175 #
1175 #
1176 def stylesheet_link_tag(*sources)
1176 def stylesheet_link_tag(*sources)
1177 options = sources.last.is_a?(Hash) ? sources.pop : {}
1177 options = sources.last.is_a?(Hash) ? sources.pop : {}
1178 plugin = options.delete(:plugin)
1178 plugin = options.delete(:plugin)
1179 sources = sources.map do |source|
1179 sources = sources.map do |source|
1180 if plugin
1180 if plugin
1181 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1181 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1182 elsif current_theme && current_theme.stylesheets.include?(source)
1182 elsif current_theme && current_theme.stylesheets.include?(source)
1183 current_theme.stylesheet_path(source)
1183 current_theme.stylesheet_path(source)
1184 else
1184 else
1185 source
1185 source
1186 end
1186 end
1187 end
1187 end
1188 super *sources, options
1188 super *sources, options
1189 end
1189 end
1190
1190
1191 # Overrides Rails' image_tag with themes and plugins support.
1191 # Overrides Rails' image_tag with themes and plugins support.
1192 # Examples:
1192 # Examples:
1193 # image_tag('image.png') # => picks image.png from the current theme or defaults
1193 # image_tag('image.png') # => picks image.png from the current theme or defaults
1194 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1194 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1195 #
1195 #
1196 def image_tag(source, options={})
1196 def image_tag(source, options={})
1197 if plugin = options.delete(:plugin)
1197 if plugin = options.delete(:plugin)
1198 source = "/plugin_assets/#{plugin}/images/#{source}"
1198 source = "/plugin_assets/#{plugin}/images/#{source}"
1199 elsif current_theme && current_theme.images.include?(source)
1199 elsif current_theme && current_theme.images.include?(source)
1200 source = current_theme.image_path(source)
1200 source = current_theme.image_path(source)
1201 end
1201 end
1202 super source, options
1202 super source, options
1203 end
1203 end
1204
1204
1205 # Overrides Rails' javascript_include_tag with plugins support
1205 # Overrides Rails' javascript_include_tag with plugins support
1206 # Examples:
1206 # Examples:
1207 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1207 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1208 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1208 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1209 #
1209 #
1210 def javascript_include_tag(*sources)
1210 def javascript_include_tag(*sources)
1211 options = sources.last.is_a?(Hash) ? sources.pop : {}
1211 options = sources.last.is_a?(Hash) ? sources.pop : {}
1212 if plugin = options.delete(:plugin)
1212 if plugin = options.delete(:plugin)
1213 sources = sources.map do |source|
1213 sources = sources.map do |source|
1214 if plugin
1214 if plugin
1215 "/plugin_assets/#{plugin}/javascripts/#{source}"
1215 "/plugin_assets/#{plugin}/javascripts/#{source}"
1216 else
1216 else
1217 source
1217 source
1218 end
1218 end
1219 end
1219 end
1220 end
1220 end
1221 super *sources, options
1221 super *sources, options
1222 end
1222 end
1223
1223
1224 def sidebar_content?
1224 def sidebar_content?
1225 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1225 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1226 end
1226 end
1227
1227
1228 def view_layouts_base_sidebar_hook_response
1228 def view_layouts_base_sidebar_hook_response
1229 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1229 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1230 end
1230 end
1231
1231
1232 def email_delivery_enabled?
1232 def email_delivery_enabled?
1233 !!ActionMailer::Base.perform_deliveries
1233 !!ActionMailer::Base.perform_deliveries
1234 end
1234 end
1235
1235
1236 # Returns the avatar image tag for the given +user+ if avatars are enabled
1236 # Returns the avatar image tag for the given +user+ if avatars are enabled
1237 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1237 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1238 def avatar(user, options = { })
1238 def avatar(user, options = { })
1239 if Setting.gravatar_enabled?
1239 if Setting.gravatar_enabled?
1240 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1240 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1241 email = nil
1241 email = nil
1242 if user.respond_to?(:mail)
1242 if user.respond_to?(:mail)
1243 email = user.mail
1243 email = user.mail
1244 elsif user.to_s =~ %r{<(.+?)>}
1244 elsif user.to_s =~ %r{<(.+?)>}
1245 email = $1
1245 email = $1
1246 end
1246 end
1247 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1247 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1248 else
1248 else
1249 ''
1249 ''
1250 end
1250 end
1251 end
1251 end
1252
1252
1253 def sanitize_anchor_name(anchor)
1253 def sanitize_anchor_name(anchor)
1254 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1254 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1255 end
1255 end
1256
1256
1257 # Returns the javascript tags that are included in the html layout head
1257 # Returns the javascript tags that are included in the html layout head
1258 def javascript_heads
1258 def javascript_heads
1259 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1259 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1260 unless User.current.pref.warn_on_leaving_unsaved == '0'
1260 unless User.current.pref.warn_on_leaving_unsaved == '0'
1261 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1261 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1262 end
1262 end
1263 tags
1263 tags
1264 end
1264 end
1265
1265
1266 def favicon
1266 def favicon
1267 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1267 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1268 end
1268 end
1269
1269
1270 # Returns the path to the favicon
1270 # Returns the path to the favicon
1271 def favicon_path
1271 def favicon_path
1272 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1272 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1273 image_path(icon)
1273 image_path(icon)
1274 end
1274 end
1275
1275
1276 # Returns the full URL to the favicon
1276 # Returns the full URL to the favicon
1277 def favicon_url
1277 def favicon_url
1278 # TODO: use #image_url introduced in Rails4
1278 # TODO: use #image_url introduced in Rails4
1279 path = favicon_path
1279 path = favicon_path
1280 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1280 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1281 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1281 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1282 end
1282 end
1283
1283
1284 def robot_exclusion_tag
1284 def robot_exclusion_tag
1285 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1285 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1286 end
1286 end
1287
1287
1288 # Returns true if arg is expected in the API response
1288 # Returns true if arg is expected in the API response
1289 def include_in_api_response?(arg)
1289 def include_in_api_response?(arg)
1290 unless @included_in_api_response
1290 unless @included_in_api_response
1291 param = params[:include]
1291 param = params[:include]
1292 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1292 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1293 @included_in_api_response.collect!(&:strip)
1293 @included_in_api_response.collect!(&:strip)
1294 end
1294 end
1295 @included_in_api_response.include?(arg.to_s)
1295 @included_in_api_response.include?(arg.to_s)
1296 end
1296 end
1297
1297
1298 # Returns options or nil if nometa param or X-Redmine-Nometa header
1298 # Returns options or nil if nometa param or X-Redmine-Nometa header
1299 # was set in the request
1299 # was set in the request
1300 def api_meta(options)
1300 def api_meta(options)
1301 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1301 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1302 # compatibility mode for activeresource clients that raise
1302 # compatibility mode for activeresource clients that raise
1303 # an error when deserializing an array with attributes
1303 # an error when deserializing an array with attributes
1304 nil
1304 nil
1305 else
1305 else
1306 options
1306 options
1307 end
1307 end
1308 end
1308 end
1309
1309
1310 private
1310 private
1311
1311
1312 def wiki_helper
1312 def wiki_helper
1313 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1313 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1314 extend helper
1314 extend helper
1315 return self
1315 return self
1316 end
1316 end
1317
1317
1318 def link_to_content_update(text, url_params = {}, html_options = {})
1318 def link_to_content_update(text, url_params = {}, html_options = {})
1319 link_to(text, url_params, html_options)
1319 link_to(text, url_params, html_options)
1320 end
1320 end
1321 end
1321 end
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,94 +1,85
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ProjectsHelperTest < ActionView::TestCase
20 class ProjectsHelperTest < ActionView::TestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ProjectsHelper
22 include ProjectsHelper
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, :trackers, :issue_statuses, :issues,
27 fixtures :projects, :trackers, :issue_statuses, :issues,
28 :enumerations, :users, :issue_categories,
28 :enumerations, :users, :issue_categories,
29 :versions,
29 :versions,
30 :projects_trackers,
30 :projects_trackers,
31 :member_roles,
31 :member_roles,
32 :members,
32 :members,
33 :groups_users,
33 :groups_users,
34 :enabled_modules
34 :enabled_modules
35
35
36 def setup
36 def setup
37 super
37 super
38 set_language_if_valid('en')
38 set_language_if_valid('en')
39 User.current = nil
39 User.current = nil
40 end
40 end
41
41
42 def test_link_to_version_within_project
42 def test_link_to_version_within_project
43 @project = Project.find(2)
43 @project = Project.find(2)
44 User.current = User.find(1)
44 User.current = User.find(1)
45 assert_equal '<a title="07/01/2006" href="/versions/5">Alpha</a>', link_to_version(Version.find(5))
45 assert_equal '<a title="07/01/2006" href="/versions/5">Alpha</a>', link_to_version(Version.find(5))
46 end
46 end
47
47
48 def test_link_to_version
48 def test_link_to_version
49 User.current = User.find(1)
49 User.current = User.find(1)
50 assert_equal '<a title="07/01/2006" href="/versions/5">Alpha</a>', link_to_version(Version.find(5))
50 assert_equal '<a title="07/01/2006" href="/versions/5">OnlineStore - Alpha</a>', link_to_version(Version.find(5))
51 end
51 end
52
52
53 def test_link_to_version_without_effective_date
53 def test_link_to_version_without_effective_date
54 User.current = User.find(1)
54 User.current = User.find(1)
55 version = Version.find(5)
55 version = Version.find(5)
56 version.effective_date = nil
56 version.effective_date = nil
57 assert_equal '<a href="/versions/5">Alpha</a>', link_to_version(version)
57 assert_equal '<a href="/versions/5">OnlineStore - Alpha</a>', link_to_version(version)
58 end
58 end
59
59
60 def test_link_to_private_version
60 def test_link_to_private_version
61 assert_equal 'Alpha', link_to_version(Version.find(5))
61 assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5))
62 end
62 end
63
63
64 def test_link_to_version_invalid_version
64 def test_link_to_version_invalid_version
65 assert_equal '', link_to_version(Object)
65 assert_equal '', link_to_version(Object)
66 end
66 end
67
67
68 def test_format_version_name_within_project
68 def test_format_version_name_within_project
69 @project = Project.find(1)
69 @project = Project.find(1)
70 assert_equal "0.1", format_version_name(Version.find(1))
70 assert_equal "0.1", format_version_name(Version.find(1))
71 end
71 end
72
72
73 def test_format_version_name
73 def test_format_version_name
74 assert_equal "0.1", format_version_name(Version.find(1))
74 assert_equal "eCookbook - 0.1", format_version_name(Version.find(1))
75 end
76
77 def test_format_version_name_for_shared_version_within_project_should_not_display_project_name
78 @project = Project.find(1)
79 version = Version.find(1)
80 version.sharing = 'system'
81 assert_equal "0.1", format_version_name(version)
82 end
75 end
83
76
84 def test_format_version_name_for_shared_version_should_display_project_name
77 def test_format_version_name_for_system_version
85 version = Version.find(1)
78 assert_equal "OnlineStore - Systemwide visible version", format_version_name(Version.find(7))
86 version.sharing = 'system'
87 assert_equal "eCookbook - 0.1", format_version_name(version)
88 end
79 end
89
80
90 def test_version_options_for_select_with_no_versions
81 def test_version_options_for_select_with_no_versions
91 assert_equal '', version_options_for_select([])
82 assert_equal '', version_options_for_select([])
92 assert_equal '', version_options_for_select([], Version.find(1))
83 assert_equal '', version_options_for_select([], Version.find(1))
93 end
84 end
94 end
85 end
General Comments 0
You need to be logged in to leave comments. Login now