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