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