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