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