##// END OF EJS Templates
Don't use a global variable for storing context menu URL....
Jean-Philippe Lang -
r15554:18073c971e69
parent child
Show More
@@ -1,1374 +1,1374
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Themes::Helper
29 include Redmine::Themes::Helper
30 include Redmine::Hook::Helper
30 include Redmine::Hook::Helper
31 include Redmine::Helpers::URL
31 include Redmine::Helpers::URL
32
32
33 extend Forwardable
33 extend Forwardable
34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
35
35
36 # Return true if user is authorized for controller/action, otherwise false
36 # Return true if user is authorized for controller/action, otherwise false
37 def authorize_for(controller, action)
37 def authorize_for(controller, action)
38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
39 end
39 end
40
40
41 # Display a link if user is authorized
41 # Display a link if user is authorized
42 #
42 #
43 # @param [String] name Anchor text (passed to link_to)
43 # @param [String] name Anchor text (passed to link_to)
44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
45 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] html_options Options passed to link_to
46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
49 end
49 end
50
50
51 # Displays a link to user's account page if active
51 # Displays a link to user's account page if active
52 def link_to_user(user, options={})
52 def link_to_user(user, options={})
53 if user.is_a?(User)
53 if user.is_a?(User)
54 name = h(user.name(options[:format]))
54 name = h(user.name(options[:format]))
55 if user.active? || (User.current.admin? && user.logged?)
55 if user.active? || (User.current.admin? && user.logged?)
56 link_to name, user_path(user), :class => user.css_classes
56 link_to name, user_path(user), :class => user.css_classes
57 else
57 else
58 name
58 name
59 end
59 end
60 else
60 else
61 h(user.to_s)
61 h(user.to_s)
62 end
62 end
63 end
63 end
64
64
65 # Displays a link to +issue+ with its subject.
65 # Displays a link to +issue+ with its subject.
66 # Examples:
66 # Examples:
67 #
67 #
68 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
78 if options[:subject] == false
78 if options[:subject] == false
79 title = issue.subject.truncate(60)
79 title = issue.subject.truncate(60)
80 else
80 else
81 subject = issue.subject
81 subject = issue.subject
82 if truncate_length = options[:truncate]
82 if truncate_length = options[:truncate]
83 subject = subject.truncate(truncate_length)
83 subject = subject.truncate(truncate_length)
84 end
84 end
85 end
85 end
86 only_path = options[:only_path].nil? ? true : options[:only_path]
86 only_path = options[:only_path].nil? ? true : options[:only_path]
87 s = link_to(text, issue_url(issue, :only_path => only_path),
87 s = link_to(text, issue_url(issue, :only_path => only_path),
88 :class => issue.css_classes, :title => title)
88 :class => issue.css_classes, :title => title)
89 s << h(": #{subject}") if subject
89 s << h(": #{subject}") if subject
90 s = h("#{issue.project} - ") + s if options[:project]
90 s = h("#{issue.project} - ") + s if options[:project]
91 s
91 s
92 end
92 end
93
93
94 # Generates a link to an attachment.
94 # Generates a link to an attachment.
95 # Options:
95 # Options:
96 # * :text - Link text (default to attachment filename)
96 # * :text - Link text (default to attachment filename)
97 # * :download - Force download (default: false)
97 # * :download - Force download (default: false)
98 def link_to_attachment(attachment, options={})
98 def link_to_attachment(attachment, options={})
99 text = options.delete(:text) || attachment.filename
99 text = options.delete(:text) || attachment.filename
100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
101 html_options = options.slice!(:only_path)
101 html_options = options.slice!(:only_path)
102 options[:only_path] = true unless options.key?(:only_path)
102 options[:only_path] = true unless options.key?(:only_path)
103 url = send(route_method, attachment, attachment.filename, options)
103 url = send(route_method, attachment, attachment.filename, options)
104 link_to text, url, html_options
104 link_to text, url, html_options
105 end
105 end
106
106
107 # Generates a link to a SCM revision
107 # Generates a link to a SCM revision
108 # Options:
108 # Options:
109 # * :text - Link text (default to the formatted revision)
109 # * :text - Link text (default to the formatted revision)
110 def link_to_revision(revision, repository, options={})
110 def link_to_revision(revision, repository, options={})
111 if repository.is_a?(Project)
111 if repository.is_a?(Project)
112 repository = repository.repository
112 repository = repository.repository
113 end
113 end
114 text = options.delete(:text) || format_revision(revision)
114 text = options.delete(:text) || format_revision(revision)
115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
116 link_to(
116 link_to(
117 h(text),
117 h(text),
118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
119 :title => l(:label_revision_id, format_revision(revision)),
119 :title => l(:label_revision_id, format_revision(revision)),
120 :accesskey => options[:accesskey]
120 :accesskey => options[:accesskey]
121 )
121 )
122 end
122 end
123
123
124 # Generates a link to a message
124 # Generates a link to a message
125 def link_to_message(message, options={}, html_options = nil)
125 def link_to_message(message, options={}, html_options = nil)
126 link_to(
126 link_to(
127 message.subject.truncate(60),
127 message.subject.truncate(60),
128 board_message_url(message.board_id, message.parent_id || message.id, {
128 board_message_url(message.board_id, message.parent_id || message.id, {
129 :r => (message.parent_id && message.id),
129 :r => (message.parent_id && message.id),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
131 :only_path => true
131 :only_path => true
132 }.merge(options)),
132 }.merge(options)),
133 html_options
133 html_options
134 )
134 )
135 end
135 end
136
136
137 # Generates a link to a project if active
137 # Generates a link to a project if active
138 # Examples:
138 # Examples:
139 #
139 #
140 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project) # => link to the specified project overview
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 #
143 #
144 def link_to_project(project, options={}, html_options = nil)
144 def link_to_project(project, options={}, html_options = nil)
145 if project.archived?
145 if project.archived?
146 h(project.name)
146 h(project.name)
147 else
147 else
148 link_to project.name,
148 link_to project.name,
149 project_url(project, {:only_path => true}.merge(options)),
149 project_url(project, {:only_path => true}.merge(options)),
150 html_options
150 html_options
151 end
151 end
152 end
152 end
153
153
154 # Generates a link to a project settings if active
154 # Generates a link to a project settings if active
155 def link_to_project_settings(project, options={}, html_options=nil)
155 def link_to_project_settings(project, options={}, html_options=nil)
156 if project.active?
156 if project.active?
157 link_to project.name, settings_project_path(project, options), html_options
157 link_to project.name, settings_project_path(project, options), html_options
158 elsif project.archived?
158 elsif project.archived?
159 h(project.name)
159 h(project.name)
160 else
160 else
161 link_to project.name, project_path(project, options), html_options
161 link_to project.name, project_path(project, options), html_options
162 end
162 end
163 end
163 end
164
164
165 # Generates a link to a version
165 # Generates a link to a version
166 def link_to_version(version, options = {})
166 def link_to_version(version, options = {})
167 return '' unless version && version.is_a?(Version)
167 return '' unless version && version.is_a?(Version)
168 options = {:title => format_date(version.effective_date)}.merge(options)
168 options = {:title => format_date(version.effective_date)}.merge(options)
169 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 link_to_if version.visible?, format_version_name(version), version_path(version), options
170 end
170 end
171
171
172 # Helper that formats object for html or text rendering
172 # Helper that formats object for html or text rendering
173 def format_object(object, html=true, &block)
173 def format_object(object, html=true, &block)
174 if block_given?
174 if block_given?
175 object = yield object
175 object = yield object
176 end
176 end
177 case object.class.name
177 case object.class.name
178 when 'Array'
178 when 'Array'
179 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 object.map {|o| format_object(o, html)}.join(', ').html_safe
180 when 'Time'
180 when 'Time'
181 format_time(object)
181 format_time(object)
182 when 'Date'
182 when 'Date'
183 format_date(object)
183 format_date(object)
184 when 'Fixnum'
184 when 'Fixnum'
185 object.to_s
185 object.to_s
186 when 'Float'
186 when 'Float'
187 sprintf "%.2f", object
187 sprintf "%.2f", object
188 when 'User'
188 when 'User'
189 html ? link_to_user(object) : object.to_s
189 html ? link_to_user(object) : object.to_s
190 when 'Project'
190 when 'Project'
191 html ? link_to_project(object) : object.to_s
191 html ? link_to_project(object) : object.to_s
192 when 'Version'
192 when 'Version'
193 html ? link_to_version(object) : object.to_s
193 html ? link_to_version(object) : object.to_s
194 when 'TrueClass'
194 when 'TrueClass'
195 l(:general_text_Yes)
195 l(:general_text_Yes)
196 when 'FalseClass'
196 when 'FalseClass'
197 l(:general_text_No)
197 l(:general_text_No)
198 when 'Issue'
198 when 'Issue'
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
200 when 'Attachment'
200 when 'Attachment'
201 html ? link_to_attachment(object, :download => true) : object.filename
201 html ? link_to_attachment(object, :download => true) : object.filename
202 when 'CustomValue', 'CustomFieldValue'
202 when 'CustomValue', 'CustomFieldValue'
203 if object.custom_field
203 if object.custom_field
204 f = object.custom_field.format.formatted_custom_value(self, object, html)
204 f = object.custom_field.format.formatted_custom_value(self, object, html)
205 if f.nil? || f.is_a?(String)
205 if f.nil? || f.is_a?(String)
206 f
206 f
207 else
207 else
208 format_object(f, html, &block)
208 format_object(f, html, &block)
209 end
209 end
210 else
210 else
211 object.value.to_s
211 object.value.to_s
212 end
212 end
213 else
213 else
214 html ? h(object) : object.to_s
214 html ? h(object) : object.to_s
215 end
215 end
216 end
216 end
217
217
218 def wiki_page_path(page, options={})
218 def wiki_page_path(page, options={})
219 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
219 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
220 end
220 end
221
221
222 def thumbnail_tag(attachment)
222 def thumbnail_tag(attachment)
223 link_to image_tag(thumbnail_path(attachment)),
223 link_to image_tag(thumbnail_path(attachment)),
224 named_attachment_path(attachment, attachment.filename),
224 named_attachment_path(attachment, attachment.filename),
225 :title => attachment.filename
225 :title => attachment.filename
226 end
226 end
227
227
228 def toggle_link(name, id, options={})
228 def toggle_link(name, id, options={})
229 onclick = "$('##{id}').toggle(); "
229 onclick = "$('##{id}').toggle(); "
230 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
230 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
231 onclick << "return false;"
231 onclick << "return false;"
232 link_to(name, "#", :onclick => onclick)
232 link_to(name, "#", :onclick => onclick)
233 end
233 end
234
234
235 # Used to format item titles on the activity view
235 # Used to format item titles on the activity view
236 def format_activity_title(text)
236 def format_activity_title(text)
237 text
237 text
238 end
238 end
239
239
240 def format_activity_day(date)
240 def format_activity_day(date)
241 date == User.current.today ? l(:label_today).titleize : format_date(date)
241 date == User.current.today ? l(:label_today).titleize : format_date(date)
242 end
242 end
243
243
244 def format_activity_description(text)
244 def format_activity_description(text)
245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
246 ).gsub(/[\r\n]+/, "<br />").html_safe
246 ).gsub(/[\r\n]+/, "<br />").html_safe
247 end
247 end
248
248
249 def format_version_name(version)
249 def format_version_name(version)
250 if version.project == @project
250 if version.project == @project
251 h(version)
251 h(version)
252 else
252 else
253 h("#{version.project} - #{version}")
253 h("#{version.project} - #{version}")
254 end
254 end
255 end
255 end
256
256
257 def due_date_distance_in_words(date)
257 def due_date_distance_in_words(date)
258 if date
258 if date
259 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
259 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
260 end
260 end
261 end
261 end
262
262
263 # Renders a tree of projects as a nested set of unordered lists
263 # Renders a tree of projects as a nested set of unordered lists
264 # The given collection may be a subset of the whole project tree
264 # The given collection may be a subset of the whole project tree
265 # (eg. some intermediate nodes are private and can not be seen)
265 # (eg. some intermediate nodes are private and can not be seen)
266 def render_project_nested_lists(projects, &block)
266 def render_project_nested_lists(projects, &block)
267 s = ''
267 s = ''
268 if projects.any?
268 if projects.any?
269 ancestors = []
269 ancestors = []
270 original_project = @project
270 original_project = @project
271 projects.sort_by(&:lft).each do |project|
271 projects.sort_by(&:lft).each do |project|
272 # set the project environment to please macros.
272 # set the project environment to please macros.
273 @project = project
273 @project = project
274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
276 else
276 else
277 ancestors.pop
277 ancestors.pop
278 s << "</li>"
278 s << "</li>"
279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
280 ancestors.pop
280 ancestors.pop
281 s << "</ul></li>\n"
281 s << "</ul></li>\n"
282 end
282 end
283 end
283 end
284 classes = (ancestors.empty? ? 'root' : 'child')
284 classes = (ancestors.empty? ? 'root' : 'child')
285 s << "<li class='#{classes}'><div class='#{classes}'>"
285 s << "<li class='#{classes}'><div class='#{classes}'>"
286 s << h(block_given? ? capture(project, &block) : project.name)
286 s << h(block_given? ? capture(project, &block) : project.name)
287 s << "</div>\n"
287 s << "</div>\n"
288 ancestors << project
288 ancestors << project
289 end
289 end
290 s << ("</li></ul>\n" * ancestors.size)
290 s << ("</li></ul>\n" * ancestors.size)
291 @project = original_project
291 @project = original_project
292 end
292 end
293 s.html_safe
293 s.html_safe
294 end
294 end
295
295
296 def render_page_hierarchy(pages, node=nil, options={})
296 def render_page_hierarchy(pages, node=nil, options={})
297 content = ''
297 content = ''
298 if pages[node]
298 if pages[node]
299 content << "<ul class=\"pages-hierarchy\">\n"
299 content << "<ul class=\"pages-hierarchy\">\n"
300 pages[node].each do |page|
300 pages[node].each do |page|
301 content << "<li>"
301 content << "<li>"
302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
305 content << "</li>\n"
305 content << "</li>\n"
306 end
306 end
307 content << "</ul>\n"
307 content << "</ul>\n"
308 end
308 end
309 content.html_safe
309 content.html_safe
310 end
310 end
311
311
312 # Renders flash messages
312 # Renders flash messages
313 def render_flash_messages
313 def render_flash_messages
314 s = ''
314 s = ''
315 flash.each do |k,v|
315 flash.each do |k,v|
316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
317 end
317 end
318 s.html_safe
318 s.html_safe
319 end
319 end
320
320
321 # Renders tabs and their content
321 # Renders tabs and their content
322 def render_tabs(tabs, selected=params[:tab])
322 def render_tabs(tabs, selected=params[:tab])
323 if tabs.any?
323 if tabs.any?
324 unless tabs.detect {|tab| tab[:name] == selected}
324 unless tabs.detect {|tab| tab[:name] == selected}
325 selected = nil
325 selected = nil
326 end
326 end
327 selected ||= tabs.first[:name]
327 selected ||= tabs.first[:name]
328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
329 else
329 else
330 content_tag 'p', l(:label_no_data), :class => "nodata"
330 content_tag 'p', l(:label_no_data), :class => "nodata"
331 end
331 end
332 end
332 end
333
333
334 # Renders the project quick-jump box
334 # Renders the project quick-jump box
335 def render_project_jump_box
335 def render_project_jump_box
336 return unless User.current.logged?
336 return unless User.current.logged?
337 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
337 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
338 if projects.any?
338 if projects.any?
339 options =
339 options =
340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
341 '<option value="" disabled="disabled">---</option>').html_safe
341 '<option value="" disabled="disabled">---</option>').html_safe
342
342
343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
344 { :value => project_path(:id => p, :jump => current_menu_item) }
344 { :value => project_path(:id => p, :jump => current_menu_item) }
345 end
345 end
346
346
347 content_tag( :span, nil, :class => 'jump-box-arrow') +
347 content_tag( :span, nil, :class => 'jump-box-arrow') +
348 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
348 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
349 end
349 end
350 end
350 end
351
351
352 def project_tree_options_for_select(projects, options = {})
352 def project_tree_options_for_select(projects, options = {})
353 s = ''.html_safe
353 s = ''.html_safe
354 if blank_text = options[:include_blank]
354 if blank_text = options[:include_blank]
355 if blank_text == true
355 if blank_text == true
356 blank_text = '&nbsp;'.html_safe
356 blank_text = '&nbsp;'.html_safe
357 end
357 end
358 s << content_tag('option', blank_text, :value => '')
358 s << content_tag('option', blank_text, :value => '')
359 end
359 end
360 project_tree(projects) do |project, level|
360 project_tree(projects) do |project, level|
361 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
361 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
362 tag_options = {:value => project.id}
362 tag_options = {:value => project.id}
363 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
363 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
364 tag_options[:selected] = 'selected'
364 tag_options[:selected] = 'selected'
365 else
365 else
366 tag_options[:selected] = nil
366 tag_options[:selected] = nil
367 end
367 end
368 tag_options.merge!(yield(project)) if block_given?
368 tag_options.merge!(yield(project)) if block_given?
369 s << content_tag('option', name_prefix + h(project), tag_options)
369 s << content_tag('option', name_prefix + h(project), tag_options)
370 end
370 end
371 s.html_safe
371 s.html_safe
372 end
372 end
373
373
374 # Yields the given block for each project with its level in the tree
374 # Yields the given block for each project with its level in the tree
375 #
375 #
376 # Wrapper for Project#project_tree
376 # Wrapper for Project#project_tree
377 def project_tree(projects, options={}, &block)
377 def project_tree(projects, options={}, &block)
378 Project.project_tree(projects, options, &block)
378 Project.project_tree(projects, options, &block)
379 end
379 end
380
380
381 def principals_check_box_tags(name, principals)
381 def principals_check_box_tags(name, principals)
382 s = ''
382 s = ''
383 principals.each do |principal|
383 principals.each do |principal|
384 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
384 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
385 end
385 end
386 s.html_safe
386 s.html_safe
387 end
387 end
388
388
389 # Returns a string for users/groups option tags
389 # Returns a string for users/groups option tags
390 def principals_options_for_select(collection, selected=nil)
390 def principals_options_for_select(collection, selected=nil)
391 s = ''
391 s = ''
392 if collection.include?(User.current)
392 if collection.include?(User.current)
393 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
393 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
394 end
394 end
395 groups = ''
395 groups = ''
396 collection.sort.each do |element|
396 collection.sort.each do |element|
397 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
397 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
398 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
398 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
399 end
399 end
400 unless groups.empty?
400 unless groups.empty?
401 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
401 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
402 end
402 end
403 s.html_safe
403 s.html_safe
404 end
404 end
405
405
406 def option_tag(name, text, value, selected=nil, options={})
406 def option_tag(name, text, value, selected=nil, options={})
407 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
407 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
408 end
408 end
409
409
410 def truncate_single_line_raw(string, length)
410 def truncate_single_line_raw(string, length)
411 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
411 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
412 end
412 end
413
413
414 # Truncates at line break after 250 characters or options[:length]
414 # Truncates at line break after 250 characters or options[:length]
415 def truncate_lines(string, options={})
415 def truncate_lines(string, options={})
416 length = options[:length] || 250
416 length = options[:length] || 250
417 if string.to_s =~ /\A(.{#{length}}.*?)$/m
417 if string.to_s =~ /\A(.{#{length}}.*?)$/m
418 "#{$1}..."
418 "#{$1}..."
419 else
419 else
420 string
420 string
421 end
421 end
422 end
422 end
423
423
424 def anchor(text)
424 def anchor(text)
425 text.to_s.gsub(' ', '_')
425 text.to_s.gsub(' ', '_')
426 end
426 end
427
427
428 def html_hours(text)
428 def html_hours(text)
429 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
429 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
430 end
430 end
431
431
432 def authoring(created, author, options={})
432 def authoring(created, author, options={})
433 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
433 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
434 end
434 end
435
435
436 def time_tag(time)
436 def time_tag(time)
437 text = distance_of_time_in_words(Time.now, time)
437 text = distance_of_time_in_words(Time.now, time)
438 if @project
438 if @project
439 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
439 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
440 else
440 else
441 content_tag('abbr', text, :title => format_time(time))
441 content_tag('abbr', text, :title => format_time(time))
442 end
442 end
443 end
443 end
444
444
445 def syntax_highlight_lines(name, content)
445 def syntax_highlight_lines(name, content)
446 lines = []
446 lines = []
447 syntax_highlight(name, content).each_line { |line| lines << line }
447 syntax_highlight(name, content).each_line { |line| lines << line }
448 lines
448 lines
449 end
449 end
450
450
451 def syntax_highlight(name, content)
451 def syntax_highlight(name, content)
452 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
452 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
453 end
453 end
454
454
455 def to_path_param(path)
455 def to_path_param(path)
456 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
456 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
457 str.blank? ? nil : str
457 str.blank? ? nil : str
458 end
458 end
459
459
460 def reorder_links(name, url, method = :post)
460 def reorder_links(name, url, method = :post)
461 # TODO: remove associated styles from application.css too
461 # TODO: remove associated styles from application.css too
462 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
462 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
463
463
464 link_to(l(:label_sort_highest),
464 link_to(l(:label_sort_highest),
465 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
465 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
466 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
466 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
467 link_to(l(:label_sort_higher),
467 link_to(l(:label_sort_higher),
468 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
468 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
469 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
469 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
470 link_to(l(:label_sort_lower),
470 link_to(l(:label_sort_lower),
471 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
471 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
472 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
472 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
473 link_to(l(:label_sort_lowest),
473 link_to(l(:label_sort_lowest),
474 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
474 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
475 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
475 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
476 end
476 end
477
477
478 def reorder_handle(object, options={})
478 def reorder_handle(object, options={})
479 data = {
479 data = {
480 :reorder_url => options[:url] || url_for(object),
480 :reorder_url => options[:url] || url_for(object),
481 :reorder_param => options[:param] || object.class.name.underscore
481 :reorder_param => options[:param] || object.class.name.underscore
482 }
482 }
483 content_tag('span', '',
483 content_tag('span', '',
484 :class => "sort-handle",
484 :class => "sort-handle",
485 :data => data,
485 :data => data,
486 :title => l(:button_sort))
486 :title => l(:button_sort))
487 end
487 end
488
488
489 def breadcrumb(*args)
489 def breadcrumb(*args)
490 elements = args.flatten
490 elements = args.flatten
491 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
491 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
492 end
492 end
493
493
494 def other_formats_links(&block)
494 def other_formats_links(&block)
495 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
495 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
496 yield Redmine::Views::OtherFormatsBuilder.new(self)
496 yield Redmine::Views::OtherFormatsBuilder.new(self)
497 concat('</p>'.html_safe)
497 concat('</p>'.html_safe)
498 end
498 end
499
499
500 def page_header_title
500 def page_header_title
501 if @project.nil? || @project.new_record?
501 if @project.nil? || @project.new_record?
502 h(Setting.app_title)
502 h(Setting.app_title)
503 else
503 else
504 b = []
504 b = []
505 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
505 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
506 if ancestors.any?
506 if ancestors.any?
507 root = ancestors.shift
507 root = ancestors.shift
508 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
508 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
509 if ancestors.size > 2
509 if ancestors.size > 2
510 b << "\xe2\x80\xa6"
510 b << "\xe2\x80\xa6"
511 ancestors = ancestors[-2, 2]
511 ancestors = ancestors[-2, 2]
512 end
512 end
513 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
513 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
514 end
514 end
515 b << content_tag(:span, h(@project), class: 'current-project')
515 b << content_tag(:span, h(@project), class: 'current-project')
516 if b.size > 1
516 if b.size > 1
517 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
517 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
518 path = safe_join(b[0..-2], separator) + separator
518 path = safe_join(b[0..-2], separator) + separator
519 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
519 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
520 end
520 end
521 safe_join b
521 safe_join b
522 end
522 end
523 end
523 end
524
524
525 # Returns a h2 tag and sets the html title with the given arguments
525 # Returns a h2 tag and sets the html title with the given arguments
526 def title(*args)
526 def title(*args)
527 strings = args.map do |arg|
527 strings = args.map do |arg|
528 if arg.is_a?(Array) && arg.size >= 2
528 if arg.is_a?(Array) && arg.size >= 2
529 link_to(*arg)
529 link_to(*arg)
530 else
530 else
531 h(arg.to_s)
531 h(arg.to_s)
532 end
532 end
533 end
533 end
534 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
534 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
535 content_tag('h2', strings.join(' &#187; ').html_safe)
535 content_tag('h2', strings.join(' &#187; ').html_safe)
536 end
536 end
537
537
538 # Sets the html title
538 # Sets the html title
539 # Returns the html title when called without arguments
539 # Returns the html title when called without arguments
540 # Current project name and app_title and automatically appended
540 # Current project name and app_title and automatically appended
541 # Exemples:
541 # Exemples:
542 # html_title 'Foo', 'Bar'
542 # html_title 'Foo', 'Bar'
543 # html_title # => 'Foo - Bar - My Project - Redmine'
543 # html_title # => 'Foo - Bar - My Project - Redmine'
544 def html_title(*args)
544 def html_title(*args)
545 if args.empty?
545 if args.empty?
546 title = @html_title || []
546 title = @html_title || []
547 title << @project.name if @project
547 title << @project.name if @project
548 title << Setting.app_title unless Setting.app_title == title.last
548 title << Setting.app_title unless Setting.app_title == title.last
549 title.reject(&:blank?).join(' - ')
549 title.reject(&:blank?).join(' - ')
550 else
550 else
551 @html_title ||= []
551 @html_title ||= []
552 @html_title += args
552 @html_title += args
553 end
553 end
554 end
554 end
555
555
556 # Returns the theme, controller name, and action as css classes for the
556 # Returns the theme, controller name, and action as css classes for the
557 # HTML body.
557 # HTML body.
558 def body_css_classes
558 def body_css_classes
559 css = []
559 css = []
560 if theme = Redmine::Themes.theme(Setting.ui_theme)
560 if theme = Redmine::Themes.theme(Setting.ui_theme)
561 css << 'theme-' + theme.name
561 css << 'theme-' + theme.name
562 end
562 end
563
563
564 css << 'project-' + @project.identifier if @project && @project.identifier.present?
564 css << 'project-' + @project.identifier if @project && @project.identifier.present?
565 css << 'controller-' + controller_name
565 css << 'controller-' + controller_name
566 css << 'action-' + action_name
566 css << 'action-' + action_name
567 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
567 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
568 css << "textarea-#{User.current.pref.textarea_font}"
568 css << "textarea-#{User.current.pref.textarea_font}"
569 end
569 end
570 css.join(' ')
570 css.join(' ')
571 end
571 end
572
572
573 def accesskey(s)
573 def accesskey(s)
574 @used_accesskeys ||= []
574 @used_accesskeys ||= []
575 key = Redmine::AccessKeys.key_for(s)
575 key = Redmine::AccessKeys.key_for(s)
576 return nil if @used_accesskeys.include?(key)
576 return nil if @used_accesskeys.include?(key)
577 @used_accesskeys << key
577 @used_accesskeys << key
578 key
578 key
579 end
579 end
580
580
581 # Formats text according to system settings.
581 # Formats text according to system settings.
582 # 2 ways to call this method:
582 # 2 ways to call this method:
583 # * with a String: textilizable(text, options)
583 # * with a String: textilizable(text, options)
584 # * with an object and one of its attribute: textilizable(issue, :description, options)
584 # * with an object and one of its attribute: textilizable(issue, :description, options)
585 def textilizable(*args)
585 def textilizable(*args)
586 options = args.last.is_a?(Hash) ? args.pop : {}
586 options = args.last.is_a?(Hash) ? args.pop : {}
587 case args.size
587 case args.size
588 when 1
588 when 1
589 obj = options[:object]
589 obj = options[:object]
590 text = args.shift
590 text = args.shift
591 when 2
591 when 2
592 obj = args.shift
592 obj = args.shift
593 attr = args.shift
593 attr = args.shift
594 text = obj.send(attr).to_s
594 text = obj.send(attr).to_s
595 else
595 else
596 raise ArgumentError, 'invalid arguments to textilizable'
596 raise ArgumentError, 'invalid arguments to textilizable'
597 end
597 end
598 return '' if text.blank?
598 return '' if text.blank?
599 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
599 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
600 @only_path = only_path = options.delete(:only_path) == false ? false : true
600 @only_path = only_path = options.delete(:only_path) == false ? false : true
601
601
602 text = text.dup
602 text = text.dup
603 macros = catch_macros(text)
603 macros = catch_macros(text)
604 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
604 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
605
605
606 @parsed_headings = []
606 @parsed_headings = []
607 @heading_anchors = {}
607 @heading_anchors = {}
608 @current_section = 0 if options[:edit_section_links]
608 @current_section = 0 if options[:edit_section_links]
609
609
610 parse_sections(text, project, obj, attr, only_path, options)
610 parse_sections(text, project, obj, attr, only_path, options)
611 text = parse_non_pre_blocks(text, obj, macros) do |text|
611 text = parse_non_pre_blocks(text, obj, macros) do |text|
612 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
612 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
613 send method_name, text, project, obj, attr, only_path, options
613 send method_name, text, project, obj, attr, only_path, options
614 end
614 end
615 end
615 end
616 parse_headings(text, project, obj, attr, only_path, options)
616 parse_headings(text, project, obj, attr, only_path, options)
617
617
618 if @parsed_headings.any?
618 if @parsed_headings.any?
619 replace_toc(text, @parsed_headings)
619 replace_toc(text, @parsed_headings)
620 end
620 end
621
621
622 text.html_safe
622 text.html_safe
623 end
623 end
624
624
625 def parse_non_pre_blocks(text, obj, macros)
625 def parse_non_pre_blocks(text, obj, macros)
626 s = StringScanner.new(text)
626 s = StringScanner.new(text)
627 tags = []
627 tags = []
628 parsed = ''
628 parsed = ''
629 while !s.eos?
629 while !s.eos?
630 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
630 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
631 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
631 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
632 if tags.empty?
632 if tags.empty?
633 yield text
633 yield text
634 inject_macros(text, obj, macros) if macros.any?
634 inject_macros(text, obj, macros) if macros.any?
635 else
635 else
636 inject_macros(text, obj, macros, false) if macros.any?
636 inject_macros(text, obj, macros, false) if macros.any?
637 end
637 end
638 parsed << text
638 parsed << text
639 if tag
639 if tag
640 if closing
640 if closing
641 if tags.last && tags.last.casecmp(tag) == 0
641 if tags.last && tags.last.casecmp(tag) == 0
642 tags.pop
642 tags.pop
643 end
643 end
644 else
644 else
645 tags << tag.downcase
645 tags << tag.downcase
646 end
646 end
647 parsed << full_tag
647 parsed << full_tag
648 end
648 end
649 end
649 end
650 # Close any non closing tags
650 # Close any non closing tags
651 while tag = tags.pop
651 while tag = tags.pop
652 parsed << "</#{tag}>"
652 parsed << "</#{tag}>"
653 end
653 end
654 parsed
654 parsed
655 end
655 end
656
656
657 def parse_inline_attachments(text, project, obj, attr, only_path, options)
657 def parse_inline_attachments(text, project, obj, attr, only_path, options)
658 return if options[:inline_attachments] == false
658 return if options[:inline_attachments] == false
659
659
660 # when using an image link, try to use an attachment, if possible
660 # when using an image link, try to use an attachment, if possible
661 attachments = options[:attachments] || []
661 attachments = options[:attachments] || []
662 attachments += obj.attachments if obj.respond_to?(:attachments)
662 attachments += obj.attachments if obj.respond_to?(:attachments)
663 if attachments.present?
663 if attachments.present?
664 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
664 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
665 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
665 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
666 # search for the picture in attachments
666 # search for the picture in attachments
667 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
667 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
668 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
668 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
669 desc = found.description.to_s.gsub('"', '')
669 desc = found.description.to_s.gsub('"', '')
670 if !desc.blank? && alttext.blank?
670 if !desc.blank? && alttext.blank?
671 alt = " title=\"#{desc}\" alt=\"#{desc}\""
671 alt = " title=\"#{desc}\" alt=\"#{desc}\""
672 end
672 end
673 "src=\"#{image_url}\"#{alt}"
673 "src=\"#{image_url}\"#{alt}"
674 else
674 else
675 m
675 m
676 end
676 end
677 end
677 end
678 end
678 end
679 end
679 end
680
680
681 # Wiki links
681 # Wiki links
682 #
682 #
683 # Examples:
683 # Examples:
684 # [[mypage]]
684 # [[mypage]]
685 # [[mypage|mytext]]
685 # [[mypage|mytext]]
686 # wiki links can refer other project wikis, using project name or identifier:
686 # wiki links can refer other project wikis, using project name or identifier:
687 # [[project:]] -> wiki starting page
687 # [[project:]] -> wiki starting page
688 # [[project:|mytext]]
688 # [[project:|mytext]]
689 # [[project:mypage]]
689 # [[project:mypage]]
690 # [[project:mypage|mytext]]
690 # [[project:mypage|mytext]]
691 def parse_wiki_links(text, project, obj, attr, only_path, options)
691 def parse_wiki_links(text, project, obj, attr, only_path, options)
692 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
692 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
693 link_project = project
693 link_project = project
694 esc, all, page, title = $1, $2, $3, $5
694 esc, all, page, title = $1, $2, $3, $5
695 if esc.nil?
695 if esc.nil?
696 if page =~ /^([^\:]+)\:(.*)$/
696 if page =~ /^([^\:]+)\:(.*)$/
697 identifier, page = $1, $2
697 identifier, page = $1, $2
698 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
698 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
699 title ||= identifier if page.blank?
699 title ||= identifier if page.blank?
700 end
700 end
701
701
702 if link_project && link_project.wiki
702 if link_project && link_project.wiki
703 # extract anchor
703 # extract anchor
704 anchor = nil
704 anchor = nil
705 if page =~ /^(.+?)\#(.+)$/
705 if page =~ /^(.+?)\#(.+)$/
706 page, anchor = $1, $2
706 page, anchor = $1, $2
707 end
707 end
708 anchor = sanitize_anchor_name(anchor) if anchor.present?
708 anchor = sanitize_anchor_name(anchor) if anchor.present?
709 # check if page exists
709 # check if page exists
710 wiki_page = link_project.wiki.find_page(page)
710 wiki_page = link_project.wiki.find_page(page)
711 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
711 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
712 "##{anchor}"
712 "##{anchor}"
713 else
713 else
714 case options[:wiki_links]
714 case options[:wiki_links]
715 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
715 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
716 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
716 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
717 else
717 else
718 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
718 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
719 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
719 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
720 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
720 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
721 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
721 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
722 end
722 end
723 end
723 end
724 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
724 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
725 else
725 else
726 # project or wiki doesn't exist
726 # project or wiki doesn't exist
727 all
727 all
728 end
728 end
729 else
729 else
730 all
730 all
731 end
731 end
732 end
732 end
733 end
733 end
734
734
735 # Redmine links
735 # Redmine links
736 #
736 #
737 # Examples:
737 # Examples:
738 # Issues:
738 # Issues:
739 # #52 -> Link to issue #52
739 # #52 -> Link to issue #52
740 # Changesets:
740 # Changesets:
741 # r52 -> Link to revision 52
741 # r52 -> Link to revision 52
742 # commit:a85130f -> Link to scmid starting with a85130f
742 # commit:a85130f -> Link to scmid starting with a85130f
743 # Documents:
743 # Documents:
744 # document#17 -> Link to document with id 17
744 # document#17 -> Link to document with id 17
745 # document:Greetings -> Link to the document with title "Greetings"
745 # document:Greetings -> Link to the document with title "Greetings"
746 # document:"Some document" -> Link to the document with title "Some document"
746 # document:"Some document" -> Link to the document with title "Some document"
747 # Versions:
747 # Versions:
748 # version#3 -> Link to version with id 3
748 # version#3 -> Link to version with id 3
749 # version:1.0.0 -> Link to version named "1.0.0"
749 # version:1.0.0 -> Link to version named "1.0.0"
750 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
750 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
751 # Attachments:
751 # Attachments:
752 # attachment:file.zip -> Link to the attachment of the current object named file.zip
752 # attachment:file.zip -> Link to the attachment of the current object named file.zip
753 # Source files:
753 # Source files:
754 # source:some/file -> Link to the file located at /some/file in the project's repository
754 # source:some/file -> Link to the file located at /some/file in the project's repository
755 # source:some/file@52 -> Link to the file's revision 52
755 # source:some/file@52 -> Link to the file's revision 52
756 # source:some/file#L120 -> Link to line 120 of the file
756 # source:some/file#L120 -> Link to line 120 of the file
757 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
757 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
758 # export:some/file -> Force the download of the file
758 # export:some/file -> Force the download of the file
759 # Forum messages:
759 # Forum messages:
760 # message#1218 -> Link to message with id 1218
760 # message#1218 -> Link to message with id 1218
761 # Projects:
761 # Projects:
762 # project:someproject -> Link to project named "someproject"
762 # project:someproject -> Link to project named "someproject"
763 # project#3 -> Link to project with id 3
763 # project#3 -> Link to project with id 3
764 #
764 #
765 # Links can refer other objects from other projects, using project identifier:
765 # Links can refer other objects from other projects, using project identifier:
766 # identifier:r52
766 # identifier:r52
767 # identifier:document:"Some document"
767 # identifier:document:"Some document"
768 # identifier:version:1.0.0
768 # identifier:version:1.0.0
769 # identifier:source:some/file
769 # identifier:source:some/file
770 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
770 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
771 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|
771 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|
772 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
772 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
773 if tag_content
773 if tag_content
774 $&
774 $&
775 else
775 else
776 link = nil
776 link = nil
777 project = default_project
777 project = default_project
778 if project_identifier
778 if project_identifier
779 project = Project.visible.find_by_identifier(project_identifier)
779 project = Project.visible.find_by_identifier(project_identifier)
780 end
780 end
781 if esc.nil?
781 if esc.nil?
782 if prefix.nil? && sep == 'r'
782 if prefix.nil? && sep == 'r'
783 if project
783 if project
784 repository = nil
784 repository = nil
785 if repo_identifier
785 if repo_identifier
786 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
786 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
787 else
787 else
788 repository = project.repository
788 repository = project.repository
789 end
789 end
790 # project.changesets.visible raises an SQL error because of a double join on repositories
790 # project.changesets.visible raises an SQL error because of a double join on repositories
791 if repository &&
791 if repository &&
792 (changeset = Changeset.visible.
792 (changeset = Changeset.visible.
793 find_by_repository_id_and_revision(repository.id, identifier))
793 find_by_repository_id_and_revision(repository.id, identifier))
794 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
794 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
795 {:only_path => only_path, :controller => 'repositories',
795 {:only_path => only_path, :controller => 'repositories',
796 :action => 'revision', :id => project,
796 :action => 'revision', :id => project,
797 :repository_id => repository.identifier_param,
797 :repository_id => repository.identifier_param,
798 :rev => changeset.revision},
798 :rev => changeset.revision},
799 :class => 'changeset',
799 :class => 'changeset',
800 :title => truncate_single_line_raw(changeset.comments, 100))
800 :title => truncate_single_line_raw(changeset.comments, 100))
801 end
801 end
802 end
802 end
803 elsif sep == '#'
803 elsif sep == '#'
804 oid = identifier.to_i
804 oid = identifier.to_i
805 case prefix
805 case prefix
806 when nil
806 when nil
807 if oid.to_s == identifier &&
807 if oid.to_s == identifier &&
808 issue = Issue.visible.find_by_id(oid)
808 issue = Issue.visible.find_by_id(oid)
809 anchor = comment_id ? "note-#{comment_id}" : nil
809 anchor = comment_id ? "note-#{comment_id}" : nil
810 link = link_to("##{oid}#{comment_suffix}",
810 link = link_to("##{oid}#{comment_suffix}",
811 issue_url(issue, :only_path => only_path, :anchor => anchor),
811 issue_url(issue, :only_path => only_path, :anchor => anchor),
812 :class => issue.css_classes,
812 :class => issue.css_classes,
813 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
813 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
814 end
814 end
815 when 'document'
815 when 'document'
816 if document = Document.visible.find_by_id(oid)
816 if document = Document.visible.find_by_id(oid)
817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
818 end
818 end
819 when 'version'
819 when 'version'
820 if version = Version.visible.find_by_id(oid)
820 if version = Version.visible.find_by_id(oid)
821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
822 end
822 end
823 when 'message'
823 when 'message'
824 if message = Message.visible.find_by_id(oid)
824 if message = Message.visible.find_by_id(oid)
825 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
825 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
826 end
826 end
827 when 'forum'
827 when 'forum'
828 if board = Board.visible.find_by_id(oid)
828 if board = Board.visible.find_by_id(oid)
829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
830 end
830 end
831 when 'news'
831 when 'news'
832 if news = News.visible.find_by_id(oid)
832 if news = News.visible.find_by_id(oid)
833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
834 end
834 end
835 when 'project'
835 when 'project'
836 if p = Project.visible.find_by_id(oid)
836 if p = Project.visible.find_by_id(oid)
837 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
837 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
838 end
838 end
839 end
839 end
840 elsif sep == ':'
840 elsif sep == ':'
841 # removes the double quotes if any
841 # removes the double quotes if any
842 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
842 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
843 name = CGI.unescapeHTML(name)
843 name = CGI.unescapeHTML(name)
844 case prefix
844 case prefix
845 when 'document'
845 when 'document'
846 if project && document = project.documents.visible.find_by_title(name)
846 if project && document = project.documents.visible.find_by_title(name)
847 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
847 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
848 end
848 end
849 when 'version'
849 when 'version'
850 if project && version = project.versions.visible.find_by_name(name)
850 if project && version = project.versions.visible.find_by_name(name)
851 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
851 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
852 end
852 end
853 when 'forum'
853 when 'forum'
854 if project && board = project.boards.visible.find_by_name(name)
854 if project && board = project.boards.visible.find_by_name(name)
855 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
855 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
856 end
856 end
857 when 'news'
857 when 'news'
858 if project && news = project.news.visible.find_by_title(name)
858 if project && news = project.news.visible.find_by_title(name)
859 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
859 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
860 end
860 end
861 when 'commit', 'source', 'export'
861 when 'commit', 'source', 'export'
862 if project
862 if project
863 repository = nil
863 repository = nil
864 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
864 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
865 repo_prefix, repo_identifier, name = $1, $2, $3
865 repo_prefix, repo_identifier, name = $1, $2, $3
866 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
866 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
867 else
867 else
868 repository = project.repository
868 repository = project.repository
869 end
869 end
870 if prefix == 'commit'
870 if prefix == 'commit'
871 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
871 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
872 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},
872 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},
873 :class => 'changeset',
873 :class => 'changeset',
874 :title => truncate_single_line_raw(changeset.comments, 100)
874 :title => truncate_single_line_raw(changeset.comments, 100)
875 end
875 end
876 else
876 else
877 if repository && User.current.allowed_to?(:browse_repository, project)
877 if repository && User.current.allowed_to?(:browse_repository, project)
878 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
878 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
879 path, rev, anchor = $1, $3, $5
879 path, rev, anchor = $1, $3, $5
880 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,
880 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,
881 :path => to_path_param(path),
881 :path => to_path_param(path),
882 :rev => rev,
882 :rev => rev,
883 :anchor => anchor},
883 :anchor => anchor},
884 :class => (prefix == 'export' ? 'source download' : 'source')
884 :class => (prefix == 'export' ? 'source download' : 'source')
885 end
885 end
886 end
886 end
887 repo_prefix = nil
887 repo_prefix = nil
888 end
888 end
889 when 'attachment'
889 when 'attachment'
890 attachments = options[:attachments] || []
890 attachments = options[:attachments] || []
891 attachments += obj.attachments if obj.respond_to?(:attachments)
891 attachments += obj.attachments if obj.respond_to?(:attachments)
892 if attachments && attachment = Attachment.latest_attach(attachments, name)
892 if attachments && attachment = Attachment.latest_attach(attachments, name)
893 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
893 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
894 end
894 end
895 when 'project'
895 when 'project'
896 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
896 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
897 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
897 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
898 end
898 end
899 end
899 end
900 end
900 end
901 end
901 end
902 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
902 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
903 end
903 end
904 end
904 end
905 end
905 end
906
906
907 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
907 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
908
908
909 def parse_sections(text, project, obj, attr, only_path, options)
909 def parse_sections(text, project, obj, attr, only_path, options)
910 return unless options[:edit_section_links]
910 return unless options[:edit_section_links]
911 text.gsub!(HEADING_RE) do
911 text.gsub!(HEADING_RE) do
912 heading, level = $1, $2
912 heading, level = $1, $2
913 @current_section += 1
913 @current_section += 1
914 if @current_section > 1
914 if @current_section > 1
915 content_tag('div',
915 content_tag('div',
916 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
916 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
917 :class => 'icon-only icon-edit'),
917 :class => 'icon-only icon-edit'),
918 :class => "contextual heading-#{level}",
918 :class => "contextual heading-#{level}",
919 :title => l(:button_edit_section),
919 :title => l(:button_edit_section),
920 :id => "section-#{@current_section}") + heading.html_safe
920 :id => "section-#{@current_section}") + heading.html_safe
921 else
921 else
922 heading
922 heading
923 end
923 end
924 end
924 end
925 end
925 end
926
926
927 # Headings and TOC
927 # Headings and TOC
928 # Adds ids and links to headings unless options[:headings] is set to false
928 # Adds ids and links to headings unless options[:headings] is set to false
929 def parse_headings(text, project, obj, attr, only_path, options)
929 def parse_headings(text, project, obj, attr, only_path, options)
930 return if options[:headings] == false
930 return if options[:headings] == false
931
931
932 text.gsub!(HEADING_RE) do
932 text.gsub!(HEADING_RE) do
933 level, attrs, content = $2.to_i, $3, $4
933 level, attrs, content = $2.to_i, $3, $4
934 item = strip_tags(content).strip
934 item = strip_tags(content).strip
935 anchor = sanitize_anchor_name(item)
935 anchor = sanitize_anchor_name(item)
936 # used for single-file wiki export
936 # used for single-file wiki export
937 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
937 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
938 @heading_anchors[anchor] ||= 0
938 @heading_anchors[anchor] ||= 0
939 idx = (@heading_anchors[anchor] += 1)
939 idx = (@heading_anchors[anchor] += 1)
940 if idx > 1
940 if idx > 1
941 anchor = "#{anchor}-#{idx}"
941 anchor = "#{anchor}-#{idx}"
942 end
942 end
943 @parsed_headings << [level, anchor, item]
943 @parsed_headings << [level, anchor, item]
944 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
944 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
945 end
945 end
946 end
946 end
947
947
948 MACROS_RE = /(
948 MACROS_RE = /(
949 (!)? # escaping
949 (!)? # escaping
950 (
950 (
951 \{\{ # opening tag
951 \{\{ # opening tag
952 ([\w]+) # macro name
952 ([\w]+) # macro name
953 (\(([^\n\r]*?)\))? # optional arguments
953 (\(([^\n\r]*?)\))? # optional arguments
954 ([\n\r].*?[\n\r])? # optional block of text
954 ([\n\r].*?[\n\r])? # optional block of text
955 \}\} # closing tag
955 \}\} # closing tag
956 )
956 )
957 )/mx unless const_defined?(:MACROS_RE)
957 )/mx unless const_defined?(:MACROS_RE)
958
958
959 MACRO_SUB_RE = /(
959 MACRO_SUB_RE = /(
960 \{\{
960 \{\{
961 macro\((\d+)\)
961 macro\((\d+)\)
962 \}\}
962 \}\}
963 )/x unless const_defined?(:MACRO_SUB_RE)
963 )/x unless const_defined?(:MACRO_SUB_RE)
964
964
965 # Extracts macros from text
965 # Extracts macros from text
966 def catch_macros(text)
966 def catch_macros(text)
967 macros = {}
967 macros = {}
968 text.gsub!(MACROS_RE) do
968 text.gsub!(MACROS_RE) do
969 all, macro = $1, $4.downcase
969 all, macro = $1, $4.downcase
970 if macro_exists?(macro) || all =~ MACRO_SUB_RE
970 if macro_exists?(macro) || all =~ MACRO_SUB_RE
971 index = macros.size
971 index = macros.size
972 macros[index] = all
972 macros[index] = all
973 "{{macro(#{index})}}"
973 "{{macro(#{index})}}"
974 else
974 else
975 all
975 all
976 end
976 end
977 end
977 end
978 macros
978 macros
979 end
979 end
980
980
981 # Executes and replaces macros in text
981 # Executes and replaces macros in text
982 def inject_macros(text, obj, macros, execute=true)
982 def inject_macros(text, obj, macros, execute=true)
983 text.gsub!(MACRO_SUB_RE) do
983 text.gsub!(MACRO_SUB_RE) do
984 all, index = $1, $2.to_i
984 all, index = $1, $2.to_i
985 orig = macros.delete(index)
985 orig = macros.delete(index)
986 if execute && orig && orig =~ MACROS_RE
986 if execute && orig && orig =~ MACROS_RE
987 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
987 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
988 if esc.nil?
988 if esc.nil?
989 h(exec_macro(macro, obj, args, block) || all)
989 h(exec_macro(macro, obj, args, block) || all)
990 else
990 else
991 h(all)
991 h(all)
992 end
992 end
993 elsif orig
993 elsif orig
994 h(orig)
994 h(orig)
995 else
995 else
996 h(all)
996 h(all)
997 end
997 end
998 end
998 end
999 end
999 end
1000
1000
1001 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1001 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1002
1002
1003 # Renders the TOC with given headings
1003 # Renders the TOC with given headings
1004 def replace_toc(text, headings)
1004 def replace_toc(text, headings)
1005 text.gsub!(TOC_RE) do
1005 text.gsub!(TOC_RE) do
1006 left_align, right_align = $2, $3
1006 left_align, right_align = $2, $3
1007 # Keep only the 4 first levels
1007 # Keep only the 4 first levels
1008 headings = headings.select{|level, anchor, item| level <= 4}
1008 headings = headings.select{|level, anchor, item| level <= 4}
1009 if headings.empty?
1009 if headings.empty?
1010 ''
1010 ''
1011 else
1011 else
1012 div_class = 'toc'
1012 div_class = 'toc'
1013 div_class << ' right' if right_align
1013 div_class << ' right' if right_align
1014 div_class << ' left' if left_align
1014 div_class << ' left' if left_align
1015 out = "<ul class=\"#{div_class}\"><li>"
1015 out = "<ul class=\"#{div_class}\"><li>"
1016 root = headings.map(&:first).min
1016 root = headings.map(&:first).min
1017 current = root
1017 current = root
1018 started = false
1018 started = false
1019 headings.each do |level, anchor, item|
1019 headings.each do |level, anchor, item|
1020 if level > current
1020 if level > current
1021 out << '<ul><li>' * (level - current)
1021 out << '<ul><li>' * (level - current)
1022 elsif level < current
1022 elsif level < current
1023 out << "</li></ul>\n" * (current - level) + "</li><li>"
1023 out << "</li></ul>\n" * (current - level) + "</li><li>"
1024 elsif started
1024 elsif started
1025 out << '</li><li>'
1025 out << '</li><li>'
1026 end
1026 end
1027 out << "<a href=\"##{anchor}\">#{item}</a>"
1027 out << "<a href=\"##{anchor}\">#{item}</a>"
1028 current = level
1028 current = level
1029 started = true
1029 started = true
1030 end
1030 end
1031 out << '</li></ul>' * (current - root)
1031 out << '</li></ul>' * (current - root)
1032 out << '</li></ul>'
1032 out << '</li></ul>'
1033 end
1033 end
1034 end
1034 end
1035 end
1035 end
1036
1036
1037 # Same as Rails' simple_format helper without using paragraphs
1037 # Same as Rails' simple_format helper without using paragraphs
1038 def simple_format_without_paragraph(text)
1038 def simple_format_without_paragraph(text)
1039 text.to_s.
1039 text.to_s.
1040 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1040 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1041 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1041 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1042 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1042 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1043 html_safe
1043 html_safe
1044 end
1044 end
1045
1045
1046 def lang_options_for_select(blank=true)
1046 def lang_options_for_select(blank=true)
1047 (blank ? [["(auto)", ""]] : []) + languages_options
1047 (blank ? [["(auto)", ""]] : []) + languages_options
1048 end
1048 end
1049
1049
1050 def labelled_form_for(*args, &proc)
1050 def labelled_form_for(*args, &proc)
1051 args << {} unless args.last.is_a?(Hash)
1051 args << {} unless args.last.is_a?(Hash)
1052 options = args.last
1052 options = args.last
1053 if args.first.is_a?(Symbol)
1053 if args.first.is_a?(Symbol)
1054 options.merge!(:as => args.shift)
1054 options.merge!(:as => args.shift)
1055 end
1055 end
1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1057 form_for(*args, &proc)
1057 form_for(*args, &proc)
1058 end
1058 end
1059
1059
1060 def labelled_fields_for(*args, &proc)
1060 def labelled_fields_for(*args, &proc)
1061 args << {} unless args.last.is_a?(Hash)
1061 args << {} unless args.last.is_a?(Hash)
1062 options = args.last
1062 options = args.last
1063 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1063 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1064 fields_for(*args, &proc)
1064 fields_for(*args, &proc)
1065 end
1065 end
1066
1066
1067 # Render the error messages for the given objects
1067 # Render the error messages for the given objects
1068 def error_messages_for(*objects)
1068 def error_messages_for(*objects)
1069 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1069 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1070 errors = objects.map {|o| o.errors.full_messages}.flatten
1070 errors = objects.map {|o| o.errors.full_messages}.flatten
1071 render_error_messages(errors)
1071 render_error_messages(errors)
1072 end
1072 end
1073
1073
1074 # Renders a list of error messages
1074 # Renders a list of error messages
1075 def render_error_messages(errors)
1075 def render_error_messages(errors)
1076 html = ""
1076 html = ""
1077 if errors.present?
1077 if errors.present?
1078 html << "<div id='errorExplanation'><ul>\n"
1078 html << "<div id='errorExplanation'><ul>\n"
1079 errors.each do |error|
1079 errors.each do |error|
1080 html << "<li>#{h error}</li>\n"
1080 html << "<li>#{h error}</li>\n"
1081 end
1081 end
1082 html << "</ul></div>\n"
1082 html << "</ul></div>\n"
1083 end
1083 end
1084 html.html_safe
1084 html.html_safe
1085 end
1085 end
1086
1086
1087 def delete_link(url, options={})
1087 def delete_link(url, options={})
1088 options = {
1088 options = {
1089 :method => :delete,
1089 :method => :delete,
1090 :data => {:confirm => l(:text_are_you_sure)},
1090 :data => {:confirm => l(:text_are_you_sure)},
1091 :class => 'icon icon-del'
1091 :class => 'icon icon-del'
1092 }.merge(options)
1092 }.merge(options)
1093
1093
1094 link_to l(:button_delete), url, options
1094 link_to l(:button_delete), url, options
1095 end
1095 end
1096
1096
1097 def preview_link(url, form, target='preview', options={})
1097 def preview_link(url, form, target='preview', options={})
1098 content_tag 'a', l(:label_preview), {
1098 content_tag 'a', l(:label_preview), {
1099 :href => "#",
1099 :href => "#",
1100 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1100 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1101 :accesskey => accesskey(:preview)
1101 :accesskey => accesskey(:preview)
1102 }.merge(options)
1102 }.merge(options)
1103 end
1103 end
1104
1104
1105 def link_to_function(name, function, html_options={})
1105 def link_to_function(name, function, html_options={})
1106 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1106 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1107 end
1107 end
1108
1108
1109 # Helper to render JSON in views
1109 # Helper to render JSON in views
1110 def raw_json(arg)
1110 def raw_json(arg)
1111 arg.to_json.to_s.gsub('/', '\/').html_safe
1111 arg.to_json.to_s.gsub('/', '\/').html_safe
1112 end
1112 end
1113
1113
1114 def back_url
1114 def back_url
1115 url = params[:back_url]
1115 url = params[:back_url]
1116 if url.nil? && referer = request.env['HTTP_REFERER']
1116 if url.nil? && referer = request.env['HTTP_REFERER']
1117 url = CGI.unescape(referer.to_s)
1117 url = CGI.unescape(referer.to_s)
1118 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1118 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1119 # parsed as invalid by URI.parse so the redirect to the back URL would
1119 # parsed as invalid by URI.parse so the redirect to the back URL would
1120 # not be accepted (ApplicationController#validate_back_url would return
1120 # not be accepted (ApplicationController#validate_back_url would return
1121 # false)
1121 # false)
1122 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1122 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1123 end
1123 end
1124 url
1124 url
1125 end
1125 end
1126
1126
1127 def back_url_hidden_field_tag
1127 def back_url_hidden_field_tag
1128 url = back_url
1128 url = back_url
1129 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1129 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1130 end
1130 end
1131
1131
1132 def check_all_links(form_name)
1132 def check_all_links(form_name)
1133 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1133 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1134 " | ".html_safe +
1134 " | ".html_safe +
1135 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1135 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1136 end
1136 end
1137
1137
1138 def toggle_checkboxes_link(selector)
1138 def toggle_checkboxes_link(selector)
1139 link_to_function '',
1139 link_to_function '',
1140 "toggleCheckboxesBySelector('#{selector}')",
1140 "toggleCheckboxesBySelector('#{selector}')",
1141 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1141 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1142 :class => 'toggle-checkboxes'
1142 :class => 'toggle-checkboxes'
1143 end
1143 end
1144
1144
1145 def progress_bar(pcts, options={})
1145 def progress_bar(pcts, options={})
1146 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1146 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1147 pcts = pcts.collect(&:round)
1147 pcts = pcts.collect(&:round)
1148 pcts[1] = pcts[1] - pcts[0]
1148 pcts[1] = pcts[1] - pcts[0]
1149 pcts << (100 - pcts[1] - pcts[0])
1149 pcts << (100 - pcts[1] - pcts[0])
1150 titles = options[:titles].to_a
1150 titles = options[:titles].to_a
1151 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1151 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1152 legend = options[:legend] || ''
1152 legend = options[:legend] || ''
1153 content_tag('table',
1153 content_tag('table',
1154 content_tag('tr',
1154 content_tag('tr',
1155 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1155 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1156 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1156 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1157 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1157 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1158 ), :class => "progress progress-#{pcts[0]}").html_safe +
1158 ), :class => "progress progress-#{pcts[0]}").html_safe +
1159 content_tag('p', legend, :class => 'percent').html_safe
1159 content_tag('p', legend, :class => 'percent').html_safe
1160 end
1160 end
1161
1161
1162 def checked_image(checked=true)
1162 def checked_image(checked=true)
1163 if checked
1163 if checked
1164 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1164 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1165 end
1165 end
1166 end
1166 end
1167
1167
1168 def context_menu(url)
1168 def context_menu
1169 unless @context_menu_included
1169 unless @context_menu_included
1170 content_for :header_tags do
1170 content_for :header_tags do
1171 javascript_include_tag('context_menu') +
1171 javascript_include_tag('context_menu') +
1172 stylesheet_link_tag('context_menu')
1172 stylesheet_link_tag('context_menu')
1173 end
1173 end
1174 if l(:direction) == 'rtl'
1174 if l(:direction) == 'rtl'
1175 content_for :header_tags do
1175 content_for :header_tags do
1176 stylesheet_link_tag('context_menu_rtl')
1176 stylesheet_link_tag('context_menu_rtl')
1177 end
1177 end
1178 end
1178 end
1179 @context_menu_included = true
1179 @context_menu_included = true
1180 end
1180 end
1181 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1181 nil
1182 end
1182 end
1183
1183
1184 def calendar_for(field_id)
1184 def calendar_for(field_id)
1185 include_calendar_headers_tags
1185 include_calendar_headers_tags
1186 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1186 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1187 end
1187 end
1188
1188
1189 def include_calendar_headers_tags
1189 def include_calendar_headers_tags
1190 unless @calendar_headers_tags_included
1190 unless @calendar_headers_tags_included
1191 tags = ''.html_safe
1191 tags = ''.html_safe
1192 @calendar_headers_tags_included = true
1192 @calendar_headers_tags_included = true
1193 content_for :header_tags do
1193 content_for :header_tags do
1194 start_of_week = Setting.start_of_week
1194 start_of_week = Setting.start_of_week
1195 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1195 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1196 # Redmine uses 1..7 (monday..sunday) in settings and locales
1196 # Redmine uses 1..7 (monday..sunday) in settings and locales
1197 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1197 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1198 start_of_week = start_of_week.to_i % 7
1198 start_of_week = start_of_week.to_i % 7
1199 tags << javascript_tag(
1199 tags << javascript_tag(
1200 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1200 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1201 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1201 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1202 path_to_image('/images/calendar.png') +
1202 path_to_image('/images/calendar.png') +
1203 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1203 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1204 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1204 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1205 "beforeShow: beforeShowDatePicker};")
1205 "beforeShow: beforeShowDatePicker};")
1206 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1206 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1207 unless jquery_locale == 'en'
1207 unless jquery_locale == 'en'
1208 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1208 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1209 end
1209 end
1210 tags
1210 tags
1211 end
1211 end
1212 end
1212 end
1213 end
1213 end
1214
1214
1215 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1215 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1216 # Examples:
1216 # Examples:
1217 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1217 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1218 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1218 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1219 #
1219 #
1220 def stylesheet_link_tag(*sources)
1220 def stylesheet_link_tag(*sources)
1221 options = sources.last.is_a?(Hash) ? sources.pop : {}
1221 options = sources.last.is_a?(Hash) ? sources.pop : {}
1222 plugin = options.delete(:plugin)
1222 plugin = options.delete(:plugin)
1223 sources = sources.map do |source|
1223 sources = sources.map do |source|
1224 if plugin
1224 if plugin
1225 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1225 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1226 elsif current_theme && current_theme.stylesheets.include?(source)
1226 elsif current_theme && current_theme.stylesheets.include?(source)
1227 current_theme.stylesheet_path(source)
1227 current_theme.stylesheet_path(source)
1228 else
1228 else
1229 source
1229 source
1230 end
1230 end
1231 end
1231 end
1232 super *sources, options
1232 super *sources, options
1233 end
1233 end
1234
1234
1235 # Overrides Rails' image_tag with themes and plugins support.
1235 # Overrides Rails' image_tag with themes and plugins support.
1236 # Examples:
1236 # Examples:
1237 # image_tag('image.png') # => picks image.png from the current theme or defaults
1237 # image_tag('image.png') # => picks image.png from the current theme or defaults
1238 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1238 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1239 #
1239 #
1240 def image_tag(source, options={})
1240 def image_tag(source, options={})
1241 if plugin = options.delete(:plugin)
1241 if plugin = options.delete(:plugin)
1242 source = "/plugin_assets/#{plugin}/images/#{source}"
1242 source = "/plugin_assets/#{plugin}/images/#{source}"
1243 elsif current_theme && current_theme.images.include?(source)
1243 elsif current_theme && current_theme.images.include?(source)
1244 source = current_theme.image_path(source)
1244 source = current_theme.image_path(source)
1245 end
1245 end
1246 super source, options
1246 super source, options
1247 end
1247 end
1248
1248
1249 # Overrides Rails' javascript_include_tag with plugins support
1249 # Overrides Rails' javascript_include_tag with plugins support
1250 # Examples:
1250 # Examples:
1251 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1251 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1252 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1252 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1253 #
1253 #
1254 def javascript_include_tag(*sources)
1254 def javascript_include_tag(*sources)
1255 options = sources.last.is_a?(Hash) ? sources.pop : {}
1255 options = sources.last.is_a?(Hash) ? sources.pop : {}
1256 if plugin = options.delete(:plugin)
1256 if plugin = options.delete(:plugin)
1257 sources = sources.map do |source|
1257 sources = sources.map do |source|
1258 if plugin
1258 if plugin
1259 "/plugin_assets/#{plugin}/javascripts/#{source}"
1259 "/plugin_assets/#{plugin}/javascripts/#{source}"
1260 else
1260 else
1261 source
1261 source
1262 end
1262 end
1263 end
1263 end
1264 end
1264 end
1265 super *sources, options
1265 super *sources, options
1266 end
1266 end
1267
1267
1268 def sidebar_content?
1268 def sidebar_content?
1269 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1269 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1270 end
1270 end
1271
1271
1272 def view_layouts_base_sidebar_hook_response
1272 def view_layouts_base_sidebar_hook_response
1273 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1273 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1274 end
1274 end
1275
1275
1276 def email_delivery_enabled?
1276 def email_delivery_enabled?
1277 !!ActionMailer::Base.perform_deliveries
1277 !!ActionMailer::Base.perform_deliveries
1278 end
1278 end
1279
1279
1280 # Returns the avatar image tag for the given +user+ if avatars are enabled
1280 # Returns the avatar image tag for the given +user+ if avatars are enabled
1281 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1281 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1282 def avatar(user, options = { })
1282 def avatar(user, options = { })
1283 if Setting.gravatar_enabled?
1283 if Setting.gravatar_enabled?
1284 options.merge!(:default => Setting.gravatar_default)
1284 options.merge!(:default => Setting.gravatar_default)
1285 email = nil
1285 email = nil
1286 if user.respond_to?(:mail)
1286 if user.respond_to?(:mail)
1287 email = user.mail
1287 email = user.mail
1288 elsif user.to_s =~ %r{<(.+?)>}
1288 elsif user.to_s =~ %r{<(.+?)>}
1289 email = $1
1289 email = $1
1290 end
1290 end
1291 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1291 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1292 else
1292 else
1293 ''
1293 ''
1294 end
1294 end
1295 end
1295 end
1296
1296
1297 # Returns a link to edit user's avatar if avatars are enabled
1297 # Returns a link to edit user's avatar if avatars are enabled
1298 def avatar_edit_link(user, options={})
1298 def avatar_edit_link(user, options={})
1299 if Setting.gravatar_enabled?
1299 if Setting.gravatar_enabled?
1300 url = "https://gravatar.com"
1300 url = "https://gravatar.com"
1301 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1301 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1302 end
1302 end
1303 end
1303 end
1304
1304
1305 def sanitize_anchor_name(anchor)
1305 def sanitize_anchor_name(anchor)
1306 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1306 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1307 end
1307 end
1308
1308
1309 # Returns the javascript tags that are included in the html layout head
1309 # Returns the javascript tags that are included in the html layout head
1310 def javascript_heads
1310 def javascript_heads
1311 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1311 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1312 unless User.current.pref.warn_on_leaving_unsaved == '0'
1312 unless User.current.pref.warn_on_leaving_unsaved == '0'
1313 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1313 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1314 end
1314 end
1315 tags
1315 tags
1316 end
1316 end
1317
1317
1318 def favicon
1318 def favicon
1319 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1319 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1320 end
1320 end
1321
1321
1322 # Returns the path to the favicon
1322 # Returns the path to the favicon
1323 def favicon_path
1323 def favicon_path
1324 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1324 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1325 image_path(icon)
1325 image_path(icon)
1326 end
1326 end
1327
1327
1328 # Returns the full URL to the favicon
1328 # Returns the full URL to the favicon
1329 def favicon_url
1329 def favicon_url
1330 # TODO: use #image_url introduced in Rails4
1330 # TODO: use #image_url introduced in Rails4
1331 path = favicon_path
1331 path = favicon_path
1332 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1332 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1333 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1333 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1334 end
1334 end
1335
1335
1336 def robot_exclusion_tag
1336 def robot_exclusion_tag
1337 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1337 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1338 end
1338 end
1339
1339
1340 # Returns true if arg is expected in the API response
1340 # Returns true if arg is expected in the API response
1341 def include_in_api_response?(arg)
1341 def include_in_api_response?(arg)
1342 unless @included_in_api_response
1342 unless @included_in_api_response
1343 param = params[:include]
1343 param = params[:include]
1344 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1344 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1345 @included_in_api_response.collect!(&:strip)
1345 @included_in_api_response.collect!(&:strip)
1346 end
1346 end
1347 @included_in_api_response.include?(arg.to_s)
1347 @included_in_api_response.include?(arg.to_s)
1348 end
1348 end
1349
1349
1350 # Returns options or nil if nometa param or X-Redmine-Nometa header
1350 # Returns options or nil if nometa param or X-Redmine-Nometa header
1351 # was set in the request
1351 # was set in the request
1352 def api_meta(options)
1352 def api_meta(options)
1353 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1353 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1354 # compatibility mode for activeresource clients that raise
1354 # compatibility mode for activeresource clients that raise
1355 # an error when deserializing an array with attributes
1355 # an error when deserializing an array with attributes
1356 nil
1356 nil
1357 else
1357 else
1358 options
1358 options
1359 end
1359 end
1360 end
1360 end
1361
1361
1362 def generate_csv(&block)
1362 def generate_csv(&block)
1363 decimal_separator = l(:general_csv_decimal_separator)
1363 decimal_separator = l(:general_csv_decimal_separator)
1364 encoding = l(:general_csv_encoding)
1364 encoding = l(:general_csv_encoding)
1365 end
1365 end
1366
1366
1367 private
1367 private
1368
1368
1369 def wiki_helper
1369 def wiki_helper
1370 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1370 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1371 extend helper
1371 extend helper
1372 return self
1372 return self
1373 end
1373 end
1374 end
1374 end
@@ -1,484 +1,484
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
23
23
24 def issue_list(issues, &block)
24 def issue_list(issues, &block)
25 ancestors = []
25 ancestors = []
26 issues.each do |issue|
26 issues.each do |issue|
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
28 ancestors.pop
28 ancestors.pop
29 end
29 end
30 yield issue, ancestors.size
30 yield issue, ancestors.size
31 ancestors << issue unless issue.leaf?
31 ancestors << issue unless issue.leaf?
32 end
32 end
33 end
33 end
34
34
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
36 ancestors = []
36 ancestors = []
37 grouped_query_results(issues, query, issue_count_by_group) do |issue, group_name, group_count, group_totals|
37 grouped_query_results(issues, query, issue_count_by_group) do |issue, group_name, group_count, group_totals|
38 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
38 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
39 ancestors.pop
39 ancestors.pop
40 end
40 end
41 yield issue, ancestors.size, group_name, group_count, group_totals
41 yield issue, ancestors.size, group_name, group_count, group_totals
42 ancestors << issue unless issue.leaf?
42 ancestors << issue unless issue.leaf?
43 end
43 end
44 end
44 end
45
45
46 # Renders a HTML/CSS tooltip
46 # Renders a HTML/CSS tooltip
47 #
47 #
48 # To use, a trigger div is needed. This is a div with the class of "tooltip"
48 # To use, a trigger div is needed. This is a div with the class of "tooltip"
49 # that contains this method wrapped in a span with the class of "tip"
49 # that contains this method wrapped in a span with the class of "tip"
50 #
50 #
51 # <div class="tooltip"><%= link_to_issue(issue) %>
51 # <div class="tooltip"><%= link_to_issue(issue) %>
52 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
52 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
53 # </div>
53 # </div>
54 #
54 #
55 def render_issue_tooltip(issue)
55 def render_issue_tooltip(issue)
56 @cached_label_status ||= l(:field_status)
56 @cached_label_status ||= l(:field_status)
57 @cached_label_start_date ||= l(:field_start_date)
57 @cached_label_start_date ||= l(:field_start_date)
58 @cached_label_due_date ||= l(:field_due_date)
58 @cached_label_due_date ||= l(:field_due_date)
59 @cached_label_assigned_to ||= l(:field_assigned_to)
59 @cached_label_assigned_to ||= l(:field_assigned_to)
60 @cached_label_priority ||= l(:field_priority)
60 @cached_label_priority ||= l(:field_priority)
61 @cached_label_project ||= l(:field_project)
61 @cached_label_project ||= l(:field_project)
62
62
63 link_to_issue(issue) + "<br /><br />".html_safe +
63 link_to_issue(issue) + "<br /><br />".html_safe +
64 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
64 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
65 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
65 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
66 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
66 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
67 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
67 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
68 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
68 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
69 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
69 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
70 end
70 end
71
71
72 def issue_heading(issue)
72 def issue_heading(issue)
73 h("#{issue.tracker} ##{issue.id}")
73 h("#{issue.tracker} ##{issue.id}")
74 end
74 end
75
75
76 def render_issue_subject_with_tree(issue)
76 def render_issue_subject_with_tree(issue)
77 s = ''
77 s = ''
78 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
78 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
79 ancestors.each do |ancestor|
79 ancestors.each do |ancestor|
80 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
80 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
81 end
81 end
82 s << '<div>'
82 s << '<div>'
83 subject = h(issue.subject)
83 subject = h(issue.subject)
84 if issue.is_private?
84 if issue.is_private?
85 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
85 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
86 end
86 end
87 s << content_tag('h3', subject)
87 s << content_tag('h3', subject)
88 s << '</div>' * (ancestors.size + 1)
88 s << '</div>' * (ancestors.size + 1)
89 s.html_safe
89 s.html_safe
90 end
90 end
91
91
92 def render_descendants_tree(issue)
92 def render_descendants_tree(issue)
93 s = '<form><table class="list issues">'
93 s = '<table class="list issues">'
94 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
94 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
95 css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}"
95 css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}"
96 css << " idnt idnt-#{level}" if level > 0
96 css << " idnt idnt-#{level}" if level > 0
97 s << content_tag('tr',
97 s << content_tag('tr',
98 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
98 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
99 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
99 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
100 content_tag('td', h(child.status), :class => 'status') +
100 content_tag('td', h(child.status), :class => 'status') +
101 content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
101 content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
102 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
102 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
103 :class => css)
103 :class => css)
104 end
104 end
105 s << '</table></form>'
105 s << '</table>'
106 s.html_safe
106 s.html_safe
107 end
107 end
108
108
109 def issue_estimated_hours_details(issue)
109 def issue_estimated_hours_details(issue)
110 if issue.total_estimated_hours.present?
110 if issue.total_estimated_hours.present?
111 if issue.total_estimated_hours == issue.estimated_hours
111 if issue.total_estimated_hours == issue.estimated_hours
112 l_hours_short(issue.estimated_hours)
112 l_hours_short(issue.estimated_hours)
113 else
113 else
114 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
114 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
115 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
115 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
116 s.html_safe
116 s.html_safe
117 end
117 end
118 end
118 end
119 end
119 end
120
120
121 def issue_spent_hours_details(issue)
121 def issue_spent_hours_details(issue)
122 if issue.total_spent_hours > 0
122 if issue.total_spent_hours > 0
123 path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
123 path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
124
124
125 if issue.total_spent_hours == issue.spent_hours
125 if issue.total_spent_hours == issue.spent_hours
126 link_to(l_hours_short(issue.spent_hours), path)
126 link_to(l_hours_short(issue.spent_hours), path)
127 else
127 else
128 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
128 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
129 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
129 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
130 s.html_safe
130 s.html_safe
131 end
131 end
132 end
132 end
133 end
133 end
134
134
135 # Returns an array of error messages for bulk edited issues
135 # Returns an array of error messages for bulk edited issues
136 def bulk_edit_error_messages(issues)
136 def bulk_edit_error_messages(issues)
137 messages = {}
137 messages = {}
138 issues.each do |issue|
138 issues.each do |issue|
139 issue.errors.full_messages.each do |message|
139 issue.errors.full_messages.each do |message|
140 messages[message] ||= []
140 messages[message] ||= []
141 messages[message] << issue
141 messages[message] << issue
142 end
142 end
143 end
143 end
144 messages.map { |message, issues|
144 messages.map { |message, issues|
145 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
145 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
146 }
146 }
147 end
147 end
148
148
149 # Returns a link for adding a new subtask to the given issue
149 # Returns a link for adding a new subtask to the given issue
150 def link_to_new_subtask(issue)
150 def link_to_new_subtask(issue)
151 attrs = {
151 attrs = {
152 :parent_issue_id => issue
152 :parent_issue_id => issue
153 }
153 }
154 attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
154 attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
155 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
155 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
156 end
156 end
157
157
158 def trackers_options_for_select(issue)
158 def trackers_options_for_select(issue)
159 trackers = issue.allowed_target_trackers
159 trackers = issue.allowed_target_trackers
160 if issue.new_record? && issue.parent_issue_id.present?
160 if issue.new_record? && issue.parent_issue_id.present?
161 trackers = trackers.reject do |tracker|
161 trackers = trackers.reject do |tracker|
162 issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
162 issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
163 end
163 end
164 end
164 end
165 trackers.collect {|t| [t.name, t.id]}
165 trackers.collect {|t| [t.name, t.id]}
166 end
166 end
167
167
168 class IssueFieldsRows
168 class IssueFieldsRows
169 include ActionView::Helpers::TagHelper
169 include ActionView::Helpers::TagHelper
170
170
171 def initialize
171 def initialize
172 @left = []
172 @left = []
173 @right = []
173 @right = []
174 end
174 end
175
175
176 def left(*args)
176 def left(*args)
177 args.any? ? @left << cells(*args) : @left
177 args.any? ? @left << cells(*args) : @left
178 end
178 end
179
179
180 def right(*args)
180 def right(*args)
181 args.any? ? @right << cells(*args) : @right
181 args.any? ? @right << cells(*args) : @right
182 end
182 end
183
183
184 def size
184 def size
185 @left.size > @right.size ? @left.size : @right.size
185 @left.size > @right.size ? @left.size : @right.size
186 end
186 end
187
187
188 def to_html
188 def to_html
189 content =
189 content =
190 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
190 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
191 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
191 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
192
192
193 content_tag('div', content, :class => 'splitcontent')
193 content_tag('div', content, :class => 'splitcontent')
194 end
194 end
195
195
196 def cells(label, text, options={})
196 def cells(label, text, options={})
197 options[:class] = [options[:class] || "", 'attribute'].join(' ')
197 options[:class] = [options[:class] || "", 'attribute'].join(' ')
198 content_tag 'div',
198 content_tag 'div',
199 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
199 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
200 options
200 options
201 end
201 end
202 end
202 end
203
203
204 def issue_fields_rows
204 def issue_fields_rows
205 r = IssueFieldsRows.new
205 r = IssueFieldsRows.new
206 yield r
206 yield r
207 r.to_html
207 r.to_html
208 end
208 end
209
209
210 def render_custom_fields_rows(issue)
210 def render_custom_fields_rows(issue)
211 values = issue.visible_custom_field_values
211 values = issue.visible_custom_field_values
212 return if values.empty?
212 return if values.empty?
213 half = (values.size / 2.0).ceil
213 half = (values.size / 2.0).ceil
214 issue_fields_rows do |rows|
214 issue_fields_rows do |rows|
215 values.each_with_index do |value, i|
215 values.each_with_index do |value, i|
216 css = "cf_#{value.custom_field.id}"
216 css = "cf_#{value.custom_field.id}"
217 m = (i < half ? :left : :right)
217 m = (i < half ? :left : :right)
218 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
218 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
219 end
219 end
220 end
220 end
221 end
221 end
222
222
223 # Returns the path for updating the issue form
223 # Returns the path for updating the issue form
224 # with project as the current project
224 # with project as the current project
225 def update_issue_form_path(project, issue)
225 def update_issue_form_path(project, issue)
226 options = {:format => 'js'}
226 options = {:format => 'js'}
227 if issue.new_record?
227 if issue.new_record?
228 if project
228 if project
229 new_project_issue_path(project, options)
229 new_project_issue_path(project, options)
230 else
230 else
231 new_issue_path(options)
231 new_issue_path(options)
232 end
232 end
233 else
233 else
234 edit_issue_path(issue, options)
234 edit_issue_path(issue, options)
235 end
235 end
236 end
236 end
237
237
238 # Returns the number of descendants for an array of issues
238 # Returns the number of descendants for an array of issues
239 def issues_descendant_count(issues)
239 def issues_descendant_count(issues)
240 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
240 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
241 ids -= issues.map(&:id)
241 ids -= issues.map(&:id)
242 ids.size
242 ids.size
243 end
243 end
244
244
245 def issues_destroy_confirmation_message(issues)
245 def issues_destroy_confirmation_message(issues)
246 issues = [issues] unless issues.is_a?(Array)
246 issues = [issues] unless issues.is_a?(Array)
247 message = l(:text_issues_destroy_confirmation)
247 message = l(:text_issues_destroy_confirmation)
248
248
249 descendant_count = issues_descendant_count(issues)
249 descendant_count = issues_descendant_count(issues)
250 if descendant_count > 0
250 if descendant_count > 0
251 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
251 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
252 end
252 end
253 message
253 message
254 end
254 end
255
255
256 # Returns an array of users that are proposed as watchers
256 # Returns an array of users that are proposed as watchers
257 # on the new issue form
257 # on the new issue form
258 def users_for_new_issue_watchers(issue)
258 def users_for_new_issue_watchers(issue)
259 users = issue.watcher_users
259 users = issue.watcher_users
260 if issue.project.users.count <= 20
260 if issue.project.users.count <= 20
261 users = (users + issue.project.users.sort).uniq
261 users = (users + issue.project.users.sort).uniq
262 end
262 end
263 users
263 users
264 end
264 end
265
265
266 def email_issue_attributes(issue, user)
266 def email_issue_attributes(issue, user)
267 items = []
267 items = []
268 %w(author status priority assigned_to category fixed_version).each do |attribute|
268 %w(author status priority assigned_to category fixed_version).each do |attribute|
269 unless issue.disabled_core_fields.include?(attribute+"_id")
269 unless issue.disabled_core_fields.include?(attribute+"_id")
270 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
270 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
271 end
271 end
272 end
272 end
273 issue.visible_custom_field_values(user).each do |value|
273 issue.visible_custom_field_values(user).each do |value|
274 items << "#{value.custom_field.name}: #{show_value(value, false)}"
274 items << "#{value.custom_field.name}: #{show_value(value, false)}"
275 end
275 end
276 items
276 items
277 end
277 end
278
278
279 def render_email_issue_attributes(issue, user, html=false)
279 def render_email_issue_attributes(issue, user, html=false)
280 items = email_issue_attributes(issue, user)
280 items = email_issue_attributes(issue, user)
281 if html
281 if html
282 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
282 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
283 else
283 else
284 items.map{|s| "* #{s}"}.join("\n")
284 items.map{|s| "* #{s}"}.join("\n")
285 end
285 end
286 end
286 end
287
287
288 # Returns the textual representation of a journal details
288 # Returns the textual representation of a journal details
289 # as an array of strings
289 # as an array of strings
290 def details_to_strings(details, no_html=false, options={})
290 def details_to_strings(details, no_html=false, options={})
291 options[:only_path] = (options[:only_path] == false ? false : true)
291 options[:only_path] = (options[:only_path] == false ? false : true)
292 strings = []
292 strings = []
293 values_by_field = {}
293 values_by_field = {}
294 details.each do |detail|
294 details.each do |detail|
295 if detail.property == 'cf'
295 if detail.property == 'cf'
296 field = detail.custom_field
296 field = detail.custom_field
297 if field && field.multiple?
297 if field && field.multiple?
298 values_by_field[field] ||= {:added => [], :deleted => []}
298 values_by_field[field] ||= {:added => [], :deleted => []}
299 if detail.old_value
299 if detail.old_value
300 values_by_field[field][:deleted] << detail.old_value
300 values_by_field[field][:deleted] << detail.old_value
301 end
301 end
302 if detail.value
302 if detail.value
303 values_by_field[field][:added] << detail.value
303 values_by_field[field][:added] << detail.value
304 end
304 end
305 next
305 next
306 end
306 end
307 end
307 end
308 strings << show_detail(detail, no_html, options)
308 strings << show_detail(detail, no_html, options)
309 end
309 end
310 if values_by_field.present?
310 if values_by_field.present?
311 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
311 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
312 values_by_field.each do |field, changes|
312 values_by_field.each do |field, changes|
313 if changes[:added].any?
313 if changes[:added].any?
314 detail = multiple_values_detail.new('cf', field.id.to_s, field)
314 detail = multiple_values_detail.new('cf', field.id.to_s, field)
315 detail.value = changes[:added]
315 detail.value = changes[:added]
316 strings << show_detail(detail, no_html, options)
316 strings << show_detail(detail, no_html, options)
317 end
317 end
318 if changes[:deleted].any?
318 if changes[:deleted].any?
319 detail = multiple_values_detail.new('cf', field.id.to_s, field)
319 detail = multiple_values_detail.new('cf', field.id.to_s, field)
320 detail.old_value = changes[:deleted]
320 detail.old_value = changes[:deleted]
321 strings << show_detail(detail, no_html, options)
321 strings << show_detail(detail, no_html, options)
322 end
322 end
323 end
323 end
324 end
324 end
325 strings
325 strings
326 end
326 end
327
327
328 # Returns the textual representation of a single journal detail
328 # Returns the textual representation of a single journal detail
329 def show_detail(detail, no_html=false, options={})
329 def show_detail(detail, no_html=false, options={})
330 multiple = false
330 multiple = false
331 show_diff = false
331 show_diff = false
332 no_details = false
332 no_details = false
333
333
334 case detail.property
334 case detail.property
335 when 'attr'
335 when 'attr'
336 field = detail.prop_key.to_s.gsub(/\_id$/, "")
336 field = detail.prop_key.to_s.gsub(/\_id$/, "")
337 label = l(("field_" + field).to_sym)
337 label = l(("field_" + field).to_sym)
338 case detail.prop_key
338 case detail.prop_key
339 when 'due_date', 'start_date'
339 when 'due_date', 'start_date'
340 value = format_date(detail.value.to_date) if detail.value
340 value = format_date(detail.value.to_date) if detail.value
341 old_value = format_date(detail.old_value.to_date) if detail.old_value
341 old_value = format_date(detail.old_value.to_date) if detail.old_value
342
342
343 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
343 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
344 'priority_id', 'category_id', 'fixed_version_id'
344 'priority_id', 'category_id', 'fixed_version_id'
345 value = find_name_by_reflection(field, detail.value)
345 value = find_name_by_reflection(field, detail.value)
346 old_value = find_name_by_reflection(field, detail.old_value)
346 old_value = find_name_by_reflection(field, detail.old_value)
347
347
348 when 'estimated_hours'
348 when 'estimated_hours'
349 value = l_hours_short(detail.value.to_f) unless detail.value.blank?
349 value = l_hours_short(detail.value.to_f) unless detail.value.blank?
350 old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
350 old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
351
351
352 when 'parent_id'
352 when 'parent_id'
353 label = l(:field_parent_issue)
353 label = l(:field_parent_issue)
354 value = "##{detail.value}" unless detail.value.blank?
354 value = "##{detail.value}" unless detail.value.blank?
355 old_value = "##{detail.old_value}" unless detail.old_value.blank?
355 old_value = "##{detail.old_value}" unless detail.old_value.blank?
356
356
357 when 'is_private'
357 when 'is_private'
358 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
358 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
359 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
359 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
360
360
361 when 'description'
361 when 'description'
362 show_diff = true
362 show_diff = true
363 end
363 end
364 when 'cf'
364 when 'cf'
365 custom_field = detail.custom_field
365 custom_field = detail.custom_field
366 if custom_field
366 if custom_field
367 label = custom_field.name
367 label = custom_field.name
368 if custom_field.format.class.change_no_details
368 if custom_field.format.class.change_no_details
369 no_details = true
369 no_details = true
370 elsif custom_field.format.class.change_as_diff
370 elsif custom_field.format.class.change_as_diff
371 show_diff = true
371 show_diff = true
372 else
372 else
373 multiple = custom_field.multiple?
373 multiple = custom_field.multiple?
374 value = format_value(detail.value, custom_field) if detail.value
374 value = format_value(detail.value, custom_field) if detail.value
375 old_value = format_value(detail.old_value, custom_field) if detail.old_value
375 old_value = format_value(detail.old_value, custom_field) if detail.old_value
376 end
376 end
377 end
377 end
378 when 'attachment'
378 when 'attachment'
379 label = l(:label_attachment)
379 label = l(:label_attachment)
380 when 'relation'
380 when 'relation'
381 if detail.value && !detail.old_value
381 if detail.value && !detail.old_value
382 rel_issue = Issue.visible.find_by_id(detail.value)
382 rel_issue = Issue.visible.find_by_id(detail.value)
383 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
383 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
384 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
384 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
385 elsif detail.old_value && !detail.value
385 elsif detail.old_value && !detail.value
386 rel_issue = Issue.visible.find_by_id(detail.old_value)
386 rel_issue = Issue.visible.find_by_id(detail.old_value)
387 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
387 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
388 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
388 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
389 end
389 end
390 relation_type = IssueRelation::TYPES[detail.prop_key]
390 relation_type = IssueRelation::TYPES[detail.prop_key]
391 label = l(relation_type[:name]) if relation_type
391 label = l(relation_type[:name]) if relation_type
392 end
392 end
393 call_hook(:helper_issues_show_detail_after_setting,
393 call_hook(:helper_issues_show_detail_after_setting,
394 {:detail => detail, :label => label, :value => value, :old_value => old_value })
394 {:detail => detail, :label => label, :value => value, :old_value => old_value })
395
395
396 label ||= detail.prop_key
396 label ||= detail.prop_key
397 value ||= detail.value
397 value ||= detail.value
398 old_value ||= detail.old_value
398 old_value ||= detail.old_value
399
399
400 unless no_html
400 unless no_html
401 label = content_tag('strong', label)
401 label = content_tag('strong', label)
402 old_value = content_tag("i", h(old_value)) if detail.old_value
402 old_value = content_tag("i", h(old_value)) if detail.old_value
403 if detail.old_value && detail.value.blank? && detail.property != 'relation'
403 if detail.old_value && detail.value.blank? && detail.property != 'relation'
404 old_value = content_tag("del", old_value)
404 old_value = content_tag("del", old_value)
405 end
405 end
406 if detail.property == 'attachment' && value.present? &&
406 if detail.property == 'attachment' && value.present? &&
407 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
407 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
408 # Link to the attachment if it has not been removed
408 # Link to the attachment if it has not been removed
409 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
409 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
410 if options[:only_path] != false && (atta.is_text? || atta.is_image?)
410 if options[:only_path] != false && (atta.is_text? || atta.is_image?)
411 value += ' '
411 value += ' '
412 value += link_to(l(:button_view),
412 value += link_to(l(:button_view),
413 { :controller => 'attachments', :action => 'show',
413 { :controller => 'attachments', :action => 'show',
414 :id => atta, :filename => atta.filename },
414 :id => atta, :filename => atta.filename },
415 :class => 'icon-only icon-magnifier',
415 :class => 'icon-only icon-magnifier',
416 :title => l(:button_view))
416 :title => l(:button_view))
417 end
417 end
418 else
418 else
419 value = content_tag("i", h(value)) if value
419 value = content_tag("i", h(value)) if value
420 end
420 end
421 end
421 end
422
422
423 if no_details
423 if no_details
424 s = l(:text_journal_changed_no_detail, :label => label).html_safe
424 s = l(:text_journal_changed_no_detail, :label => label).html_safe
425 elsif show_diff
425 elsif show_diff
426 s = l(:text_journal_changed_no_detail, :label => label)
426 s = l(:text_journal_changed_no_detail, :label => label)
427 unless no_html
427 unless no_html
428 diff_link = link_to 'diff',
428 diff_link = link_to 'diff',
429 diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
429 diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
430 :title => l(:label_view_diff)
430 :title => l(:label_view_diff)
431 s << " (#{ diff_link })"
431 s << " (#{ diff_link })"
432 end
432 end
433 s.html_safe
433 s.html_safe
434 elsif detail.value.present?
434 elsif detail.value.present?
435 case detail.property
435 case detail.property
436 when 'attr', 'cf'
436 when 'attr', 'cf'
437 if detail.old_value.present?
437 if detail.old_value.present?
438 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
438 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
439 elsif multiple
439 elsif multiple
440 l(:text_journal_added, :label => label, :value => value).html_safe
440 l(:text_journal_added, :label => label, :value => value).html_safe
441 else
441 else
442 l(:text_journal_set_to, :label => label, :value => value).html_safe
442 l(:text_journal_set_to, :label => label, :value => value).html_safe
443 end
443 end
444 when 'attachment', 'relation'
444 when 'attachment', 'relation'
445 l(:text_journal_added, :label => label, :value => value).html_safe
445 l(:text_journal_added, :label => label, :value => value).html_safe
446 end
446 end
447 else
447 else
448 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
448 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
449 end
449 end
450 end
450 end
451
451
452 # Find the name of an associated record stored in the field attribute
452 # Find the name of an associated record stored in the field attribute
453 def find_name_by_reflection(field, id)
453 def find_name_by_reflection(field, id)
454 unless id.present?
454 unless id.present?
455 return nil
455 return nil
456 end
456 end
457 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
457 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
458 association = Issue.reflect_on_association(key.first.to_sym)
458 association = Issue.reflect_on_association(key.first.to_sym)
459 name = nil
459 name = nil
460 if association
460 if association
461 record = association.klass.find_by_id(key.last)
461 record = association.klass.find_by_id(key.last)
462 if record
462 if record
463 name = record.name.force_encoding('UTF-8')
463 name = record.name.force_encoding('UTF-8')
464 end
464 end
465 end
465 end
466 hash[key] = name
466 hash[key] = name
467 end
467 end
468 @detail_value_name_by_reflection[[field, id]]
468 @detail_value_name_by_reflection[[field, id]]
469 end
469 end
470
470
471 # Renders issue children recursively
471 # Renders issue children recursively
472 def render_api_issue_children(issue, api)
472 def render_api_issue_children(issue, api)
473 return if issue.leaf?
473 return if issue.leaf?
474 api.array :children do
474 api.array :children do
475 issue.children.each do |child|
475 issue.children.each do |child|
476 api.issue(:id => child.id) do
476 api.issue(:id => child.id) do
477 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
477 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
478 api.subject child.subject
478 api.subject child.subject
479 render_api_issue_children(child, api)
479 render_api_issue_children(child, api)
480 end
480 end
481 end
481 end
482 end
482 end
483 end
483 end
484 end
484 end
@@ -1,44 +1,44
1 <%= form_tag({}) do -%>
1 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
2 <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list issues <%= sort_css_classes %>">
4 <table class="list issues <%= sort_css_classes %>">
5 <thead>
5 <thead>
6 <tr>
6 <tr>
7 <th class="checkbox hide-when-print">
7 <th class="checkbox hide-when-print">
8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
10 </th>
10 </th>
11 <% query.inline_columns.each do |column| %>
11 <% query.inline_columns.each do |column| %>
12 <%= column_header(column) %>
12 <%= column_header(column) %>
13 <% end %>
13 <% end %>
14 </tr>
14 </tr>
15 </thead>
15 </thead>
16 <tbody>
16 <tbody>
17 <% grouped_issue_list(issues, @query, @issue_count_by_group) do |issue, level, group_name, group_count, group_totals| -%>
17 <% grouped_issue_list(issues, @query, @issue_count_by_group) do |issue, level, group_name, group_count, group_totals| -%>
18 <% if group_name %>
18 <% if group_name %>
19 <% reset_cycle %>
19 <% reset_cycle %>
20 <tr class="group open">
20 <tr class="group open">
21 <td colspan="<%= query.inline_columns.size + 1 %>">
21 <td colspan="<%= query.inline_columns.size + 1 %>">
22 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
22 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
23 <span class="name"><%= group_name %></span> <span class="count"><%= group_count %></span> <span class="totals"><%= group_totals %></span>
23 <span class="name"><%= group_name %></span> <span class="count"><%= group_count %></span> <span class="totals"><%= group_totals %></span>
24 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
24 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
25 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
25 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
26 </td>
26 </td>
27 </tr>
27 </tr>
28 <% end %>
28 <% end %>
29 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
30 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
30 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
31 <%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
31 <%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
32 </tr>
32 </tr>
33 <% @query.block_columns.each do |column|
33 <% @query.block_columns.each do |column|
34 if (text = column_content(column, issue)) && text.present? -%>
34 if (text = column_content(column, issue)) && text.present? -%>
35 <tr class="<%= current_cycle %>">
35 <tr class="<%= current_cycle %>">
36 <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td>
36 <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td>
37 </tr>
37 </tr>
38 <% end -%>
38 <% end -%>
39 <% end -%>
39 <% end -%>
40 <% end -%>
40 <% end -%>
41 </tbody>
41 </tbody>
42 </table>
42 </table>
43 </div>
43 </div>
44 <% end -%>
44 <% end -%>
@@ -1,29 +1,29
1 <% if issues && issues.any? %>
1 <% if issues && issues.any? %>
2 <%= form_tag({}) do %>
2 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
3 <table class="list list-simple issues">
3 <table class="list list-simple issues">
4 <thead><tr>
4 <thead><tr>
5 <th class="id">#</th>
5 <th class="id">#</th>
6 <th class="project"><%=l(:field_project)%></th>
6 <th class="project"><%=l(:field_project)%></th>
7 <th class="status"><%=l(:field_status)%></th>
7 <th class="status"><%=l(:field_status)%></th>
8 <th class="subject"><%=l(:field_subject)%></th>
8 <th class="subject"><%=l(:field_subject)%></th>
9 </tr></thead>
9 </tr></thead>
10 <tbody>
10 <tbody>
11 <% for issue in issues %>
11 <% for issue in issues %>
12 <tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
12 <tr id="issue-<%= h(issue.id) %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
13 <td class="id">
13 <td class="id">
14 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %>
14 <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %>
15 <%= link_to("#{issue.tracker} ##{issue.id}", issue_path(issue)) %>
15 <%= link_to("#{issue.tracker} ##{issue.id}", issue_path(issue)) %>
16 </td>
16 </td>
17 <td class="project"><%= link_to_project(issue.project) %></td>
17 <td class="project"><%= link_to_project(issue.project) %></td>
18 <td class="status"><%= issue.status %></td>
18 <td class="status"><%= issue.status %></td>
19 <td class="subject">
19 <td class="subject">
20 <span><%= link_to(issue.subject, issue_path(issue)) %></span>
20 <span><%= link_to(issue.subject, issue_path(issue)) %></span>
21 </td>
21 </td>
22 </tr>
22 </tr>
23 <% end %>
23 <% end %>
24 </tbody>
24 </tbody>
25 </table>
25 </table>
26 <% end %>
26 <% end %>
27 <% else %>
27 <% else %>
28 <p class="nodata"><%= l(:label_no_data) %></p>
28 <p class="nodata"><%= l(:label_no_data) %></p>
29 <% end %>
29 <% end %>
@@ -1,43 +1,43
1 <div class="contextual">
1 <div class="contextual">
2 <% if User.current.allowed_to?(:manage_issue_relations, @project) %>
2 <% if User.current.allowed_to?(:manage_issue_relations, @project) %>
3 <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
3 <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
4 <% end %>
4 <% end %>
5 </div>
5 </div>
6
6
7 <p><strong><%=l(:label_related_issues)%></strong></p>
7 <p><strong><%=l(:label_related_issues)%></strong></p>
8
8
9 <% if @relations.present? %>
9 <% if @relations.present? %>
10 <form>
10 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
11 <table class="list issues">
11 <table class="list issues">
12 <% @relations.each do |relation| %>
12 <% @relations.each do |relation| %>
13 <% other_issue = relation.other_issue(@issue) -%>
13 <% other_issue = relation.other_issue(@issue) -%>
14 <tr class="issue hascontextmenu <%= other_issue.css_classes %>" id="relation-<%= relation.id %>">
14 <tr class="issue hascontextmenu <%= other_issue.css_classes %>" id="relation-<%= relation.id %>">
15 <td class="checkbox"><%= check_box_tag("ids[]", other_issue.id, false, :id => nil) %></td>
15 <td class="checkbox"><%= check_box_tag("ids[]", other_issue.id, false, :id => nil) %></td>
16 <td class="subject" style="width: 50%">
16 <td class="subject" style="width: 50%">
17 <%= relation.to_s(@issue) {|other| link_to_issue(other, :project => Setting.cross_project_issue_relations?)}.html_safe %>
17 <%= relation.to_s(@issue) {|other| link_to_issue(other, :project => Setting.cross_project_issue_relations?)}.html_safe %>
18 </td>
18 </td>
19 <td class="status"><%= other_issue.status.name %></td>
19 <td class="status"><%= other_issue.status.name %></td>
20 <td class="start_date"><%= format_date(other_issue.start_date) %></td>
20 <td class="start_date"><%= format_date(other_issue.start_date) %></td>
21 <td class="due_date"><%= format_date(other_issue.due_date) %></td>
21 <td class="due_date"><%= format_date(other_issue.due_date) %></td>
22 <td class="buttons"><%= link_to(l(:label_relation_delete),
22 <td class="buttons"><%= link_to(l(:label_relation_delete),
23 relation_path(relation),
23 relation_path(relation),
24 :remote => true,
24 :remote => true,
25 :method => :delete,
25 :method => :delete,
26 :data => {:confirm => l(:text_are_you_sure)},
26 :data => {:confirm => l(:text_are_you_sure)},
27 :title => l(:label_relation_delete),
27 :title => l(:label_relation_delete),
28 :class => 'icon-only icon-link-break'
28 :class => 'icon-only icon-link-break'
29 ) if User.current.allowed_to?(:manage_issue_relations, @project) %></td>
29 ) if User.current.allowed_to?(:manage_issue_relations, @project) %></td>
30 </tr>
30 </tr>
31 <% end %>
31 <% end %>
32 </table>
32 </table>
33 </form>
33 <% end %>
34 <% end %>
34 <% end %>
35
35
36 <%= form_for @relation, {
36 <%= form_for @relation, {
37 :as => :relation, :remote => true,
37 :as => :relation, :remote => true,
38 :url => issue_relations_path(@issue),
38 :url => issue_relations_path(@issue),
39 :method => :post,
39 :method => :post,
40 :html => {:id => 'new-relation-form', :style => 'display: none;'}
40 :html => {:id => 'new-relation-form', :style => 'display: none;'}
41 } do |f| %>
41 } do |f| %>
42 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
42 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
43 <% end %>
43 <% end %>
@@ -1,72 +1,72
1 <div class="contextual">
1 <div class="contextual">
2 <% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || Issue.allowed_target_trackers(@project).any?) %>
2 <% if User.current.allowed_to?(:add_issues, @project, :global => true) && (@project.nil? || Issue.allowed_target_trackers(@project).any?) %>
3 <%= link_to l(:label_issue_new), _new_project_issue_path(@project), :class => 'icon icon-add new-issue' %>
3 <%= link_to l(:label_issue_new), _new_project_issue_path(@project), :class => 'icon icon-add new-issue' %>
4 <% end %>
4 <% end %>
5 </div>
5 </div>
6
6
7 <h2><%= @query.new_record? ? l(:label_issue_plural) : @query.name %></h2>
7 <h2><%= @query.new_record? ? l(:label_issue_plural) : @query.name %></h2>
8 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
8 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
9
9
10 <%= form_tag(_project_issues_path(@project), :method => :get, :id => 'query_form') do %>
10 <%= form_tag(_project_issues_path(@project), :method => :get, :id => 'query_form') do %>
11 <%= render :partial => 'queries/query_form' %>
11 <%= render :partial => 'queries/query_form' %>
12 <% end %>
12 <% end %>
13
13
14 <% if @query.valid? %>
14 <% if @query.valid? %>
15 <% if @issues.empty? %>
15 <% if @issues.empty? %>
16 <p class="nodata"><%= l(:label_no_data) %></p>
16 <p class="nodata"><%= l(:label_no_data) %></p>
17 <% else %>
17 <% else %>
18 <%= render_query_totals(@query) %>
18 <%= render_query_totals(@query) %>
19 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
19 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
20 <span class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></span>
20 <span class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></span>
21 <% end %>
21 <% end %>
22
22
23 <% other_formats_links do |f| %>
23 <% other_formats_links do |f| %>
24 <%= f.link_to_with_query_parameters 'Atom', :key => User.current.rss_key %>
24 <%= f.link_to_with_query_parameters 'Atom', :key => User.current.rss_key %>
25 <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
25 <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
26 <%= f.link_to_with_query_parameters 'PDF' %>
26 <%= f.link_to_with_query_parameters 'PDF' %>
27 <% end %>
27 <% end %>
28
28
29 <div id="csv-export-options" style="display:none;">
29 <div id="csv-export-options" style="display:none;">
30 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
30 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
31 <%= form_tag(_project_issues_path(@project, :format => 'csv'), :method => :get, :id => 'csv-export-form') do %>
31 <%= form_tag(_project_issues_path(@project, :format => 'csv'), :method => :get, :id => 'csv-export-form') do %>
32 <%= query_as_hidden_field_tags(@query) %>
32 <%= query_as_hidden_field_tags(@query) %>
33 <%= hidden_field_tag 'sort', @sort_criteria.to_param, :id => nil %>
33 <%= hidden_field_tag 'sort', @sort_criteria.to_param, :id => nil %>
34 <p>
34 <p>
35 <label><%= radio_button_tag 'csv[columns]', '', true %> <%= l(:description_selected_columns) %></label><br />
35 <label><%= radio_button_tag 'csv[columns]', '', true %> <%= l(:description_selected_columns) %></label><br />
36 <label><%= radio_button_tag 'csv[columns]', 'all' %> <%= l(:description_all_columns) %></label>
36 <label><%= radio_button_tag 'csv[columns]', 'all' %> <%= l(:description_all_columns) %></label>
37 </p>
37 </p>
38 <p>
38 <p>
39 <label><%= check_box_tag 'csv[description]', '1', @query.has_column?(:description) %> <%= l(:field_description) %></label>
39 <label><%= check_box_tag 'csv[description]', '1', @query.has_column?(:description) %> <%= l(:field_description) %></label>
40 </p>
40 </p>
41 <% if @issue_count > Setting.issues_export_limit.to_i %>
41 <% if @issue_count > Setting.issues_export_limit.to_i %>
42 <p class="icon icon-warning">
42 <p class="icon icon-warning">
43 <%= l(:setting_issues_export_limit) %>: <%= Setting.issues_export_limit.to_i %>
43 <%= l(:setting_issues_export_limit) %>: <%= Setting.issues_export_limit.to_i %>
44 </p>
44 </p>
45 <% end %>
45 <% end %>
46 <p class="buttons">
46 <p class="buttons">
47 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
47 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
48 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
48 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
49 </p>
49 </p>
50 <% end %>
50 <% end %>
51 </div>
51 </div>
52
52
53 <% end %>
53 <% end %>
54 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
54 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
55
55
56 <% content_for :sidebar do %>
56 <% content_for :sidebar do %>
57 <%= render :partial => 'issues/sidebar' %>
57 <%= render :partial => 'issues/sidebar' %>
58 <% end %>
58 <% end %>
59
59
60 <% content_for :header_tags do %>
60 <% content_for :header_tags do %>
61 <%= auto_discovery_link_tag(:atom,
61 <%= auto_discovery_link_tag(:atom,
62 {:query_id => @query, :format => 'atom',
62 {:query_id => @query, :format => 'atom',
63 :page => nil, :key => User.current.rss_key},
63 :page => nil, :key => User.current.rss_key},
64 :title => l(:label_issue_plural)) %>
64 :title => l(:label_issue_plural)) %>
65 <%= auto_discovery_link_tag(:atom,
65 <%= auto_discovery_link_tag(:atom,
66 {:controller => 'journals', :action => 'index',
66 {:controller => 'journals', :action => 'index',
67 :query_id => @query, :format => 'atom',
67 :query_id => @query, :format => 'atom',
68 :page => nil, :key => User.current.rss_key},
68 :page => nil, :key => User.current.rss_key},
69 :title => l(:label_changes_details)) %>
69 :title => l(:label_changes_details)) %>
70 <% end %>
70 <% end %>
71
71
72 <%= context_menu issues_context_menu_path %>
72 <%= context_menu %>
@@ -1,160 +1,162
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 :accesskey => accesskey(:previous) %> |
13 <% if @issue_position && @issue_count %>
13 <% if @issue_position && @issue_count %>
14 <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> |
15 <% end %>
15 <% end %>
16 <%= link_to_if @next_issue_id,
16 <%= link_to_if @next_issue_id,
17 "#{l(:label_next)} \xc2\xbb",
17 "#{l(:label_next)} \xc2\xbb",
18 (@next_issue_id ? issue_path(@next_issue_id) : nil),
18 (@next_issue_id ? issue_path(@next_issue_id) : nil),
19 :title => "##{@next_issue_id}",
19 :title => "##{@next_issue_id}",
20 :accesskey => accesskey(:next) %>
20 :accesskey => accesskey(:next) %>
21 </div>
21 </div>
22 <% end %>
22 <% end %>
23
23
24 <%= avatar(@issue.author, :size => "50") %>
24 <%= avatar(@issue.author, :size => "50") %>
25
25
26 <div class="subject">
26 <div class="subject">
27 <%= render_issue_subject_with_tree(@issue) %>
27 <%= render_issue_subject_with_tree(@issue) %>
28 </div>
28 </div>
29 <p class="author">
29 <p class="author">
30 <%= authoring @issue.created_on, @issue.author %>.
30 <%= authoring @issue.created_on, @issue.author %>.
31 <% if @issue.created_on != @issue.updated_on %>
31 <% if @issue.created_on != @issue.updated_on %>
32 <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
32 <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
33 <% end %>
33 <% end %>
34 </p>
34 </p>
35
35
36 <div class="attributes">
36 <div class="attributes">
37 <%= issue_fields_rows do |rows|
37 <%= issue_fields_rows do |rows|
38 rows.left l(:field_status), @issue.status.name, :class => 'status'
38 rows.left l(:field_status), @issue.status.name, :class => 'status'
39 rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
39 rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
40
40
41 unless @issue.disabled_core_fields.include?('assigned_to_id')
41 unless @issue.disabled_core_fields.include?('assigned_to_id')
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'
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'
43 end
43 end
44 unless @issue.disabled_core_fields.include?('category_id') || (@issue.category.nil? && @issue.project.issue_categories.none?)
44 unless @issue.disabled_core_fields.include?('category_id') || (@issue.category.nil? && @issue.project.issue_categories.none?)
45 rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), :class => 'category'
45 rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), :class => 'category'
46 end
46 end
47 unless @issue.disabled_core_fields.include?('fixed_version_id') || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
47 unless @issue.disabled_core_fields.include?('fixed_version_id') || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
48 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'
49 end
49 end
50
50
51 unless @issue.disabled_core_fields.include?('start_date')
51 unless @issue.disabled_core_fields.include?('start_date')
52 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'
53 end
53 end
54 unless @issue.disabled_core_fields.include?('due_date')
54 unless @issue.disabled_core_fields.include?('due_date')
55 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'
56 end
56 end
57 unless @issue.disabled_core_fields.include?('done_ratio')
57 unless @issue.disabled_core_fields.include?('done_ratio')
58 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
58 rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
59 end
59 end
60 unless @issue.disabled_core_fields.include?('estimated_hours')
60 unless @issue.disabled_core_fields.include?('estimated_hours')
61 rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
61 rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
62 end
62 end
63 if User.current.allowed_to_view_all_time_entries?(@project)
63 if User.current.allowed_to_view_all_time_entries?(@project)
64 if @issue.total_spent_hours > 0
64 if @issue.total_spent_hours > 0
65 rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
65 rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
66 end
66 end
67 end
67 end
68 end %>
68 end %>
69 <%= render_custom_fields_rows(@issue) %>
69 <%= render_custom_fields_rows(@issue) %>
70 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
70 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
71 </div>
71 </div>
72
72
73 <% if @issue.description? || @issue.attachments.any? -%>
73 <% if @issue.description? || @issue.attachments.any? -%>
74 <hr />
74 <hr />
75 <% if @issue.description? %>
75 <% if @issue.description? %>
76 <div class="description">
76 <div class="description">
77 <div class="contextual">
77 <div class="contextual">
78 <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %>
78 <%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %>
79 </div>
79 </div>
80
80
81 <p><strong><%=l(:field_description)%></strong></p>
81 <p><strong><%=l(:field_description)%></strong></p>
82 <div class="wiki">
82 <div class="wiki">
83 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
83 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
84 </div>
84 </div>
85 </div>
85 </div>
86 <% end %>
86 <% end %>
87 <%= link_to_attachments @issue, :thumbnails => true %>
87 <%= link_to_attachments @issue, :thumbnails => true %>
88 <% end -%>
88 <% end -%>
89
89
90 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
90 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
91
91
92 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
92 <% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
93 <hr />
93 <hr />
94 <div id="issue_tree">
94 <div id="issue_tree">
95 <div class="contextual">
95 <div class="contextual">
96 <%= 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) %>
97 </div>
97 </div>
98 <p><strong><%=l(:label_subtask_plural)%></strong></p>
98 <p><strong><%=l(:label_subtask_plural)%></strong></p>
99 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %>
99 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
100 <%= render_descendants_tree(@issue) unless @issue.leaf? %>
101 <% end %>
100 </div>
102 </div>
101 <% end %>
103 <% end %>
102
104
103 <% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
105 <% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
104 <hr />
106 <hr />
105 <div id="relations">
107 <div id="relations">
106 <%= render :partial => 'relations' %>
108 <%= render :partial => 'relations' %>
107 </div>
109 </div>
108 <% end %>
110 <% end %>
109
111
110 </div>
112 </div>
111
113
112 <% if @changesets.present? %>
114 <% if @changesets.present? %>
113 <div id="issue-changesets">
115 <div id="issue-changesets">
114 <h3><%=l(:label_associated_revisions)%></h3>
116 <h3><%=l(:label_associated_revisions)%></h3>
115 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
117 <%= render :partial => 'changesets', :locals => { :changesets => @changesets} %>
116 </div>
118 </div>
117 <% end %>
119 <% end %>
118
120
119 <% if @journals.present? %>
121 <% if @journals.present? %>
120 <div id="history">
122 <div id="history">
121 <h3><%=l(:label_history)%></h3>
123 <h3><%=l(:label_history)%></h3>
122 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
124 <%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
123 </div>
125 </div>
124 <% end %>
126 <% end %>
125
127
126
128
127 <div style="clear: both;"></div>
129 <div style="clear: both;"></div>
128 <%= render :partial => 'action_menu' %>
130 <%= render :partial => 'action_menu' %>
129
131
130 <div style="clear: both;"></div>
132 <div style="clear: both;"></div>
131 <% if @issue.editable? %>
133 <% if @issue.editable? %>
132 <div id="update" style="display:none;">
134 <div id="update" style="display:none;">
133 <h3><%= l(:button_edit) %></h3>
135 <h3><%= l(:button_edit) %></h3>
134 <%= render :partial => 'edit' %>
136 <%= render :partial => 'edit' %>
135 </div>
137 </div>
136 <% end %>
138 <% end %>
137
139
138 <% other_formats_links do |f| %>
140 <% other_formats_links do |f| %>
139 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
141 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
140 <%= f.link_to 'PDF' %>
142 <%= f.link_to 'PDF' %>
141 <% end %>
143 <% end %>
142
144
143 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
145 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
144
146
145 <% content_for :sidebar do %>
147 <% content_for :sidebar do %>
146 <%= render :partial => 'issues/sidebar' %>
148 <%= render :partial => 'issues/sidebar' %>
147
149
148 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
150 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
149 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
151 (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
150 <div id="watchers">
152 <div id="watchers">
151 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
153 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
152 </div>
154 </div>
153 <% end %>
155 <% end %>
154 <% end %>
156 <% end %>
155
157
156 <% content_for :header_tags do %>
158 <% content_for :header_tags do %>
157 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
159 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
158 <% end %>
160 <% end %>
159
161
160 <%= context_menu issues_context_menu_path %>
162 <%= context_menu %>
@@ -1,21 +1,21
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_personalize_page), {:action => 'page_layout'}, :class => 'icon icon-edit' %>
2 <%= link_to l(:label_personalize_page), {:action => 'page_layout'}, :class => 'icon icon-edit' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_my_page)%></h2>
5 <h2><%=l(:label_my_page)%></h2>
6
6
7 <div id="list-top">
7 <div id="list-top">
8 <%= render_blocks(@blocks['top'], @user) %>
8 <%= render_blocks(@blocks['top'], @user) %>
9 </div>
9 </div>
10
10
11 <div id="list-left" class="splitcontentleft">
11 <div id="list-left" class="splitcontentleft">
12 <%= render_blocks(@blocks['left'], @user) %>
12 <%= render_blocks(@blocks['left'], @user) %>
13 </div>
13 </div>
14
14
15 <div id="list-right" class="splitcontentright">
15 <div id="list-right" class="splitcontentright">
16 <%= render_blocks(@blocks['right'], @user) %>
16 <%= render_blocks(@blocks['right'], @user) %>
17 </div>
17 </div>
18
18
19 <%= context_menu issues_context_menu_path %>
19 <%= context_menu %>
20
20
21 <% html_title(l(:label_my_page)) -%>
21 <% html_title(l(:label_my_page)) -%>
@@ -1,63 +1,63
1 <%= form_tag({}) do -%>
1 <%= form_tag({}, :data => {:cm_url => time_entries_context_menu_path}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
2 <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list time-entries">
4 <table class="list time-entries">
5 <thead>
5 <thead>
6 <tr>
6 <tr>
7 <th class="checkbox hide-when-print">
7 <th class="checkbox hide-when-print">
8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
8 <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
9 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
10 </th>
10 </th>
11 <% @query.inline_columns.each do |column| %>
11 <% @query.inline_columns.each do |column| %>
12 <%= column_header(column) %>
12 <%= column_header(column) %>
13 <% end %>
13 <% end %>
14 <th></th>
14 <th></th>
15 </tr>
15 </tr>
16 </thead>
16 </thead>
17 <tbody>
17 <tbody>
18 <% grouped_query_results(entries, @query, @entry_count_by_group) do |entry, group_name, group_count, group_totals| -%>
18 <% grouped_query_results(entries, @query, @entry_count_by_group) do |entry, group_name, group_count, group_totals| -%>
19 <% if group_name %>
19 <% if group_name %>
20 <% reset_cycle %>
20 <% reset_cycle %>
21 <tr class="group open">
21 <tr class="group open">
22 <td colspan="<%= @query.inline_columns.size + 2 %>">
22 <td colspan="<%= @query.inline_columns.size + 2 %>">
23 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
23 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
24 <span class="name"><%= group_name %></span>
24 <span class="name"><%= group_name %></span>
25 <% if group_count %>
25 <% if group_count %>
26 <span class="count"><%= group_count %></span>
26 <span class="count"><%= group_count %></span>
27 <% end %>
27 <% end %>
28 <span class="totals"><%= group_totals %></span>
28 <span class="totals"><%= group_totals %></span>
29 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
29 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
30 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
30 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
31 </td>
31 </td>
32 </tr>
32 </tr>
33 <% end %>
33 <% end %>
34 <tr id="time-entry-<%= entry.id %>" class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
34 <tr id="time-entry-<%= entry.id %>" class="time-entry <%= cycle("odd", "even") %> hascontextmenu">
35 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
35 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", entry.id, false, :id => nil) %></td>
36 <%= raw @query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, entry)}</td>"}.join %>
36 <%= raw @query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, entry)}</td>"}.join %>
37 <td class="buttons">
37 <td class="buttons">
38 <% if entry.editable_by?(User.current) -%>
38 <% if entry.editable_by?(User.current) -%>
39 <%= link_to l(:button_edit), edit_time_entry_path(entry),
39 <%= link_to l(:button_edit), edit_time_entry_path(entry),
40 :title => l(:button_edit),
40 :title => l(:button_edit),
41 :class => 'icon-only icon-edit' %>
41 :class => 'icon-only icon-edit' %>
42 <%= link_to l(:button_delete), time_entry_path(entry),
42 <%= link_to l(:button_delete), time_entry_path(entry),
43 :data => {:confirm => l(:text_are_you_sure)},
43 :data => {:confirm => l(:text_are_you_sure)},
44 :method => :delete,
44 :method => :delete,
45 :title => l(:button_delete),
45 :title => l(:button_delete),
46 :class => 'icon-only icon-del' %>
46 :class => 'icon-only icon-del' %>
47 <% end -%>
47 <% end -%>
48 </td>
48 </td>
49 </tr>
49 </tr>
50 <% @query.block_columns.each do |column|
50 <% @query.block_columns.each do |column|
51 if (text = column_content(column, issue)) && text.present? -%>
51 if (text = column_content(column, issue)) && text.present? -%>
52 <tr class="<%= current_cycle %>">
52 <tr class="<%= current_cycle %>">
53 <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td>
53 <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"><%= text %></td>
54 </tr>
54 </tr>
55 <% end -%>
55 <% end -%>
56 <% end -%>
56 <% end -%>
57 <% end -%>
57 <% end -%>
58 </tbody>
58 </tbody>
59 </table>
59 </table>
60 </div>
60 </div>
61 <% end -%>
61 <% end -%>
62
62
63 <%= context_menu time_entries_context_menu_path %>
63 <%= context_menu %>
@@ -1,102 +1,102
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:label_version_new), new_project_version_path(@project),
2 <%= link_to(l(:label_version_new), new_project_version_path(@project),
3 :class => 'icon icon-add') if User.current.allowed_to?(:manage_versions, @project) %>
3 :class => 'icon icon-add') if User.current.allowed_to?(:manage_versions, @project) %>
4 </div>
4 </div>
5
5
6 <h2><%=l(:label_roadmap)%></h2>
6 <h2><%=l(:label_roadmap)%></h2>
7
7
8 <% if @versions.empty? %>
8 <% if @versions.empty? %>
9 <p class="nodata"><%= l(:label_no_data) %></p>
9 <p class="nodata"><%= l(:label_no_data) %></p>
10 <% else %>
10 <% else %>
11 <div id="roadmap">
11 <div id="roadmap">
12 <% @versions.each do |version| %>
12 <% @versions.each do |version| %>
13 <article class="version-article <%= version.css_classes %>">
13 <article class="version-article <%= version.css_classes %>">
14 <% if User.current.allowed_to?(:manage_versions, version.project) %>
14 <% if User.current.allowed_to?(:manage_versions, version.project) %>
15 <div class="contextual">
15 <div class="contextual">
16 <%= link_to l(:button_edit), edit_version_path(version), :title => l(:button_edit), :class => 'icon-only icon-edit' %>
16 <%= link_to l(:button_edit), edit_version_path(version), :title => l(:button_edit), :class => 'icon-only icon-edit' %>
17 </div>
17 </div>
18 <% end %>
18 <% end %>
19 <header>
19 <header>
20 <h3 class="version"><%= link_to_version version, :name => version_anchor(version) %></h3>
20 <h3 class="version"><%= link_to_version version, :name => version_anchor(version) %></h3>
21 </header>
21 </header>
22 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
22 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
23 <%= render(:partial => "wiki/content",
23 <%= render(:partial => "wiki/content",
24 :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
24 :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
25 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
25 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
26 <%= form_tag({}) do -%>
26 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
27 <table class="list related-issues">
27 <table class="list related-issues">
28 <caption><%= l(:label_related_issues) %></caption>
28 <caption><%= l(:label_related_issues) %></caption>
29 <% issues.each do |issue| -%>
29 <% issues.each do |issue| -%>
30 <tr class="hascontextmenu">
30 <tr class="hascontextmenu">
31 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
31 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
32 <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
32 <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
33 </tr>
33 </tr>
34 <% end -%>
34 <% end -%>
35 </table>
35 </table>
36 <% end %>
36 <% end %>
37 <% end %>
37 <% end %>
38 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
38 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
39 </article>
39 </article>
40 <% end %>
40 <% end %>
41 </div>
41 </div>
42 <% end %>
42 <% end %>
43
43
44 <% content_for :sidebar do %>
44 <% content_for :sidebar do %>
45 <%= form_tag({}, :method => :get) do %>
45 <%= form_tag({}, :method => :get) do %>
46 <h3><%= l(:label_roadmap) %></h3>
46 <h3><%= l(:label_roadmap) %></h3>
47 <ul>
47 <ul>
48 <% @trackers.each do |tracker| %>
48 <% @trackers.each do |tracker| %>
49 <li>
49 <li>
50 <label>
50 <label>
51 <%= check_box_tag("tracker_ids[]", tracker.id,
51 <%= check_box_tag("tracker_ids[]", tracker.id,
52 (@selected_tracker_ids.include? tracker.id.to_s),
52 (@selected_tracker_ids.include? tracker.id.to_s),
53 :id => nil) %>
53 :id => nil) %>
54 <%= tracker.name %>
54 <%= tracker.name %>
55 </label>
55 </label>
56 </li>
56 </li>
57 <% end %>
57 <% end %>
58 </ul>
58 </ul>
59 <p></p>
59 <p></p>
60 <ul>
60 <ul>
61 <li>
61 <li>
62 <label for="completed">
62 <label for="completed">
63 <%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %>
63 <%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %>
64 </label>
64 </label>
65 </li>
65 </li>
66 <% if @project.descendants.active.any? %>
66 <% if @project.descendants.active.any? %>
67 <li>
67 <li>
68 <%= hidden_field_tag 'with_subprojects', 0, :id => nil %>
68 <%= hidden_field_tag 'with_subprojects', 0, :id => nil %>
69 <label>
69 <label>
70 <%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%>
70 <%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%>
71 </label>
71 </label>
72 </li>
72 </li>
73 <% end %>
73 <% end %>
74 </ul>
74 </ul>
75 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
75 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
76 <% end %>
76 <% end %>
77
77
78 <h3><%= l(:label_version_plural) %></h3>
78 <h3><%= l(:label_version_plural) %></h3>
79 <ul>
79 <ul>
80 <% @versions.each do |version| %>
80 <% @versions.each do |version| %>
81 <li>
81 <li>
82 <%= link_to(format_version_name(version), "##{version_anchor(version)}") %>
82 <%= link_to(format_version_name(version), "##{version_anchor(version)}") %>
83 </li>
83 </li>
84 <% end %>
84 <% end %>
85 </ul>
85 </ul>
86 <% if @completed_versions.present? %>
86 <% if @completed_versions.present? %>
87 <p>
87 <p>
88 <%= link_to_function l(:label_completed_versions),
88 <%= link_to_function l(:label_completed_versions),
89 '$("#toggle-completed-versions").toggleClass("collapsed"); $("#completed-versions").toggle()',
89 '$("#toggle-completed-versions").toggleClass("collapsed"); $("#completed-versions").toggle()',
90 :id => 'toggle-completed-versions', :class => 'collapsible collapsed' %>
90 :id => 'toggle-completed-versions', :class => 'collapsible collapsed' %>
91 <ul id = "completed-versions" style = "display:none;">
91 <ul id = "completed-versions" style = "display:none;">
92 <% @completed_versions.each do |version| %>
92 <% @completed_versions.each do |version| %>
93 <li><%= link_to_version version %></li>
93 <li><%= link_to_version version %></li>
94 <% end %>
94 <% end %>
95 </ul>
95 </ul>
96 </p>
96 </p>
97 <% end %>
97 <% end %>
98 <% end %>
98 <% end %>
99
99
100 <% html_title(l(:label_roadmap)) %>
100 <% html_title(l(:label_roadmap)) %>
101
101
102 <%= context_menu issues_context_menu_path %>
102 <%= context_menu %>
@@ -1,56 +1,56
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:button_edit), edit_version_path(@version), :class => 'icon icon-edit') if User.current.allowed_to?(:manage_versions, @version.project) %>
2 <%= link_to(l(:button_edit), edit_version_path(@version), :class => 'icon icon-edit') if User.current.allowed_to?(:manage_versions, @version.project) %>
3 <%= link_to_if_authorized(l(:button_edit_associated_wikipage, :page_title => @version.wiki_page_title), {:controller => 'wiki', :action => 'edit', :project_id => @version.project, :id => Wiki.titleize(@version.wiki_page_title)}, :class => 'icon icon-edit') unless @version.wiki_page_title.blank? || @version.project.wiki.nil? %>
3 <%= link_to_if_authorized(l(:button_edit_associated_wikipage, :page_title => @version.wiki_page_title), {:controller => 'wiki', :action => 'edit', :project_id => @version.project, :id => Wiki.titleize(@version.wiki_page_title)}, :class => 'icon icon-edit') unless @version.wiki_page_title.blank? || @version.project.wiki.nil? %>
4 <%= delete_link version_path(@version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)) if User.current.allowed_to?(:manage_versions, @version.project) %>
4 <%= delete_link version_path(@version, :back_url => url_for(:controller => 'versions', :action => 'index', :project_id => @version.project)) if User.current.allowed_to?(:manage_versions, @version.project) %>
5 <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
5 <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
6 </div>
6 </div>
7
7
8 <h2><%= @version.name %></h2>
8 <h2><%= @version.name %></h2>
9
9
10 <div id="roadmap" class="<%= @version.css_classes %>">
10 <div id="roadmap" class="<%= @version.css_classes %>">
11 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
11 <%= render :partial => 'versions/overview', :locals => {:version => @version} %>
12 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
12 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
13
13
14 <div id="version-summary">
14 <div id="version-summary">
15 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
15 <% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %>
16 <fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
16 <fieldset class="time-tracking"><legend><%= l(:label_time_tracking) %></legend>
17 <table>
17 <table>
18 <tr>
18 <tr>
19 <th><%= l(:field_estimated_hours) %></th>
19 <th><%= l(:field_estimated_hours) %></th>
20 <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
20 <td class="total-hours"><%= html_hours(l_hours(@version.estimated_hours)) %></td>
21 </tr>
21 </tr>
22 <% if User.current.allowed_to_view_all_time_entries?(@project) %>
22 <% if User.current.allowed_to_view_all_time_entries?(@project) %>
23 <tr>
23 <tr>
24 <th><%= l(:label_spent_time) %></th>
24 <th><%= l(:label_spent_time) %></th>
25 <td class="total-hours"><%= link_to html_hours(l_hours(@version.spent_hours)),
25 <td class="total-hours"><%= link_to html_hours(l_hours(@version.spent_hours)),
26 project_time_entries_path(@version.project, :set_filter => 1, :"issue.fixed_version_id" => @version.id) %></td>
26 project_time_entries_path(@version.project, :set_filter => 1, :"issue.fixed_version_id" => @version.id) %></td>
27 </tr>
27 </tr>
28 <% end %>
28 <% end %>
29 </table>
29 </table>
30 </fieldset>
30 </fieldset>
31 <% end %>
31 <% end %>
32
32
33 <div id="status_by">
33 <div id="status_by">
34 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
34 <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
35 </div>
35 </div>
36 </div>
36 </div>
37
37
38 <% if @issues.present? %>
38 <% if @issues.present? %>
39 <%= form_tag({}) do -%>
39 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
40 <table class="list related-issues">
40 <table class="list related-issues">
41 <caption><%= l(:label_related_issues) %></caption>
41 <caption><%= l(:label_related_issues) %></caption>
42 <%- @issues.each do |issue| -%>
42 <%- @issues.each do |issue| -%>
43 <tr class="issue hascontextmenu">
43 <tr class="issue hascontextmenu">
44 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
44 <td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
45 <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
45 <td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
46 </tr>
46 </tr>
47 <% end %>
47 <% end %>
48 </table>
48 </table>
49 <% end %>
49 <% end %>
50 <%= context_menu issues_context_menu_path %>
50 <%= context_menu %>
51 <% end %>
51 <% end %>
52 </div>
52 </div>
53
53
54 <%= call_hook :view_versions_show_bottom, :version => @version %>
54 <%= call_hook :view_versions_show_bottom, :version => @version %>
55
55
56 <% html_title @version.name %>
56 <% html_title @version.name %>
@@ -1,247 +1,249
1 /* Redmine - project management software
1 /* Redmine - project management software
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3
3
4 var contextMenuObserving;
4 var contextMenuObserving;
5 var contextMenuUrl;
6
5
7 function contextMenuRightClick(event) {
6 function contextMenuRightClick(event) {
8 var target = $(event.target);
7 var target = $(event.target);
9 if (target.is('a')) {return;}
8 if (target.is('a')) {return;}
10 var tr = target.parents('tr').first();
9 var tr = target.parents('tr').first();
11 if (!tr.hasClass('hascontextmenu')) {return;}
10 if (!tr.hasClass('hascontextmenu')) {return;}
12 event.preventDefault();
11 event.preventDefault();
13 if (!contextMenuIsSelected(tr)) {
12 if (!contextMenuIsSelected(tr)) {
14 contextMenuUnselectAll();
13 contextMenuUnselectAll();
15 contextMenuAddSelection(tr);
14 contextMenuAddSelection(tr);
16 contextMenuSetLastSelected(tr);
15 contextMenuSetLastSelected(tr);
17 }
16 }
18 contextMenuShow(event);
17 contextMenuShow(event);
19 }
18 }
20
19
21 function contextMenuClick(event) {
20 function contextMenuClick(event) {
22 var target = $(event.target);
21 var target = $(event.target);
23 var lastSelected;
22 var lastSelected;
24
23
25 if (target.is('a') && target.hasClass('submenu')) {
24 if (target.is('a') && target.hasClass('submenu')) {
26 event.preventDefault();
25 event.preventDefault();
27 return;
26 return;
28 }
27 }
29 contextMenuHide();
28 contextMenuHide();
30 if (target.is('a') || target.is('img')) { return; }
29 if (target.is('a') || target.is('img')) { return; }
31 if (event.which == 1 || (navigator.appVersion.match(/\bMSIE\b/))) {
30 if (event.which == 1 || (navigator.appVersion.match(/\bMSIE\b/))) {
32 var tr = target.parents('tr').first();
31 var tr = target.parents('tr').first();
33 if (tr.length && tr.hasClass('hascontextmenu')) {
32 if (tr.length && tr.hasClass('hascontextmenu')) {
34 // a row was clicked, check if the click was on checkbox
33 // a row was clicked, check if the click was on checkbox
35 if (target.is('input')) {
34 if (target.is('input')) {
36 // a checkbox may be clicked
35 // a checkbox may be clicked
37 if (target.prop('checked')) {
36 if (target.prop('checked')) {
38 tr.addClass('context-menu-selection');
37 tr.addClass('context-menu-selection');
39 } else {
38 } else {
40 tr.removeClass('context-menu-selection');
39 tr.removeClass('context-menu-selection');
41 }
40 }
42 } else {
41 } else {
43 if (event.ctrlKey || event.metaKey) {
42 if (event.ctrlKey || event.metaKey) {
44 contextMenuToggleSelection(tr);
43 contextMenuToggleSelection(tr);
45 } else if (event.shiftKey) {
44 } else if (event.shiftKey) {
46 lastSelected = contextMenuLastSelected();
45 lastSelected = contextMenuLastSelected();
47 if (lastSelected.length) {
46 if (lastSelected.length) {
48 var toggling = false;
47 var toggling = false;
49 $('.hascontextmenu').each(function(){
48 $('.hascontextmenu').each(function(){
50 if (toggling || $(this).is(tr)) {
49 if (toggling || $(this).is(tr)) {
51 contextMenuAddSelection($(this));
50 contextMenuAddSelection($(this));
52 }
51 }
53 if ($(this).is(tr) || $(this).is(lastSelected)) {
52 if ($(this).is(tr) || $(this).is(lastSelected)) {
54 toggling = !toggling;
53 toggling = !toggling;
55 }
54 }
56 });
55 });
57 } else {
56 } else {
58 contextMenuAddSelection(tr);
57 contextMenuAddSelection(tr);
59 }
58 }
60 } else {
59 } else {
61 contextMenuUnselectAll();
60 contextMenuUnselectAll();
62 contextMenuAddSelection(tr);
61 contextMenuAddSelection(tr);
63 }
62 }
64 contextMenuSetLastSelected(tr);
63 contextMenuSetLastSelected(tr);
65 }
64 }
66 } else {
65 } else {
67 // click is outside the rows
66 // click is outside the rows
68 if (target.is('a') && (target.hasClass('disabled') || target.hasClass('submenu'))) {
67 if (target.is('a') && (target.hasClass('disabled') || target.hasClass('submenu'))) {
69 event.preventDefault();
68 event.preventDefault();
70 } else if (target.is('.toggle-selection') || target.is('.ui-dialog *') || $('#ajax-modal').is(':visible')) {
69 } else if (target.is('.toggle-selection') || target.is('.ui-dialog *') || $('#ajax-modal').is(':visible')) {
71 // nop
70 // nop
72 } else {
71 } else {
73 contextMenuUnselectAll();
72 contextMenuUnselectAll();
74 }
73 }
75 }
74 }
76 }
75 }
77 }
76 }
78
77
79 function contextMenuCreate() {
78 function contextMenuCreate() {
80 if ($('#context-menu').length < 1) {
79 if ($('#context-menu').length < 1) {
81 var menu = document.createElement("div");
80 var menu = document.createElement("div");
82 menu.setAttribute("id", "context-menu");
81 menu.setAttribute("id", "context-menu");
83 menu.setAttribute("style", "display:none;");
82 menu.setAttribute("style", "display:none;");
84 document.getElementById("content").appendChild(menu);
83 document.getElementById("content").appendChild(menu);
85 }
84 }
86 }
85 }
87
86
88 function contextMenuShow(event) {
87 function contextMenuShow(event) {
89 var mouse_x = event.pageX;
88 var mouse_x = event.pageX;
90 var mouse_y = event.pageY;
89 var mouse_y = event.pageY;
91 var mouse_y_c = event.clientY;
90 var mouse_y_c = event.clientY;
92 var render_x = mouse_x;
91 var render_x = mouse_x;
93 var render_y = mouse_y;
92 var render_y = mouse_y;
94 var dims;
93 var dims;
95 var menu_width;
94 var menu_width;
96 var menu_height;
95 var menu_height;
97 var window_width;
96 var window_width;
98 var window_height;
97 var window_height;
99 var max_width;
98 var max_width;
100 var max_height;
99 var max_height;
100 var url;
101
101
102 $('#context-menu').css('left', (render_x + 'px'));
102 $('#context-menu').css('left', (render_x + 'px'));
103 $('#context-menu').css('top', (render_y + 'px'));
103 $('#context-menu').css('top', (render_y + 'px'));
104 $('#context-menu').html('');
104 $('#context-menu').html('');
105
105
106 url = $(event.target).parents('form').first().data('cm-url');
107
106 $.ajax({
108 $.ajax({
107 url: contextMenuUrl,
109 url: url,
108 data: $(event.target).parents('form').first().serialize(),
110 data: $(event.target).parents('form').first().serialize(),
109 success: function(data, textStatus, jqXHR) {
111 success: function(data, textStatus, jqXHR) {
110 $('#context-menu').html(data);
112 $('#context-menu').html(data);
111 menu_width = $('#context-menu').width();
113 menu_width = $('#context-menu').width();
112 menu_height = $('#context-menu').height();
114 menu_height = $('#context-menu').height();
113 max_width = mouse_x + 2*menu_width;
115 max_width = mouse_x + 2*menu_width;
114 max_height = mouse_y_c + menu_height;
116 max_height = mouse_y_c + menu_height;
115
117
116 var ws = window_size();
118 var ws = window_size();
117 window_width = ws.width;
119 window_width = ws.width;
118 window_height = ws.height;
120 window_height = ws.height;
119
121
120 /* display the menu above and/or to the left of the click if needed */
122 /* display the menu above and/or to the left of the click if needed */
121 if (max_width > window_width) {
123 if (max_width > window_width) {
122 render_x -= menu_width;
124 render_x -= menu_width;
123 $('#context-menu').addClass('reverse-x');
125 $('#context-menu').addClass('reverse-x');
124 } else {
126 } else {
125 $('#context-menu').removeClass('reverse-x');
127 $('#context-menu').removeClass('reverse-x');
126 }
128 }
127
129
128 if (max_height > window_height) {
130 if (max_height > window_height) {
129 render_y -= menu_height;
131 render_y -= menu_height;
130 $('#context-menu').addClass('reverse-y');
132 $('#context-menu').addClass('reverse-y');
131 // adding class for submenu
133 // adding class for submenu
132 if (mouse_y_c < 325) {
134 if (mouse_y_c < 325) {
133 $('#context-menu .folder').addClass('down');
135 $('#context-menu .folder').addClass('down');
134 }
136 }
135 } else {
137 } else {
136 // adding class for submenu
138 // adding class for submenu
137 if (window_height - mouse_y_c < 345) {
139 if (window_height - mouse_y_c < 345) {
138 $('#context-menu .folder').addClass('up');
140 $('#context-menu .folder').addClass('up');
139 }
141 }
140 $('#context-menu').removeClass('reverse-y');
142 $('#context-menu').removeClass('reverse-y');
141 }
143 }
142
144
143 if (render_x <= 0) render_x = 1;
145 if (render_x <= 0) render_x = 1;
144 if (render_y <= 0) render_y = 1;
146 if (render_y <= 0) render_y = 1;
145 $('#context-menu').css('left', (render_x + 'px'));
147 $('#context-menu').css('left', (render_x + 'px'));
146 $('#context-menu').css('top', (render_y + 'px'));
148 $('#context-menu').css('top', (render_y + 'px'));
147 $('#context-menu').show();
149 $('#context-menu').show();
148
150
149 //if (window.parseStylesheets) { window.parseStylesheets(); } // IE
151 //if (window.parseStylesheets) { window.parseStylesheets(); } // IE
150 }
152 }
151 });
153 });
152 }
154 }
153
155
154 function contextMenuSetLastSelected(tr) {
156 function contextMenuSetLastSelected(tr) {
155 $('.cm-last').removeClass('cm-last');
157 $('.cm-last').removeClass('cm-last');
156 tr.addClass('cm-last');
158 tr.addClass('cm-last');
157 }
159 }
158
160
159 function contextMenuLastSelected() {
161 function contextMenuLastSelected() {
160 return $('.cm-last').first();
162 return $('.cm-last').first();
161 }
163 }
162
164
163 function contextMenuUnselectAll() {
165 function contextMenuUnselectAll() {
164 $('input[type=checkbox].toggle-selection').prop('checked', false);
166 $('input[type=checkbox].toggle-selection').prop('checked', false);
165 $('.hascontextmenu').each(function(){
167 $('.hascontextmenu').each(function(){
166 contextMenuRemoveSelection($(this));
168 contextMenuRemoveSelection($(this));
167 });
169 });
168 $('.cm-last').removeClass('cm-last');
170 $('.cm-last').removeClass('cm-last');
169 }
171 }
170
172
171 function contextMenuHide() {
173 function contextMenuHide() {
172 $('#context-menu').hide();
174 $('#context-menu').hide();
173 }
175 }
174
176
175 function contextMenuToggleSelection(tr) {
177 function contextMenuToggleSelection(tr) {
176 if (contextMenuIsSelected(tr)) {
178 if (contextMenuIsSelected(tr)) {
177 contextMenuRemoveSelection(tr);
179 contextMenuRemoveSelection(tr);
178 } else {
180 } else {
179 contextMenuAddSelection(tr);
181 contextMenuAddSelection(tr);
180 }
182 }
181 }
183 }
182
184
183 function contextMenuAddSelection(tr) {
185 function contextMenuAddSelection(tr) {
184 tr.addClass('context-menu-selection');
186 tr.addClass('context-menu-selection');
185 contextMenuCheckSelectionBox(tr, true);
187 contextMenuCheckSelectionBox(tr, true);
186 contextMenuClearDocumentSelection();
188 contextMenuClearDocumentSelection();
187 }
189 }
188
190
189 function contextMenuRemoveSelection(tr) {
191 function contextMenuRemoveSelection(tr) {
190 tr.removeClass('context-menu-selection');
192 tr.removeClass('context-menu-selection');
191 contextMenuCheckSelectionBox(tr, false);
193 contextMenuCheckSelectionBox(tr, false);
192 }
194 }
193
195
194 function contextMenuIsSelected(tr) {
196 function contextMenuIsSelected(tr) {
195 return tr.hasClass('context-menu-selection');
197 return tr.hasClass('context-menu-selection');
196 }
198 }
197
199
198 function contextMenuCheckSelectionBox(tr, checked) {
200 function contextMenuCheckSelectionBox(tr, checked) {
199 tr.find('input[type=checkbox]').prop('checked', checked);
201 tr.find('input[type=checkbox]').prop('checked', checked);
200 }
202 }
201
203
202 function contextMenuClearDocumentSelection() {
204 function contextMenuClearDocumentSelection() {
203 // TODO
205 // TODO
204 if (document.selection) {
206 if (document.selection) {
205 document.selection.empty(); // IE
207 document.selection.empty(); // IE
206 } else {
208 } else {
207 window.getSelection().removeAllRanges();
209 window.getSelection().removeAllRanges();
208 }
210 }
209 }
211 }
210
212
211 function contextMenuInit(url) {
213 function contextMenuInit() {
212 contextMenuUrl = url;
213 contextMenuCreate();
214 contextMenuCreate();
214 contextMenuUnselectAll();
215 contextMenuUnselectAll();
215
216
216 if (!contextMenuObserving) {
217 if (!contextMenuObserving) {
217 $(document).click(contextMenuClick);
218 $(document).click(contextMenuClick);
218 $(document).contextmenu(contextMenuRightClick);
219 $(document).contextmenu(contextMenuRightClick);
219 contextMenuObserving = true;
220 contextMenuObserving = true;
220 }
221 }
221 }
222 }
222
223
223 function toggleIssuesSelection(el) {
224 function toggleIssuesSelection(el) {
224 var checked = $(this).prop('checked');
225 var checked = $(this).prop('checked');
225 var boxes = $(this).parents('table').find('input[name=ids\\[\\]]');
226 var boxes = $(this).parents('table').find('input[name=ids\\[\\]]');
226 boxes.prop('checked', checked).parents('tr').toggleClass('context-menu-selection', checked);
227 boxes.prop('checked', checked).parents('tr').toggleClass('context-menu-selection', checked);
227 }
228 }
228
229
229 function window_size() {
230 function window_size() {
230 var w;
231 var w;
231 var h;
232 var h;
232 if (window.innerWidth) {
233 if (window.innerWidth) {
233 w = window.innerWidth;
234 w = window.innerWidth;
234 h = window.innerHeight;
235 h = window.innerHeight;
235 } else if (document.documentElement) {
236 } else if (document.documentElement) {
236 w = document.documentElement.clientWidth;
237 w = document.documentElement.clientWidth;
237 h = document.documentElement.clientHeight;
238 h = document.documentElement.clientHeight;
238 } else {
239 } else {
239 w = document.body.clientWidth;
240 w = document.body.clientWidth;
240 h = document.body.clientHeight;
241 h = document.body.clientHeight;
241 }
242 }
242 return {width: w, height: h};
243 return {width: w, height: h};
243 }
244 }
244
245
245 $(document).ready(function(){
246 $(document).ready(function(){
247 contextMenuInit();
246 $('input[type=checkbox].toggle-selection').on('change', toggleIssuesSelection);
248 $('input[type=checkbox].toggle-selection').on('change', toggleIssuesSelection);
247 });
249 });
@@ -1,257 +1,257
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class VersionsControllerTest < Redmine::ControllerTest
20 class VersionsControllerTest < Redmine::ControllerTest
21 fixtures :projects, :versions, :issues, :users, :roles, :members,
21 fixtures :projects, :versions, :issues, :users, :roles, :members,
22 :member_roles, :enabled_modules, :issue_statuses,
22 :member_roles, :enabled_modules, :issue_statuses,
23 :issue_categories, :enumerations
23 :issue_categories, :enumerations
24
24
25 def setup
25 def setup
26 User.current = nil
26 User.current = nil
27 end
27 end
28
28
29 def test_index
29 def test_index
30 get :index, :params => {:project_id => 1}
30 get :index, :params => {:project_id => 1}
31 assert_response :success
31 assert_response :success
32
32
33 # Version with no date set appears
33 # Version with no date set appears
34 assert_select 'h3', :text => Version.find(3).name
34 assert_select 'h3', :text => Version.find(3).name
35 # Completed version doesn't appear
35 # Completed version doesn't appear
36 assert_select 'h3', :text => Version.find(1).name, :count => 0
36 assert_select 'h3', :text => Version.find(1).name, :count => 0
37
37
38 # Context menu on issues
38 # Context menu on issues
39 assert_select "script", :text => Regexp.new(Regexp.escape("contextMenuInit('/issues/context_menu')"))
39 assert_select "form[data-cm-url=?]", '/issues/context_menu'
40 assert_select "div#sidebar" do
40 assert_select "div#sidebar" do
41 # Links to versions anchors
41 # Links to versions anchors
42 assert_select 'a[href=?]', '#2.0'
42 assert_select 'a[href=?]', '#2.0'
43 # Links to completed versions in the sidebar
43 # Links to completed versions in the sidebar
44 assert_select 'a[href=?]', '/versions/1'
44 assert_select 'a[href=?]', '/versions/1'
45 end
45 end
46 end
46 end
47
47
48 def test_index_with_completed_versions
48 def test_index_with_completed_versions
49 get :index, :params => {:project_id => 1, :completed => 1}
49 get :index, :params => {:project_id => 1, :completed => 1}
50 assert_response :success
50 assert_response :success
51
51
52 # Version with no date set appears
52 # Version with no date set appears
53 assert_select 'h3', :text => Version.find(3).name
53 assert_select 'h3', :text => Version.find(3).name
54 # Completed version appears
54 # Completed version appears
55 assert_select 'h3', :text => Version.find(1).name
55 assert_select 'h3', :text => Version.find(1).name
56 end
56 end
57
57
58 def test_index_with_tracker_ids
58 def test_index_with_tracker_ids
59 (1..3).each do |tracker_id|
59 (1..3).each do |tracker_id|
60 Issue.generate! :project_id => 1, :fixed_version_id => 3, :tracker_id => tracker_id
60 Issue.generate! :project_id => 1, :fixed_version_id => 3, :tracker_id => tracker_id
61 end
61 end
62 get :index, :params => {:project_id => 1, :tracker_ids => [1, 3]}
62 get :index, :params => {:project_id => 1, :tracker_ids => [1, 3]}
63 assert_response :success
63 assert_response :success
64 assert_select 'a.issue.tracker-1'
64 assert_select 'a.issue.tracker-1'
65 assert_select 'a.issue.tracker-2', 0
65 assert_select 'a.issue.tracker-2', 0
66 assert_select 'a.issue.tracker-3'
66 assert_select 'a.issue.tracker-3'
67 end
67 end
68
68
69 def test_index_showing_subprojects_versions
69 def test_index_showing_subprojects_versions
70 @subproject_version = Version.create!(:project => Project.find(3), :name => "Subproject version")
70 @subproject_version = Version.create!(:project => Project.find(3), :name => "Subproject version")
71 get :index, :params => {:project_id => 1, :with_subprojects => 1}
71 get :index, :params => {:project_id => 1, :with_subprojects => 1}
72 assert_response :success
72 assert_response :success
73
73
74 # Shared version
74 # Shared version
75 assert_select 'h3', :text => Version.find(4).name
75 assert_select 'h3', :text => Version.find(4).name
76 # Subproject version
76 # Subproject version
77 assert_select 'h3', :text => /Subproject version/
77 assert_select 'h3', :text => /Subproject version/
78 end
78 end
79
79
80 def test_index_should_prepend_shared_versions
80 def test_index_should_prepend_shared_versions
81 get :index, :params => {:project_id => 1}
81 get :index, :params => {:project_id => 1}
82 assert_response :success
82 assert_response :success
83
83
84 assert_select '#sidebar' do
84 assert_select '#sidebar' do
85 assert_select 'a[href=?]', '#2.0', :text => '2.0'
85 assert_select 'a[href=?]', '#2.0', :text => '2.0'
86 assert_select 'a[href=?]', '#subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
86 assert_select 'a[href=?]', '#subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
87 end
87 end
88 assert_select '#content' do
88 assert_select '#content' do
89 assert_select 'a[name=?]', '2.0', :text => '2.0'
89 assert_select 'a[name=?]', '2.0', :text => '2.0'
90 assert_select 'a[name=?]', 'subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
90 assert_select 'a[name=?]', 'subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0'
91 end
91 end
92 end
92 end
93
93
94 def test_show
94 def test_show
95 get :show, :params => {:id => 2}
95 get :show, :params => {:id => 2}
96 assert_response :success
96 assert_response :success
97
97
98 assert_select 'h2', :text => /1.0/
98 assert_select 'h2', :text => /1.0/
99 end
99 end
100
100
101 def test_show_should_link_to_spent_time_on_version
101 def test_show_should_link_to_spent_time_on_version
102 version = Version.generate!
102 version = Version.generate!
103 issue = Issue.generate(:fixed_version => version)
103 issue = Issue.generate(:fixed_version => version)
104 TimeEntry.generate!(:issue => issue, :hours => 7.2)
104 TimeEntry.generate!(:issue => issue, :hours => 7.2)
105
105
106 get :show, :params => {:id => version.id}
106 get :show, :params => {:id => version.id}
107 assert_response :success
107 assert_response :success
108
108
109 assert_select '.total-hours', :text => '7.20 hours'
109 assert_select '.total-hours', :text => '7.20 hours'
110 assert_select '.total-hours a[href=?]', "/projects/ecookbook/time_entries?issue.fixed_version_id=#{version.id}&set_filter=1"
110 assert_select '.total-hours a[href=?]', "/projects/ecookbook/time_entries?issue.fixed_version_id=#{version.id}&set_filter=1"
111 end
111 end
112
112
113 def test_show_should_display_nil_counts
113 def test_show_should_display_nil_counts
114 with_settings :default_language => 'en' do
114 with_settings :default_language => 'en' do
115 get :show, :params => {:id => 2, :status_by => 'category'}
115 get :show, :params => {:id => 2, :status_by => 'category'}
116 assert_response :success
116 assert_response :success
117 assert_select 'div#status_by' do
117 assert_select 'div#status_by' do
118 assert_select 'select[name=status_by]' do
118 assert_select 'select[name=status_by]' do
119 assert_select 'option[value=category][selected=selected]'
119 assert_select 'option[value=category][selected=selected]'
120 end
120 end
121 assert_select 'a', :text => 'none'
121 assert_select 'a', :text => 'none'
122 end
122 end
123 end
123 end
124 end
124 end
125
125
126 def test_new
126 def test_new
127 @request.session[:user_id] = 2
127 @request.session[:user_id] = 2
128 get :new, :params => {:project_id => '1'}
128 get :new, :params => {:project_id => '1'}
129 assert_response :success
129 assert_response :success
130 assert_select 'input[name=?]', 'version[name]'
130 assert_select 'input[name=?]', 'version[name]'
131 assert_select 'select[name=?]', 'version[status]', false
131 assert_select 'select[name=?]', 'version[status]', false
132 end
132 end
133
133
134 def test_new_from_issue_form
134 def test_new_from_issue_form
135 @request.session[:user_id] = 2
135 @request.session[:user_id] = 2
136 xhr :get, :new, :params => {:project_id => '1'}
136 xhr :get, :new, :params => {:project_id => '1'}
137 assert_response :success
137 assert_response :success
138 assert_equal 'text/javascript', response.content_type
138 assert_equal 'text/javascript', response.content_type
139 end
139 end
140
140
141 def test_create
141 def test_create
142 @request.session[:user_id] = 2 # manager
142 @request.session[:user_id] = 2 # manager
143 assert_difference 'Version.count' do
143 assert_difference 'Version.count' do
144 post :create, :params => {:project_id => '1', :version => {:name => 'test_add_version'}}
144 post :create, :params => {:project_id => '1', :version => {:name => 'test_add_version'}}
145 end
145 end
146 assert_redirected_to '/projects/ecookbook/settings/versions'
146 assert_redirected_to '/projects/ecookbook/settings/versions'
147 version = Version.find_by_name('test_add_version')
147 version = Version.find_by_name('test_add_version')
148 assert_not_nil version
148 assert_not_nil version
149 assert_equal 1, version.project_id
149 assert_equal 1, version.project_id
150 end
150 end
151
151
152 def test_create_from_issue_form
152 def test_create_from_issue_form
153 @request.session[:user_id] = 2
153 @request.session[:user_id] = 2
154 assert_difference 'Version.count' do
154 assert_difference 'Version.count' do
155 xhr :post, :create, :params => {:project_id => '1', :version => {:name => 'test_add_version_from_issue_form'}}
155 xhr :post, :create, :params => {:project_id => '1', :version => {:name => 'test_add_version_from_issue_form'}}
156 end
156 end
157 version = Version.find_by_name('test_add_version_from_issue_form')
157 version = Version.find_by_name('test_add_version_from_issue_form')
158 assert_not_nil version
158 assert_not_nil version
159 assert_equal 1, version.project_id
159 assert_equal 1, version.project_id
160
160
161 assert_response :success
161 assert_response :success
162 assert_equal 'text/javascript', response.content_type
162 assert_equal 'text/javascript', response.content_type
163 assert_include 'test_add_version_from_issue_form', response.body
163 assert_include 'test_add_version_from_issue_form', response.body
164 end
164 end
165
165
166 def test_create_from_issue_form_with_failure
166 def test_create_from_issue_form_with_failure
167 @request.session[:user_id] = 2
167 @request.session[:user_id] = 2
168 assert_no_difference 'Version.count' do
168 assert_no_difference 'Version.count' do
169 xhr :post, :create, :params => {:project_id => '1', :version => {:name => ''}}
169 xhr :post, :create, :params => {:project_id => '1', :version => {:name => ''}}
170 end
170 end
171 assert_response :success
171 assert_response :success
172 assert_equal 'text/javascript', response.content_type
172 assert_equal 'text/javascript', response.content_type
173 end
173 end
174
174
175 def test_get_edit
175 def test_get_edit
176 @request.session[:user_id] = 2
176 @request.session[:user_id] = 2
177 get :edit, :params => {:id => 2}
177 get :edit, :params => {:id => 2}
178 assert_response :success
178 assert_response :success
179 version = Version.find(2)
179 version = Version.find(2)
180
180
181 assert_select 'select[name=?]', 'version[status]' do
181 assert_select 'select[name=?]', 'version[status]' do
182 assert_select 'option[value=?][selected="selected"]', version.status
182 assert_select 'option[value=?][selected="selected"]', version.status
183 end
183 end
184 assert_select 'input[name=?][value=?]', 'version[name]', version.name
184 assert_select 'input[name=?][value=?]', 'version[name]', version.name
185 end
185 end
186
186
187 def test_close_completed
187 def test_close_completed
188 Version.update_all("status = 'open'")
188 Version.update_all("status = 'open'")
189 @request.session[:user_id] = 2
189 @request.session[:user_id] = 2
190 put :close_completed, :params => {:project_id => 'ecookbook'}
190 put :close_completed, :params => {:project_id => 'ecookbook'}
191 assert_redirected_to :controller => 'projects', :action => 'settings',
191 assert_redirected_to :controller => 'projects', :action => 'settings',
192 :tab => 'versions', :id => 'ecookbook'
192 :tab => 'versions', :id => 'ecookbook'
193 assert_not_nil Version.find_by_status('closed')
193 assert_not_nil Version.find_by_status('closed')
194 end
194 end
195
195
196 def test_post_update
196 def test_post_update
197 @request.session[:user_id] = 2
197 @request.session[:user_id] = 2
198 put :update, :params => {
198 put :update, :params => {
199 :id => 2,
199 :id => 2,
200 :version => {
200 :version => {
201 :name => 'New version name',
201 :name => 'New version name',
202 :effective_date => Date.today.strftime("%Y-%m-%d")
202 :effective_date => Date.today.strftime("%Y-%m-%d")
203 }
203 }
204 }
204 }
205 assert_redirected_to :controller => 'projects', :action => 'settings',
205 assert_redirected_to :controller => 'projects', :action => 'settings',
206 :tab => 'versions', :id => 'ecookbook'
206 :tab => 'versions', :id => 'ecookbook'
207 version = Version.find(2)
207 version = Version.find(2)
208 assert_equal 'New version name', version.name
208 assert_equal 'New version name', version.name
209 assert_equal Date.today, version.effective_date
209 assert_equal Date.today, version.effective_date
210 end
210 end
211
211
212 def test_post_update_with_validation_failure
212 def test_post_update_with_validation_failure
213 @request.session[:user_id] = 2
213 @request.session[:user_id] = 2
214 put :update, :params => {
214 put :update, :params => {
215 :id => 2,
215 :id => 2,
216 :version => {
216 :version => {
217 :name => '',
217 :name => '',
218 :effective_date => Date.today.strftime("%Y-%m-%d")
218 :effective_date => Date.today.strftime("%Y-%m-%d")
219 }
219 }
220 }
220 }
221 assert_response :success
221 assert_response :success
222 assert_select_error /Name cannot be blank/
222 assert_select_error /Name cannot be blank/
223 end
223 end
224
224
225 def test_destroy
225 def test_destroy
226 @request.session[:user_id] = 2
226 @request.session[:user_id] = 2
227 assert_difference 'Version.count', -1 do
227 assert_difference 'Version.count', -1 do
228 delete :destroy, :params => {:id => 3}
228 delete :destroy, :params => {:id => 3}
229 end
229 end
230 assert_redirected_to :controller => 'projects', :action => 'settings',
230 assert_redirected_to :controller => 'projects', :action => 'settings',
231 :tab => 'versions', :id => 'ecookbook'
231 :tab => 'versions', :id => 'ecookbook'
232 assert_nil Version.find_by_id(3)
232 assert_nil Version.find_by_id(3)
233 end
233 end
234
234
235 def test_destroy_version_in_use_should_fail
235 def test_destroy_version_in_use_should_fail
236 @request.session[:user_id] = 2
236 @request.session[:user_id] = 2
237 assert_no_difference 'Version.count' do
237 assert_no_difference 'Version.count' do
238 delete :destroy, :params => {:id => 2}
238 delete :destroy, :params => {:id => 2}
239 end
239 end
240 assert_redirected_to :controller => 'projects', :action => 'settings',
240 assert_redirected_to :controller => 'projects', :action => 'settings',
241 :tab => 'versions', :id => 'ecookbook'
241 :tab => 'versions', :id => 'ecookbook'
242 assert flash[:error].match(/Unable to delete version/)
242 assert flash[:error].match(/Unable to delete version/)
243 assert Version.find_by_id(2)
243 assert Version.find_by_id(2)
244 end
244 end
245
245
246 def test_issue_status_by
246 def test_issue_status_by
247 xhr :get, :status_by, :params => {:id => 2}
247 xhr :get, :status_by, :params => {:id => 2}
248 assert_response :success
248 assert_response :success
249 end
249 end
250
250
251 def test_issue_status_by_status
251 def test_issue_status_by_status
252 xhr :get, :status_by, :params => {:id => 2, :status_by => 'status'}
252 xhr :get, :status_by, :params => {:id => 2, :status_by => 'status'}
253 assert_response :success
253 assert_response :success
254 assert_include 'Assigned', response.body
254 assert_include 'Assigned', response.body
255 assert_include 'Closed', response.body
255 assert_include 'Closed', response.body
256 end
256 end
257 end
257 end
General Comments 0
You need to be logged in to leave comments. Login now