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