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