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