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