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