##// END OF EJS Templates
Use HTML5 date input fields instead of text fields with jquery ui date pickers (#19468)....
Jean-Philippe Lang -
r14993:c418fab8a76b
parent child
Show More
@@ -1,1366 +1,1366
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Themes::Helper
29 include Redmine::Themes::Helper
30 include Redmine::Hook::Helper
30 include Redmine::Hook::Helper
31
31
32 extend Forwardable
32 extend Forwardable
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34
34
35 # Return true if user is authorized for controller/action, otherwise false
35 # Return true if user is authorized for controller/action, otherwise false
36 def authorize_for(controller, action)
36 def authorize_for(controller, action)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 end
38 end
39
39
40 # Display a link if user is authorized
40 # Display a link if user is authorized
41 #
41 #
42 # @param [String] name Anchor text (passed to link_to)
42 # @param [String] name Anchor text (passed to link_to)
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [optional, Hash] html_options Options passed to link_to
44 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active? || (User.current.admin? && user.logged?)
54 if user.active? || (User.current.admin? && user.logged?)
55 link_to name, user_path(user), :class => user.css_classes
55 link_to name, user_path(user), :class => user.css_classes
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 #
72 #
73 def link_to_issue(issue, options={})
73 def link_to_issue(issue, options={})
74 title = nil
74 title = nil
75 subject = nil
75 subject = nil
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 if options[:subject] == false
77 if options[:subject] == false
78 title = issue.subject.truncate(60)
78 title = issue.subject.truncate(60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if truncate_length = options[:truncate]
81 if truncate_length = options[:truncate]
82 subject = subject.truncate(truncate_length)
82 subject = subject.truncate(truncate_length)
83 end
83 end
84 end
84 end
85 only_path = options[:only_path].nil? ? true : options[:only_path]
85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 s = link_to(text, issue_url(issue, :only_path => only_path),
86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 :class => issue.css_classes, :title => title)
87 :class => issue.css_classes, :title => title)
88 s << h(": #{subject}") if subject
88 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
89 s = h("#{issue.project} - ") + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 html_options = options.slice!(:only_path)
100 html_options = options.slice!(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
102 url = send(route_method, attachment, attachment.filename, options)
102 url = send(route_method, attachment, attachment.filename, options)
103 link_to text, url, html_options
103 link_to text, url, html_options
104 end
104 end
105
105
106 # Generates a link to a SCM revision
106 # Generates a link to a SCM revision
107 # Options:
107 # Options:
108 # * :text - Link text (default to the formatted revision)
108 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, repository, options={})
109 def link_to_revision(revision, repository, options={})
110 if repository.is_a?(Project)
110 if repository.is_a?(Project)
111 repository = repository.repository
111 repository = repository.repository
112 end
112 end
113 text = options.delete(:text) || format_revision(revision)
113 text = options.delete(:text) || format_revision(revision)
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 link_to(
115 link_to(
116 h(text),
116 h(text),
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 :title => l(:label_revision_id, format_revision(revision)),
118 :title => l(:label_revision_id, format_revision(revision)),
119 :accesskey => options[:accesskey]
119 :accesskey => options[:accesskey]
120 )
120 )
121 end
121 end
122
122
123 # Generates a link to a message
123 # Generates a link to a message
124 def link_to_message(message, options={}, html_options = nil)
124 def link_to_message(message, options={}, html_options = nil)
125 link_to(
125 link_to(
126 message.subject.truncate(60),
126 message.subject.truncate(60),
127 board_message_url(message.board_id, message.parent_id || message.id, {
127 board_message_url(message.board_id, message.parent_id || message.id, {
128 :r => (message.parent_id && message.id),
128 :r => (message.parent_id && message.id),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :only_path => true
130 :only_path => true
131 }.merge(options)),
131 }.merge(options)),
132 html_options
132 html_options
133 )
133 )
134 end
134 end
135
135
136 # Generates a link to a project if active
136 # Generates a link to a project if active
137 # Examples:
137 # Examples:
138 #
138 #
139 # link_to_project(project) # => link to the specified project overview
139 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 #
142 #
143 def link_to_project(project, options={}, html_options = nil)
143 def link_to_project(project, options={}, html_options = nil)
144 if project.archived?
144 if project.archived?
145 h(project.name)
145 h(project.name)
146 else
146 else
147 link_to project.name,
147 link_to project.name,
148 project_url(project, {:only_path => true}.merge(options)),
148 project_url(project, {:only_path => true}.merge(options)),
149 html_options
149 html_options
150 end
150 end
151 end
151 end
152
152
153 # Generates a link to a project settings if active
153 # Generates a link to a project settings if active
154 def link_to_project_settings(project, options={}, html_options=nil)
154 def link_to_project_settings(project, options={}, html_options=nil)
155 if project.active?
155 if project.active?
156 link_to project.name, settings_project_path(project, options), html_options
156 link_to project.name, settings_project_path(project, options), html_options
157 elsif project.archived?
157 elsif project.archived?
158 h(project.name)
158 h(project.name)
159 else
159 else
160 link_to project.name, project_path(project, options), html_options
160 link_to project.name, project_path(project, options), html_options
161 end
161 end
162 end
162 end
163
163
164 # Generates a link to a version
164 # Generates a link to a version
165 def link_to_version(version, options = {})
165 def link_to_version(version, options = {})
166 return '' unless version && version.is_a?(Version)
166 return '' unless version && version.is_a?(Version)
167 options = {:title => format_date(version.effective_date)}.merge(options)
167 options = {:title => format_date(version.effective_date)}.merge(options)
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 end
169 end
170
170
171 # Helper that formats object for html or text rendering
171 # Helper that formats object for html or text rendering
172 def format_object(object, html=true, &block)
172 def format_object(object, html=true, &block)
173 if block_given?
173 if block_given?
174 object = yield object
174 object = yield object
175 end
175 end
176 case object.class.name
176 case object.class.name
177 when 'Array'
177 when 'Array'
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 when 'Time'
179 when 'Time'
180 format_time(object)
180 format_time(object)
181 when 'Date'
181 when 'Date'
182 format_date(object)
182 format_date(object)
183 when 'Fixnum'
183 when 'Fixnum'
184 object.to_s
184 object.to_s
185 when 'Float'
185 when 'Float'
186 sprintf "%.2f", object
186 sprintf "%.2f", object
187 when 'User'
187 when 'User'
188 html ? link_to_user(object) : object.to_s
188 html ? link_to_user(object) : object.to_s
189 when 'Project'
189 when 'Project'
190 html ? link_to_project(object) : object.to_s
190 html ? link_to_project(object) : object.to_s
191 when 'Version'
191 when 'Version'
192 html ? link_to_version(object) : object.to_s
192 html ? link_to_version(object) : object.to_s
193 when 'TrueClass'
193 when 'TrueClass'
194 l(:general_text_Yes)
194 l(:general_text_Yes)
195 when 'FalseClass'
195 when 'FalseClass'
196 l(:general_text_No)
196 l(:general_text_No)
197 when 'Issue'
197 when 'Issue'
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 when 'CustomValue', 'CustomFieldValue'
199 when 'CustomValue', 'CustomFieldValue'
200 if object.custom_field
200 if object.custom_field
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 if f.nil? || f.is_a?(String)
202 if f.nil? || f.is_a?(String)
203 f
203 f
204 else
204 else
205 format_object(f, html, &block)
205 format_object(f, html, &block)
206 end
206 end
207 else
207 else
208 object.value.to_s
208 object.value.to_s
209 end
209 end
210 else
210 else
211 html ? h(object) : object.to_s
211 html ? h(object) : object.to_s
212 end
212 end
213 end
213 end
214
214
215 def wiki_page_path(page, options={})
215 def wiki_page_path(page, options={})
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 end
217 end
218
218
219 def thumbnail_tag(attachment)
219 def thumbnail_tag(attachment)
220 link_to image_tag(thumbnail_path(attachment)),
220 link_to image_tag(thumbnail_path(attachment)),
221 named_attachment_path(attachment, attachment.filename),
221 named_attachment_path(attachment, attachment.filename),
222 :title => attachment.filename
222 :title => attachment.filename
223 end
223 end
224
224
225 def toggle_link(name, id, options={})
225 def toggle_link(name, id, options={})
226 onclick = "$('##{id}').toggle(); "
226 onclick = "$('##{id}').toggle(); "
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 onclick << "return false;"
228 onclick << "return false;"
229 link_to(name, "#", :onclick => onclick)
229 link_to(name, "#", :onclick => onclick)
230 end
230 end
231
231
232 def format_activity_title(text)
232 def format_activity_title(text)
233 h(truncate_single_line_raw(text, 100))
233 h(truncate_single_line_raw(text, 100))
234 end
234 end
235
235
236 def format_activity_day(date)
236 def format_activity_day(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 end
238 end
239
239
240 def format_activity_description(text)
240 def format_activity_description(text)
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 ).gsub(/[\r\n]+/, "<br />").html_safe
242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 end
243 end
244
244
245 def format_version_name(version)
245 def format_version_name(version)
246 if version.project == @project
246 if version.project == @project
247 h(version)
247 h(version)
248 else
248 else
249 h("#{version.project} - #{version}")
249 h("#{version.project} - #{version}")
250 end
250 end
251 end
251 end
252
252
253 def due_date_distance_in_words(date)
253 def due_date_distance_in_words(date)
254 if date
254 if date
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 end
256 end
257 end
257 end
258
258
259 # Renders a tree of projects as a nested set of unordered lists
259 # Renders a tree of projects as a nested set of unordered lists
260 # The given collection may be a subset of the whole project tree
260 # The given collection may be a subset of the whole project tree
261 # (eg. some intermediate nodes are private and can not be seen)
261 # (eg. some intermediate nodes are private and can not be seen)
262 def render_project_nested_lists(projects, &block)
262 def render_project_nested_lists(projects, &block)
263 s = ''
263 s = ''
264 if projects.any?
264 if projects.any?
265 ancestors = []
265 ancestors = []
266 original_project = @project
266 original_project = @project
267 projects.sort_by(&:lft).each do |project|
267 projects.sort_by(&:lft).each do |project|
268 # set the project environment to please macros.
268 # set the project environment to please macros.
269 @project = project
269 @project = project
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 else
272 else
273 ancestors.pop
273 ancestors.pop
274 s << "</li>"
274 s << "</li>"
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 ancestors.pop
276 ancestors.pop
277 s << "</ul></li>\n"
277 s << "</ul></li>\n"
278 end
278 end
279 end
279 end
280 classes = (ancestors.empty? ? 'root' : 'child')
280 classes = (ancestors.empty? ? 'root' : 'child')
281 s << "<li class='#{classes}'><div class='#{classes}'>"
281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << h(block_given? ? capture(project, &block) : project.name)
282 s << h(block_given? ? capture(project, &block) : project.name)
283 s << "</div>\n"
283 s << "</div>\n"
284 ancestors << project
284 ancestors << project
285 end
285 end
286 s << ("</li></ul>\n" * ancestors.size)
286 s << ("</li></ul>\n" * ancestors.size)
287 @project = original_project
287 @project = original_project
288 end
288 end
289 s.html_safe
289 s.html_safe
290 end
290 end
291
291
292 def render_page_hierarchy(pages, node=nil, options={})
292 def render_page_hierarchy(pages, node=nil, options={})
293 content = ''
293 content = ''
294 if pages[node]
294 if pages[node]
295 content << "<ul class=\"pages-hierarchy\">\n"
295 content << "<ul class=\"pages-hierarchy\">\n"
296 pages[node].each do |page|
296 pages[node].each do |page|
297 content << "<li>"
297 content << "<li>"
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "</li>\n"
301 content << "</li>\n"
302 end
302 end
303 content << "</ul>\n"
303 content << "</ul>\n"
304 end
304 end
305 content.html_safe
305 content.html_safe
306 end
306 end
307
307
308 # Renders flash messages
308 # Renders flash messages
309 def render_flash_messages
309 def render_flash_messages
310 s = ''
310 s = ''
311 flash.each do |k,v|
311 flash.each do |k,v|
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 end
313 end
314 s.html_safe
314 s.html_safe
315 end
315 end
316
316
317 # Renders tabs and their content
317 # Renders tabs and their content
318 def render_tabs(tabs, selected=params[:tab])
318 def render_tabs(tabs, selected=params[:tab])
319 if tabs.any?
319 if tabs.any?
320 unless tabs.detect {|tab| tab[:name] == selected}
320 unless tabs.detect {|tab| tab[:name] == selected}
321 selected = nil
321 selected = nil
322 end
322 end
323 selected ||= tabs.first[:name]
323 selected ||= tabs.first[:name]
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 else
325 else
326 content_tag 'p', l(:label_no_data), :class => "nodata"
326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 end
327 end
328 end
328 end
329
329
330 # Renders the project quick-jump box
330 # Renders the project quick-jump box
331 def render_project_jump_box
331 def render_project_jump_box
332 return unless User.current.logged?
332 return unless User.current.logged?
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 if projects.any?
334 if projects.any?
335 options =
335 options =
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 '<option value="" disabled="disabled">---</option>').html_safe
337 '<option value="" disabled="disabled">---</option>').html_safe
338
338
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 { :value => project_path(:id => p, :jump => current_menu_item) }
340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 end
341 end
342
342
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 end
345 end
346 end
346 end
347
347
348 def project_tree_options_for_select(projects, options = {})
348 def project_tree_options_for_select(projects, options = {})
349 s = ''.html_safe
349 s = ''.html_safe
350 if blank_text = options[:include_blank]
350 if blank_text = options[:include_blank]
351 if blank_text == true
351 if blank_text == true
352 blank_text = '&nbsp;'.html_safe
352 blank_text = '&nbsp;'.html_safe
353 end
353 end
354 s << content_tag('option', blank_text, :value => '')
354 s << content_tag('option', blank_text, :value => '')
355 end
355 end
356 project_tree(projects) do |project, level|
356 project_tree(projects) do |project, level|
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 tag_options = {:value => project.id}
358 tag_options = {:value => project.id}
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 tag_options[:selected] = 'selected'
360 tag_options[:selected] = 'selected'
361 else
361 else
362 tag_options[:selected] = nil
362 tag_options[:selected] = nil
363 end
363 end
364 tag_options.merge!(yield(project)) if block_given?
364 tag_options.merge!(yield(project)) if block_given?
365 s << content_tag('option', name_prefix + h(project), tag_options)
365 s << content_tag('option', name_prefix + h(project), tag_options)
366 end
366 end
367 s.html_safe
367 s.html_safe
368 end
368 end
369
369
370 # Yields the given block for each project with its level in the tree
370 # Yields the given block for each project with its level in the tree
371 #
371 #
372 # Wrapper for Project#project_tree
372 # Wrapper for Project#project_tree
373 def project_tree(projects, &block)
373 def project_tree(projects, &block)
374 Project.project_tree(projects, &block)
374 Project.project_tree(projects, &block)
375 end
375 end
376
376
377 def principals_check_box_tags(name, principals)
377 def principals_check_box_tags(name, principals)
378 s = ''
378 s = ''
379 principals.each do |principal|
379 principals.each do |principal|
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 end
381 end
382 s.html_safe
382 s.html_safe
383 end
383 end
384
384
385 # Returns a string for users/groups option tags
385 # Returns a string for users/groups option tags
386 def principals_options_for_select(collection, selected=nil)
386 def principals_options_for_select(collection, selected=nil)
387 s = ''
387 s = ''
388 if collection.include?(User.current)
388 if collection.include?(User.current)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 end
390 end
391 groups = ''
391 groups = ''
392 collection.sort.each do |element|
392 collection.sort.each do |element|
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 end
395 end
396 unless groups.empty?
396 unless groups.empty?
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 end
398 end
399 s.html_safe
399 s.html_safe
400 end
400 end
401
401
402 def option_tag(name, text, value, selected=nil, options={})
402 def option_tag(name, text, value, selected=nil, options={})
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 end
404 end
405
405
406 def truncate_single_line_raw(string, length)
406 def truncate_single_line_raw(string, length)
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 end
408 end
409
409
410 # Truncates at line break after 250 characters or options[:length]
410 # Truncates at line break after 250 characters or options[:length]
411 def truncate_lines(string, options={})
411 def truncate_lines(string, options={})
412 length = options[:length] || 250
412 length = options[:length] || 250
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 "#{$1}..."
414 "#{$1}..."
415 else
415 else
416 string
416 string
417 end
417 end
418 end
418 end
419
419
420 def anchor(text)
420 def anchor(text)
421 text.to_s.gsub(' ', '_')
421 text.to_s.gsub(' ', '_')
422 end
422 end
423
423
424 def html_hours(text)
424 def html_hours(text)
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 end
426 end
427
427
428 def authoring(created, author, options={})
428 def authoring(created, author, options={})
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 end
430 end
431
431
432 def time_tag(time)
432 def time_tag(time)
433 text = distance_of_time_in_words(Time.now, time)
433 text = distance_of_time_in_words(Time.now, time)
434 if @project
434 if @project
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 else
436 else
437 content_tag('abbr', text, :title => format_time(time))
437 content_tag('abbr', text, :title => format_time(time))
438 end
438 end
439 end
439 end
440
440
441 def syntax_highlight_lines(name, content)
441 def syntax_highlight_lines(name, content)
442 lines = []
442 lines = []
443 syntax_highlight(name, content).each_line { |line| lines << line }
443 syntax_highlight(name, content).each_line { |line| lines << line }
444 lines
444 lines
445 end
445 end
446
446
447 def syntax_highlight(name, content)
447 def syntax_highlight(name, content)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 end
449 end
450
450
451 def to_path_param(path)
451 def to_path_param(path)
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 str.blank? ? nil : str
453 str.blank? ? nil : str
454 end
454 end
455
455
456 def reorder_links(name, url, method = :post)
456 def reorder_links(name, url, method = :post)
457 # TODO: remove associated styles from application.css too
457 # TODO: remove associated styles from application.css too
458 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
458 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
459
459
460 link_to(l(:label_sort_highest),
460 link_to(l(:label_sort_highest),
461 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
461 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
462 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
462 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
463 link_to(l(:label_sort_higher),
463 link_to(l(:label_sort_higher),
464 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
464 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
465 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
465 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
466 link_to(l(:label_sort_lower),
466 link_to(l(:label_sort_lower),
467 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
467 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
468 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
468 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
469 link_to(l(:label_sort_lowest),
469 link_to(l(:label_sort_lowest),
470 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
470 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
471 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
471 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
472 end
472 end
473
473
474 def reorder_handle(object, options={})
474 def reorder_handle(object, options={})
475 data = {
475 data = {
476 :reorder_url => options[:url] || url_for(object),
476 :reorder_url => options[:url] || url_for(object),
477 :reorder_param => options[:param] || object.class.name.underscore
477 :reorder_param => options[:param] || object.class.name.underscore
478 }
478 }
479 content_tag('span', '',
479 content_tag('span', '',
480 :class => "sort-handle",
480 :class => "sort-handle",
481 :data => data,
481 :data => data,
482 :title => l(:button_sort))
482 :title => l(:button_sort))
483 end
483 end
484
484
485 def breadcrumb(*args)
485 def breadcrumb(*args)
486 elements = args.flatten
486 elements = args.flatten
487 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
487 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
488 end
488 end
489
489
490 def other_formats_links(&block)
490 def other_formats_links(&block)
491 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
491 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
492 yield Redmine::Views::OtherFormatsBuilder.new(self)
492 yield Redmine::Views::OtherFormatsBuilder.new(self)
493 concat('</p>'.html_safe)
493 concat('</p>'.html_safe)
494 end
494 end
495
495
496 def page_header_title
496 def page_header_title
497 if @project.nil? || @project.new_record?
497 if @project.nil? || @project.new_record?
498 h(Setting.app_title)
498 h(Setting.app_title)
499 else
499 else
500 b = []
500 b = []
501 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
501 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
502 if ancestors.any?
502 if ancestors.any?
503 root = ancestors.shift
503 root = ancestors.shift
504 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
504 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
505 if ancestors.size > 2
505 if ancestors.size > 2
506 b << "\xe2\x80\xa6"
506 b << "\xe2\x80\xa6"
507 ancestors = ancestors[-2, 2]
507 ancestors = ancestors[-2, 2]
508 end
508 end
509 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
509 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
510 end
510 end
511 b << content_tag(:span, h(@project), class: 'current-project')
511 b << content_tag(:span, h(@project), class: 'current-project')
512 if b.size > 1
512 if b.size > 1
513 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
513 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
514 path = safe_join(b[0..-2], separator) + separator
514 path = safe_join(b[0..-2], separator) + separator
515 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
515 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
516 end
516 end
517 safe_join b
517 safe_join b
518 end
518 end
519 end
519 end
520
520
521 # Returns a h2 tag and sets the html title with the given arguments
521 # Returns a h2 tag and sets the html title with the given arguments
522 def title(*args)
522 def title(*args)
523 strings = args.map do |arg|
523 strings = args.map do |arg|
524 if arg.is_a?(Array) && arg.size >= 2
524 if arg.is_a?(Array) && arg.size >= 2
525 link_to(*arg)
525 link_to(*arg)
526 else
526 else
527 h(arg.to_s)
527 h(arg.to_s)
528 end
528 end
529 end
529 end
530 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
530 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
531 content_tag('h2', strings.join(' &#187; ').html_safe)
531 content_tag('h2', strings.join(' &#187; ').html_safe)
532 end
532 end
533
533
534 # Sets the html title
534 # Sets the html title
535 # Returns the html title when called without arguments
535 # Returns the html title when called without arguments
536 # Current project name and app_title and automatically appended
536 # Current project name and app_title and automatically appended
537 # Exemples:
537 # Exemples:
538 # html_title 'Foo', 'Bar'
538 # html_title 'Foo', 'Bar'
539 # html_title # => 'Foo - Bar - My Project - Redmine'
539 # html_title # => 'Foo - Bar - My Project - Redmine'
540 def html_title(*args)
540 def html_title(*args)
541 if args.empty?
541 if args.empty?
542 title = @html_title || []
542 title = @html_title || []
543 title << @project.name if @project
543 title << @project.name if @project
544 title << Setting.app_title unless Setting.app_title == title.last
544 title << Setting.app_title unless Setting.app_title == title.last
545 title.reject(&:blank?).join(' - ')
545 title.reject(&:blank?).join(' - ')
546 else
546 else
547 @html_title ||= []
547 @html_title ||= []
548 @html_title += args
548 @html_title += args
549 end
549 end
550 end
550 end
551
551
552 # Returns the theme, controller name, and action as css classes for the
552 # Returns the theme, controller name, and action as css classes for the
553 # HTML body.
553 # HTML body.
554 def body_css_classes
554 def body_css_classes
555 css = []
555 css = []
556 if theme = Redmine::Themes.theme(Setting.ui_theme)
556 if theme = Redmine::Themes.theme(Setting.ui_theme)
557 css << 'theme-' + theme.name
557 css << 'theme-' + theme.name
558 end
558 end
559
559
560 css << 'project-' + @project.identifier if @project && @project.identifier.present?
560 css << 'project-' + @project.identifier if @project && @project.identifier.present?
561 css << 'controller-' + controller_name
561 css << 'controller-' + controller_name
562 css << 'action-' + action_name
562 css << 'action-' + action_name
563 css.join(' ')
563 css.join(' ')
564 end
564 end
565
565
566 def accesskey(s)
566 def accesskey(s)
567 @used_accesskeys ||= []
567 @used_accesskeys ||= []
568 key = Redmine::AccessKeys.key_for(s)
568 key = Redmine::AccessKeys.key_for(s)
569 return nil if @used_accesskeys.include?(key)
569 return nil if @used_accesskeys.include?(key)
570 @used_accesskeys << key
570 @used_accesskeys << key
571 key
571 key
572 end
572 end
573
573
574 # Formats text according to system settings.
574 # Formats text according to system settings.
575 # 2 ways to call this method:
575 # 2 ways to call this method:
576 # * with a String: textilizable(text, options)
576 # * with a String: textilizable(text, options)
577 # * with an object and one of its attribute: textilizable(issue, :description, options)
577 # * with an object and one of its attribute: textilizable(issue, :description, options)
578 def textilizable(*args)
578 def textilizable(*args)
579 options = args.last.is_a?(Hash) ? args.pop : {}
579 options = args.last.is_a?(Hash) ? args.pop : {}
580 case args.size
580 case args.size
581 when 1
581 when 1
582 obj = options[:object]
582 obj = options[:object]
583 text = args.shift
583 text = args.shift
584 when 2
584 when 2
585 obj = args.shift
585 obj = args.shift
586 attr = args.shift
586 attr = args.shift
587 text = obj.send(attr).to_s
587 text = obj.send(attr).to_s
588 else
588 else
589 raise ArgumentError, 'invalid arguments to textilizable'
589 raise ArgumentError, 'invalid arguments to textilizable'
590 end
590 end
591 return '' if text.blank?
591 return '' if text.blank?
592 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
592 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
593 @only_path = only_path = options.delete(:only_path) == false ? false : true
593 @only_path = only_path = options.delete(:only_path) == false ? false : true
594
594
595 text = text.dup
595 text = text.dup
596 macros = catch_macros(text)
596 macros = catch_macros(text)
597 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
597 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
598
598
599 @parsed_headings = []
599 @parsed_headings = []
600 @heading_anchors = {}
600 @heading_anchors = {}
601 @current_section = 0 if options[:edit_section_links]
601 @current_section = 0 if options[:edit_section_links]
602
602
603 parse_sections(text, project, obj, attr, only_path, options)
603 parse_sections(text, project, obj, attr, only_path, options)
604 text = parse_non_pre_blocks(text, obj, macros) do |text|
604 text = parse_non_pre_blocks(text, obj, macros) do |text|
605 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
605 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
606 send method_name, text, project, obj, attr, only_path, options
606 send method_name, text, project, obj, attr, only_path, options
607 end
607 end
608 end
608 end
609 parse_headings(text, project, obj, attr, only_path, options)
609 parse_headings(text, project, obj, attr, only_path, options)
610
610
611 if @parsed_headings.any?
611 if @parsed_headings.any?
612 replace_toc(text, @parsed_headings)
612 replace_toc(text, @parsed_headings)
613 end
613 end
614
614
615 text.html_safe
615 text.html_safe
616 end
616 end
617
617
618 def parse_non_pre_blocks(text, obj, macros)
618 def parse_non_pre_blocks(text, obj, macros)
619 s = StringScanner.new(text)
619 s = StringScanner.new(text)
620 tags = []
620 tags = []
621 parsed = ''
621 parsed = ''
622 while !s.eos?
622 while !s.eos?
623 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
623 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
624 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
624 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
625 if tags.empty?
625 if tags.empty?
626 yield text
626 yield text
627 inject_macros(text, obj, macros) if macros.any?
627 inject_macros(text, obj, macros) if macros.any?
628 else
628 else
629 inject_macros(text, obj, macros, false) if macros.any?
629 inject_macros(text, obj, macros, false) if macros.any?
630 end
630 end
631 parsed << text
631 parsed << text
632 if tag
632 if tag
633 if closing
633 if closing
634 if tags.last && tags.last.casecmp(tag) == 0
634 if tags.last && tags.last.casecmp(tag) == 0
635 tags.pop
635 tags.pop
636 end
636 end
637 else
637 else
638 tags << tag.downcase
638 tags << tag.downcase
639 end
639 end
640 parsed << full_tag
640 parsed << full_tag
641 end
641 end
642 end
642 end
643 # Close any non closing tags
643 # Close any non closing tags
644 while tag = tags.pop
644 while tag = tags.pop
645 parsed << "</#{tag}>"
645 parsed << "</#{tag}>"
646 end
646 end
647 parsed
647 parsed
648 end
648 end
649
649
650 def parse_inline_attachments(text, project, obj, attr, only_path, options)
650 def parse_inline_attachments(text, project, obj, attr, only_path, options)
651 return if options[:inline_attachments] == false
651 return if options[:inline_attachments] == false
652
652
653 # when using an image link, try to use an attachment, if possible
653 # when using an image link, try to use an attachment, if possible
654 attachments = options[:attachments] || []
654 attachments = options[:attachments] || []
655 attachments += obj.attachments if obj.respond_to?(:attachments)
655 attachments += obj.attachments if obj.respond_to?(:attachments)
656 if attachments.present?
656 if attachments.present?
657 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
657 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
658 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
658 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
659 # search for the picture in attachments
659 # search for the picture in attachments
660 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
660 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
661 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
661 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
662 desc = found.description.to_s.gsub('"', '')
662 desc = found.description.to_s.gsub('"', '')
663 if !desc.blank? && alttext.blank?
663 if !desc.blank? && alttext.blank?
664 alt = " title=\"#{desc}\" alt=\"#{desc}\""
664 alt = " title=\"#{desc}\" alt=\"#{desc}\""
665 end
665 end
666 "src=\"#{image_url}\"#{alt}"
666 "src=\"#{image_url}\"#{alt}"
667 else
667 else
668 m
668 m
669 end
669 end
670 end
670 end
671 end
671 end
672 end
672 end
673
673
674 # Wiki links
674 # Wiki links
675 #
675 #
676 # Examples:
676 # Examples:
677 # [[mypage]]
677 # [[mypage]]
678 # [[mypage|mytext]]
678 # [[mypage|mytext]]
679 # wiki links can refer other project wikis, using project name or identifier:
679 # wiki links can refer other project wikis, using project name or identifier:
680 # [[project:]] -> wiki starting page
680 # [[project:]] -> wiki starting page
681 # [[project:|mytext]]
681 # [[project:|mytext]]
682 # [[project:mypage]]
682 # [[project:mypage]]
683 # [[project:mypage|mytext]]
683 # [[project:mypage|mytext]]
684 def parse_wiki_links(text, project, obj, attr, only_path, options)
684 def parse_wiki_links(text, project, obj, attr, only_path, options)
685 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
685 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
686 link_project = project
686 link_project = project
687 esc, all, page, title = $1, $2, $3, $5
687 esc, all, page, title = $1, $2, $3, $5
688 if esc.nil?
688 if esc.nil?
689 if page =~ /^([^\:]+)\:(.*)$/
689 if page =~ /^([^\:]+)\:(.*)$/
690 identifier, page = $1, $2
690 identifier, page = $1, $2
691 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
691 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
692 title ||= identifier if page.blank?
692 title ||= identifier if page.blank?
693 end
693 end
694
694
695 if link_project && link_project.wiki
695 if link_project && link_project.wiki
696 # extract anchor
696 # extract anchor
697 anchor = nil
697 anchor = nil
698 if page =~ /^(.+?)\#(.+)$/
698 if page =~ /^(.+?)\#(.+)$/
699 page, anchor = $1, $2
699 page, anchor = $1, $2
700 end
700 end
701 anchor = sanitize_anchor_name(anchor) if anchor.present?
701 anchor = sanitize_anchor_name(anchor) if anchor.present?
702 # check if page exists
702 # check if page exists
703 wiki_page = link_project.wiki.find_page(page)
703 wiki_page = link_project.wiki.find_page(page)
704 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
704 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
705 "##{anchor}"
705 "##{anchor}"
706 else
706 else
707 case options[:wiki_links]
707 case options[:wiki_links]
708 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
708 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
709 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
709 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
710 else
710 else
711 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
711 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
712 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
712 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
713 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
713 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
714 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
714 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
715 end
715 end
716 end
716 end
717 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
717 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
718 else
718 else
719 # project or wiki doesn't exist
719 # project or wiki doesn't exist
720 all
720 all
721 end
721 end
722 else
722 else
723 all
723 all
724 end
724 end
725 end
725 end
726 end
726 end
727
727
728 # Redmine links
728 # Redmine links
729 #
729 #
730 # Examples:
730 # Examples:
731 # Issues:
731 # Issues:
732 # #52 -> Link to issue #52
732 # #52 -> Link to issue #52
733 # Changesets:
733 # Changesets:
734 # r52 -> Link to revision 52
734 # r52 -> Link to revision 52
735 # commit:a85130f -> Link to scmid starting with a85130f
735 # commit:a85130f -> Link to scmid starting with a85130f
736 # Documents:
736 # Documents:
737 # document#17 -> Link to document with id 17
737 # document#17 -> Link to document with id 17
738 # document:Greetings -> Link to the document with title "Greetings"
738 # document:Greetings -> Link to the document with title "Greetings"
739 # document:"Some document" -> Link to the document with title "Some document"
739 # document:"Some document" -> Link to the document with title "Some document"
740 # Versions:
740 # Versions:
741 # version#3 -> Link to version with id 3
741 # version#3 -> Link to version with id 3
742 # version:1.0.0 -> Link to version named "1.0.0"
742 # version:1.0.0 -> Link to version named "1.0.0"
743 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
743 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
744 # Attachments:
744 # Attachments:
745 # attachment:file.zip -> Link to the attachment of the current object named file.zip
745 # attachment:file.zip -> Link to the attachment of the current object named file.zip
746 # Source files:
746 # Source files:
747 # source:some/file -> Link to the file located at /some/file in the project's repository
747 # source:some/file -> Link to the file located at /some/file in the project's repository
748 # source:some/file@52 -> Link to the file's revision 52
748 # source:some/file@52 -> Link to the file's revision 52
749 # source:some/file#L120 -> Link to line 120 of the file
749 # source:some/file#L120 -> Link to line 120 of the file
750 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
750 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
751 # export:some/file -> Force the download of the file
751 # export:some/file -> Force the download of the file
752 # Forum messages:
752 # Forum messages:
753 # message#1218 -> Link to message with id 1218
753 # message#1218 -> Link to message with id 1218
754 # Projects:
754 # Projects:
755 # project:someproject -> Link to project named "someproject"
755 # project:someproject -> Link to project named "someproject"
756 # project#3 -> Link to project with id 3
756 # project#3 -> Link to project with id 3
757 #
757 #
758 # Links can refer other objects from other projects, using project identifier:
758 # Links can refer other objects from other projects, using project identifier:
759 # identifier:r52
759 # identifier:r52
760 # identifier:document:"Some document"
760 # identifier:document:"Some document"
761 # identifier:version:1.0.0
761 # identifier:version:1.0.0
762 # identifier:source:some/file
762 # identifier:source:some/file
763 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
763 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
764 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|
764 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|
765 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
765 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
766 if tag_content
766 if tag_content
767 $&
767 $&
768 else
768 else
769 link = nil
769 link = nil
770 project = default_project
770 project = default_project
771 if project_identifier
771 if project_identifier
772 project = Project.visible.find_by_identifier(project_identifier)
772 project = Project.visible.find_by_identifier(project_identifier)
773 end
773 end
774 if esc.nil?
774 if esc.nil?
775 if prefix.nil? && sep == 'r'
775 if prefix.nil? && sep == 'r'
776 if project
776 if project
777 repository = nil
777 repository = nil
778 if repo_identifier
778 if repo_identifier
779 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
779 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
780 else
780 else
781 repository = project.repository
781 repository = project.repository
782 end
782 end
783 # project.changesets.visible raises an SQL error because of a double join on repositories
783 # project.changesets.visible raises an SQL error because of a double join on repositories
784 if repository &&
784 if repository &&
785 (changeset = Changeset.visible.
785 (changeset = Changeset.visible.
786 find_by_repository_id_and_revision(repository.id, identifier))
786 find_by_repository_id_and_revision(repository.id, identifier))
787 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
787 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
788 {:only_path => only_path, :controller => 'repositories',
788 {:only_path => only_path, :controller => 'repositories',
789 :action => 'revision', :id => project,
789 :action => 'revision', :id => project,
790 :repository_id => repository.identifier_param,
790 :repository_id => repository.identifier_param,
791 :rev => changeset.revision},
791 :rev => changeset.revision},
792 :class => 'changeset',
792 :class => 'changeset',
793 :title => truncate_single_line_raw(changeset.comments, 100))
793 :title => truncate_single_line_raw(changeset.comments, 100))
794 end
794 end
795 end
795 end
796 elsif sep == '#'
796 elsif sep == '#'
797 oid = identifier.to_i
797 oid = identifier.to_i
798 case prefix
798 case prefix
799 when nil
799 when nil
800 if oid.to_s == identifier &&
800 if oid.to_s == identifier &&
801 issue = Issue.visible.find_by_id(oid)
801 issue = Issue.visible.find_by_id(oid)
802 anchor = comment_id ? "note-#{comment_id}" : nil
802 anchor = comment_id ? "note-#{comment_id}" : nil
803 link = link_to("##{oid}#{comment_suffix}",
803 link = link_to("##{oid}#{comment_suffix}",
804 issue_url(issue, :only_path => only_path, :anchor => anchor),
804 issue_url(issue, :only_path => only_path, :anchor => anchor),
805 :class => issue.css_classes,
805 :class => issue.css_classes,
806 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
806 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
807 end
807 end
808 when 'document'
808 when 'document'
809 if document = Document.visible.find_by_id(oid)
809 if document = Document.visible.find_by_id(oid)
810 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
810 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
811 end
811 end
812 when 'version'
812 when 'version'
813 if version = Version.visible.find_by_id(oid)
813 if version = Version.visible.find_by_id(oid)
814 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
814 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
815 end
815 end
816 when 'message'
816 when 'message'
817 if message = Message.visible.find_by_id(oid)
817 if message = Message.visible.find_by_id(oid)
818 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
818 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
819 end
819 end
820 when 'forum'
820 when 'forum'
821 if board = Board.visible.find_by_id(oid)
821 if board = Board.visible.find_by_id(oid)
822 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
822 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
823 end
823 end
824 when 'news'
824 when 'news'
825 if news = News.visible.find_by_id(oid)
825 if news = News.visible.find_by_id(oid)
826 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
826 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
827 end
827 end
828 when 'project'
828 when 'project'
829 if p = Project.visible.find_by_id(oid)
829 if p = Project.visible.find_by_id(oid)
830 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
830 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
831 end
831 end
832 end
832 end
833 elsif sep == ':'
833 elsif sep == ':'
834 # removes the double quotes if any
834 # removes the double quotes if any
835 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
835 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
836 name = CGI.unescapeHTML(name)
836 name = CGI.unescapeHTML(name)
837 case prefix
837 case prefix
838 when 'document'
838 when 'document'
839 if project && document = project.documents.visible.find_by_title(name)
839 if project && document = project.documents.visible.find_by_title(name)
840 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
840 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
841 end
841 end
842 when 'version'
842 when 'version'
843 if project && version = project.versions.visible.find_by_name(name)
843 if project && version = project.versions.visible.find_by_name(name)
844 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
844 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
845 end
845 end
846 when 'forum'
846 when 'forum'
847 if project && board = project.boards.visible.find_by_name(name)
847 if project && board = project.boards.visible.find_by_name(name)
848 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
848 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
849 end
849 end
850 when 'news'
850 when 'news'
851 if project && news = project.news.visible.find_by_title(name)
851 if project && news = project.news.visible.find_by_title(name)
852 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
852 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
853 end
853 end
854 when 'commit', 'source', 'export'
854 when 'commit', 'source', 'export'
855 if project
855 if project
856 repository = nil
856 repository = nil
857 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
857 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
858 repo_prefix, repo_identifier, name = $1, $2, $3
858 repo_prefix, repo_identifier, name = $1, $2, $3
859 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
859 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
860 else
860 else
861 repository = project.repository
861 repository = project.repository
862 end
862 end
863 if prefix == 'commit'
863 if prefix == 'commit'
864 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
864 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
865 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},
865 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},
866 :class => 'changeset',
866 :class => 'changeset',
867 :title => truncate_single_line_raw(changeset.comments, 100)
867 :title => truncate_single_line_raw(changeset.comments, 100)
868 end
868 end
869 else
869 else
870 if repository && User.current.allowed_to?(:browse_repository, project)
870 if repository && User.current.allowed_to?(:browse_repository, project)
871 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
871 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
872 path, rev, anchor = $1, $3, $5
872 path, rev, anchor = $1, $3, $5
873 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,
873 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,
874 :path => to_path_param(path),
874 :path => to_path_param(path),
875 :rev => rev,
875 :rev => rev,
876 :anchor => anchor},
876 :anchor => anchor},
877 :class => (prefix == 'export' ? 'source download' : 'source')
877 :class => (prefix == 'export' ? 'source download' : 'source')
878 end
878 end
879 end
879 end
880 repo_prefix = nil
880 repo_prefix = nil
881 end
881 end
882 when 'attachment'
882 when 'attachment'
883 attachments = options[:attachments] || []
883 attachments = options[:attachments] || []
884 attachments += obj.attachments if obj.respond_to?(:attachments)
884 attachments += obj.attachments if obj.respond_to?(:attachments)
885 if attachments && attachment = Attachment.latest_attach(attachments, name)
885 if attachments && attachment = Attachment.latest_attach(attachments, name)
886 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
886 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
887 end
887 end
888 when 'project'
888 when 'project'
889 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
889 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
890 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
890 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
891 end
891 end
892 end
892 end
893 end
893 end
894 end
894 end
895 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
895 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
896 end
896 end
897 end
897 end
898 end
898 end
899
899
900 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
900 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
901
901
902 def parse_sections(text, project, obj, attr, only_path, options)
902 def parse_sections(text, project, obj, attr, only_path, options)
903 return unless options[:edit_section_links]
903 return unless options[:edit_section_links]
904 text.gsub!(HEADING_RE) do
904 text.gsub!(HEADING_RE) do
905 heading, level = $1, $2
905 heading, level = $1, $2
906 @current_section += 1
906 @current_section += 1
907 if @current_section > 1
907 if @current_section > 1
908 content_tag('div',
908 content_tag('div',
909 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
909 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
910 :class => 'icon-only icon-edit'),
910 :class => 'icon-only icon-edit'),
911 :class => "contextual heading-#{level}",
911 :class => "contextual heading-#{level}",
912 :title => l(:button_edit_section),
912 :title => l(:button_edit_section),
913 :id => "section-#{@current_section}") + heading.html_safe
913 :id => "section-#{@current_section}") + heading.html_safe
914 else
914 else
915 heading
915 heading
916 end
916 end
917 end
917 end
918 end
918 end
919
919
920 # Headings and TOC
920 # Headings and TOC
921 # Adds ids and links to headings unless options[:headings] is set to false
921 # Adds ids and links to headings unless options[:headings] is set to false
922 def parse_headings(text, project, obj, attr, only_path, options)
922 def parse_headings(text, project, obj, attr, only_path, options)
923 return if options[:headings] == false
923 return if options[:headings] == false
924
924
925 text.gsub!(HEADING_RE) do
925 text.gsub!(HEADING_RE) do
926 level, attrs, content = $2.to_i, $3, $4
926 level, attrs, content = $2.to_i, $3, $4
927 item = strip_tags(content).strip
927 item = strip_tags(content).strip
928 anchor = sanitize_anchor_name(item)
928 anchor = sanitize_anchor_name(item)
929 # used for single-file wiki export
929 # used for single-file wiki export
930 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
930 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
931 @heading_anchors[anchor] ||= 0
931 @heading_anchors[anchor] ||= 0
932 idx = (@heading_anchors[anchor] += 1)
932 idx = (@heading_anchors[anchor] += 1)
933 if idx > 1
933 if idx > 1
934 anchor = "#{anchor}-#{idx}"
934 anchor = "#{anchor}-#{idx}"
935 end
935 end
936 @parsed_headings << [level, anchor, item]
936 @parsed_headings << [level, anchor, item]
937 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
937 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
938 end
938 end
939 end
939 end
940
940
941 MACROS_RE = /(
941 MACROS_RE = /(
942 (!)? # escaping
942 (!)? # escaping
943 (
943 (
944 \{\{ # opening tag
944 \{\{ # opening tag
945 ([\w]+) # macro name
945 ([\w]+) # macro name
946 (\(([^\n\r]*?)\))? # optional arguments
946 (\(([^\n\r]*?)\))? # optional arguments
947 ([\n\r].*?[\n\r])? # optional block of text
947 ([\n\r].*?[\n\r])? # optional block of text
948 \}\} # closing tag
948 \}\} # closing tag
949 )
949 )
950 )/mx unless const_defined?(:MACROS_RE)
950 )/mx unless const_defined?(:MACROS_RE)
951
951
952 MACRO_SUB_RE = /(
952 MACRO_SUB_RE = /(
953 \{\{
953 \{\{
954 macro\((\d+)\)
954 macro\((\d+)\)
955 \}\}
955 \}\}
956 )/x unless const_defined?(:MACRO_SUB_RE)
956 )/x unless const_defined?(:MACRO_SUB_RE)
957
957
958 # Extracts macros from text
958 # Extracts macros from text
959 def catch_macros(text)
959 def catch_macros(text)
960 macros = {}
960 macros = {}
961 text.gsub!(MACROS_RE) do
961 text.gsub!(MACROS_RE) do
962 all, macro = $1, $4.downcase
962 all, macro = $1, $4.downcase
963 if macro_exists?(macro) || all =~ MACRO_SUB_RE
963 if macro_exists?(macro) || all =~ MACRO_SUB_RE
964 index = macros.size
964 index = macros.size
965 macros[index] = all
965 macros[index] = all
966 "{{macro(#{index})}}"
966 "{{macro(#{index})}}"
967 else
967 else
968 all
968 all
969 end
969 end
970 end
970 end
971 macros
971 macros
972 end
972 end
973
973
974 # Executes and replaces macros in text
974 # Executes and replaces macros in text
975 def inject_macros(text, obj, macros, execute=true)
975 def inject_macros(text, obj, macros, execute=true)
976 text.gsub!(MACRO_SUB_RE) do
976 text.gsub!(MACRO_SUB_RE) do
977 all, index = $1, $2.to_i
977 all, index = $1, $2.to_i
978 orig = macros.delete(index)
978 orig = macros.delete(index)
979 if execute && orig && orig =~ MACROS_RE
979 if execute && orig && orig =~ MACROS_RE
980 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
980 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
981 if esc.nil?
981 if esc.nil?
982 h(exec_macro(macro, obj, args, block) || all)
982 h(exec_macro(macro, obj, args, block) || all)
983 else
983 else
984 h(all)
984 h(all)
985 end
985 end
986 elsif orig
986 elsif orig
987 h(orig)
987 h(orig)
988 else
988 else
989 h(all)
989 h(all)
990 end
990 end
991 end
991 end
992 end
992 end
993
993
994 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
994 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
995
995
996 # Renders the TOC with given headings
996 # Renders the TOC with given headings
997 def replace_toc(text, headings)
997 def replace_toc(text, headings)
998 text.gsub!(TOC_RE) do
998 text.gsub!(TOC_RE) do
999 left_align, right_align = $2, $3
999 left_align, right_align = $2, $3
1000 # Keep only the 4 first levels
1000 # Keep only the 4 first levels
1001 headings = headings.select{|level, anchor, item| level <= 4}
1001 headings = headings.select{|level, anchor, item| level <= 4}
1002 if headings.empty?
1002 if headings.empty?
1003 ''
1003 ''
1004 else
1004 else
1005 div_class = 'toc'
1005 div_class = 'toc'
1006 div_class << ' right' if right_align
1006 div_class << ' right' if right_align
1007 div_class << ' left' if left_align
1007 div_class << ' left' if left_align
1008 out = "<ul class=\"#{div_class}\"><li>"
1008 out = "<ul class=\"#{div_class}\"><li>"
1009 root = headings.map(&:first).min
1009 root = headings.map(&:first).min
1010 current = root
1010 current = root
1011 started = false
1011 started = false
1012 headings.each do |level, anchor, item|
1012 headings.each do |level, anchor, item|
1013 if level > current
1013 if level > current
1014 out << '<ul><li>' * (level - current)
1014 out << '<ul><li>' * (level - current)
1015 elsif level < current
1015 elsif level < current
1016 out << "</li></ul>\n" * (current - level) + "</li><li>"
1016 out << "</li></ul>\n" * (current - level) + "</li><li>"
1017 elsif started
1017 elsif started
1018 out << '</li><li>'
1018 out << '</li><li>'
1019 end
1019 end
1020 out << "<a href=\"##{anchor}\">#{item}</a>"
1020 out << "<a href=\"##{anchor}\">#{item}</a>"
1021 current = level
1021 current = level
1022 started = true
1022 started = true
1023 end
1023 end
1024 out << '</li></ul>' * (current - root)
1024 out << '</li></ul>' * (current - root)
1025 out << '</li></ul>'
1025 out << '</li></ul>'
1026 end
1026 end
1027 end
1027 end
1028 end
1028 end
1029
1029
1030 # Same as Rails' simple_format helper without using paragraphs
1030 # Same as Rails' simple_format helper without using paragraphs
1031 def simple_format_without_paragraph(text)
1031 def simple_format_without_paragraph(text)
1032 text.to_s.
1032 text.to_s.
1033 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1033 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1034 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1034 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1035 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1035 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1036 html_safe
1036 html_safe
1037 end
1037 end
1038
1038
1039 def lang_options_for_select(blank=true)
1039 def lang_options_for_select(blank=true)
1040 (blank ? [["(auto)", ""]] : []) + languages_options
1040 (blank ? [["(auto)", ""]] : []) + languages_options
1041 end
1041 end
1042
1042
1043 def labelled_form_for(*args, &proc)
1043 def labelled_form_for(*args, &proc)
1044 args << {} unless args.last.is_a?(Hash)
1044 args << {} unless args.last.is_a?(Hash)
1045 options = args.last
1045 options = args.last
1046 if args.first.is_a?(Symbol)
1046 if args.first.is_a?(Symbol)
1047 options.merge!(:as => args.shift)
1047 options.merge!(:as => args.shift)
1048 end
1048 end
1049 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1049 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1050 form_for(*args, &proc)
1050 form_for(*args, &proc)
1051 end
1051 end
1052
1052
1053 def labelled_fields_for(*args, &proc)
1053 def labelled_fields_for(*args, &proc)
1054 args << {} unless args.last.is_a?(Hash)
1054 args << {} unless args.last.is_a?(Hash)
1055 options = args.last
1055 options = args.last
1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1057 fields_for(*args, &proc)
1057 fields_for(*args, &proc)
1058 end
1058 end
1059
1059
1060 # Render the error messages for the given objects
1060 # Render the error messages for the given objects
1061 def error_messages_for(*objects)
1061 def error_messages_for(*objects)
1062 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1062 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1063 errors = objects.map {|o| o.errors.full_messages}.flatten
1063 errors = objects.map {|o| o.errors.full_messages}.flatten
1064 render_error_messages(errors)
1064 render_error_messages(errors)
1065 end
1065 end
1066
1066
1067 # Renders a list of error messages
1067 # Renders a list of error messages
1068 def render_error_messages(errors)
1068 def render_error_messages(errors)
1069 html = ""
1069 html = ""
1070 if errors.present?
1070 if errors.present?
1071 html << "<div id='errorExplanation'><ul>\n"
1071 html << "<div id='errorExplanation'><ul>\n"
1072 errors.each do |error|
1072 errors.each do |error|
1073 html << "<li>#{h error}</li>\n"
1073 html << "<li>#{h error}</li>\n"
1074 end
1074 end
1075 html << "</ul></div>\n"
1075 html << "</ul></div>\n"
1076 end
1076 end
1077 html.html_safe
1077 html.html_safe
1078 end
1078 end
1079
1079
1080 def delete_link(url, options={})
1080 def delete_link(url, options={})
1081 options = {
1081 options = {
1082 :method => :delete,
1082 :method => :delete,
1083 :data => {:confirm => l(:text_are_you_sure)},
1083 :data => {:confirm => l(:text_are_you_sure)},
1084 :class => 'icon icon-del'
1084 :class => 'icon icon-del'
1085 }.merge(options)
1085 }.merge(options)
1086
1086
1087 link_to l(:button_delete), url, options
1087 link_to l(:button_delete), url, options
1088 end
1088 end
1089
1089
1090 def preview_link(url, form, target='preview', options={})
1090 def preview_link(url, form, target='preview', options={})
1091 content_tag 'a', l(:label_preview), {
1091 content_tag 'a', l(:label_preview), {
1092 :href => "#",
1092 :href => "#",
1093 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1093 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1094 :accesskey => accesskey(:preview)
1094 :accesskey => accesskey(:preview)
1095 }.merge(options)
1095 }.merge(options)
1096 end
1096 end
1097
1097
1098 def link_to_function(name, function, html_options={})
1098 def link_to_function(name, function, html_options={})
1099 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1099 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1100 end
1100 end
1101
1101
1102 # Helper to render JSON in views
1102 # Helper to render JSON in views
1103 def raw_json(arg)
1103 def raw_json(arg)
1104 arg.to_json.to_s.gsub('/', '\/').html_safe
1104 arg.to_json.to_s.gsub('/', '\/').html_safe
1105 end
1105 end
1106
1106
1107 def back_url
1107 def back_url
1108 url = params[:back_url]
1108 url = params[:back_url]
1109 if url.nil? && referer = request.env['HTTP_REFERER']
1109 if url.nil? && referer = request.env['HTTP_REFERER']
1110 url = CGI.unescape(referer.to_s)
1110 url = CGI.unescape(referer.to_s)
1111 end
1111 end
1112 url
1112 url
1113 end
1113 end
1114
1114
1115 def back_url_hidden_field_tag
1115 def back_url_hidden_field_tag
1116 url = back_url
1116 url = back_url
1117 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1117 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1118 end
1118 end
1119
1119
1120 def check_all_links(form_name)
1120 def check_all_links(form_name)
1121 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1121 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1122 " | ".html_safe +
1122 " | ".html_safe +
1123 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1123 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1124 end
1124 end
1125
1125
1126 def toggle_checkboxes_link(selector)
1126 def toggle_checkboxes_link(selector)
1127 link_to_function '',
1127 link_to_function '',
1128 "toggleCheckboxesBySelector('#{selector}')",
1128 "toggleCheckboxesBySelector('#{selector}')",
1129 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1129 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1130 :class => 'toggle-checkboxes'
1130 :class => 'toggle-checkboxes'
1131 end
1131 end
1132
1132
1133 def progress_bar(pcts, options={})
1133 def progress_bar(pcts, options={})
1134 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1134 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1135 pcts = pcts.collect(&:round)
1135 pcts = pcts.collect(&:round)
1136 pcts[1] = pcts[1] - pcts[0]
1136 pcts[1] = pcts[1] - pcts[0]
1137 pcts << (100 - pcts[1] - pcts[0])
1137 pcts << (100 - pcts[1] - pcts[0])
1138 titles = options[:titles].to_a
1138 titles = options[:titles].to_a
1139 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1139 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1140 legend = options[:legend] || ''
1140 legend = options[:legend] || ''
1141 content_tag('table',
1141 content_tag('table',
1142 content_tag('tr',
1142 content_tag('tr',
1143 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1143 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1144 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1144 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1145 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1145 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1146 ), :class => "progress progress-#{pcts[0]}").html_safe +
1146 ), :class => "progress progress-#{pcts[0]}").html_safe +
1147 content_tag('p', legend, :class => 'percent').html_safe
1147 content_tag('p', legend, :class => 'percent').html_safe
1148 end
1148 end
1149
1149
1150 def checked_image(checked=true)
1150 def checked_image(checked=true)
1151 if checked
1151 if checked
1152 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1152 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1153 end
1153 end
1154 end
1154 end
1155
1155
1156 def context_menu(url)
1156 def context_menu(url)
1157 unless @context_menu_included
1157 unless @context_menu_included
1158 content_for :header_tags do
1158 content_for :header_tags do
1159 javascript_include_tag('context_menu') +
1159 javascript_include_tag('context_menu') +
1160 stylesheet_link_tag('context_menu')
1160 stylesheet_link_tag('context_menu')
1161 end
1161 end
1162 if l(:direction) == 'rtl'
1162 if l(:direction) == 'rtl'
1163 content_for :header_tags do
1163 content_for :header_tags do
1164 stylesheet_link_tag('context_menu_rtl')
1164 stylesheet_link_tag('context_menu_rtl')
1165 end
1165 end
1166 end
1166 end
1167 @context_menu_included = true
1167 @context_menu_included = true
1168 end
1168 end
1169 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1169 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1170 end
1170 end
1171
1171
1172 def calendar_for(field_id)
1172 def calendar_for(field_id)
1173 include_calendar_headers_tags
1173 include_calendar_headers_tags
1174 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1174 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1175 end
1175 end
1176
1176
1177 def include_calendar_headers_tags
1177 def include_calendar_headers_tags
1178 unless @calendar_headers_tags_included
1178 unless @calendar_headers_tags_included
1179 tags = ''.html_safe
1179 tags = ''.html_safe
1180 @calendar_headers_tags_included = true
1180 @calendar_headers_tags_included = true
1181 content_for :header_tags do
1181 content_for :header_tags do
1182 start_of_week = Setting.start_of_week
1182 start_of_week = Setting.start_of_week
1183 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1183 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1184 # Redmine uses 1..7 (monday..sunday) in settings and locales
1184 # Redmine uses 1..7 (monday..sunday) in settings and locales
1185 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1185 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1186 start_of_week = start_of_week.to_i % 7
1186 start_of_week = start_of_week.to_i % 7
1187 tags << javascript_tag(
1187 tags << javascript_tag(
1188 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1188 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1189 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1189 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1190 path_to_image('/images/calendar.png') +
1190 path_to_image('/images/calendar.png') +
1191 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1191 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1192 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1192 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1193 "beforeShow: beforeShowDatePicker};")
1193 "beforeShow: beforeShowDatePicker};")
1194 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1194 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1195 unless jquery_locale == 'en'
1195 unless jquery_locale == 'en'
1196 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1196 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1197 end
1197 end
1198 tags
1198 tags
1199 end
1199 end
1200 end
1200 end
1201 end
1201 end
1202
1202
1203 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1203 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1204 # Examples:
1204 # Examples:
1205 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1205 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1206 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1206 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1207 #
1207 #
1208 def stylesheet_link_tag(*sources)
1208 def stylesheet_link_tag(*sources)
1209 options = sources.last.is_a?(Hash) ? sources.pop : {}
1209 options = sources.last.is_a?(Hash) ? sources.pop : {}
1210 plugin = options.delete(:plugin)
1210 plugin = options.delete(:plugin)
1211 sources = sources.map do |source|
1211 sources = sources.map do |source|
1212 if plugin
1212 if plugin
1213 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1213 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1214 elsif current_theme && current_theme.stylesheets.include?(source)
1214 elsif current_theme && current_theme.stylesheets.include?(source)
1215 current_theme.stylesheet_path(source)
1215 current_theme.stylesheet_path(source)
1216 else
1216 else
1217 source
1217 source
1218 end
1218 end
1219 end
1219 end
1220 super *sources, options
1220 super *sources, options
1221 end
1221 end
1222
1222
1223 # Overrides Rails' image_tag with themes and plugins support.
1223 # Overrides Rails' image_tag with themes and plugins support.
1224 # Examples:
1224 # Examples:
1225 # image_tag('image.png') # => picks image.png from the current theme or defaults
1225 # image_tag('image.png') # => picks image.png from the current theme or defaults
1226 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1226 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1227 #
1227 #
1228 def image_tag(source, options={})
1228 def image_tag(source, options={})
1229 if plugin = options.delete(:plugin)
1229 if plugin = options.delete(:plugin)
1230 source = "/plugin_assets/#{plugin}/images/#{source}"
1230 source = "/plugin_assets/#{plugin}/images/#{source}"
1231 elsif current_theme && current_theme.images.include?(source)
1231 elsif current_theme && current_theme.images.include?(source)
1232 source = current_theme.image_path(source)
1232 source = current_theme.image_path(source)
1233 end
1233 end
1234 super source, options
1234 super source, options
1235 end
1235 end
1236
1236
1237 # Overrides Rails' javascript_include_tag with plugins support
1237 # Overrides Rails' javascript_include_tag with plugins support
1238 # Examples:
1238 # Examples:
1239 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1239 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1240 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1240 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1241 #
1241 #
1242 def javascript_include_tag(*sources)
1242 def javascript_include_tag(*sources)
1243 options = sources.last.is_a?(Hash) ? sources.pop : {}
1243 options = sources.last.is_a?(Hash) ? sources.pop : {}
1244 if plugin = options.delete(:plugin)
1244 if plugin = options.delete(:plugin)
1245 sources = sources.map do |source|
1245 sources = sources.map do |source|
1246 if plugin
1246 if plugin
1247 "/plugin_assets/#{plugin}/javascripts/#{source}"
1247 "/plugin_assets/#{plugin}/javascripts/#{source}"
1248 else
1248 else
1249 source
1249 source
1250 end
1250 end
1251 end
1251 end
1252 end
1252 end
1253 super *sources, options
1253 super *sources, options
1254 end
1254 end
1255
1255
1256 def sidebar_content?
1256 def sidebar_content?
1257 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1257 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1258 end
1258 end
1259
1259
1260 def view_layouts_base_sidebar_hook_response
1260 def view_layouts_base_sidebar_hook_response
1261 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1261 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1262 end
1262 end
1263
1263
1264 def email_delivery_enabled?
1264 def email_delivery_enabled?
1265 !!ActionMailer::Base.perform_deliveries
1265 !!ActionMailer::Base.perform_deliveries
1266 end
1266 end
1267
1267
1268 # Returns the avatar image tag for the given +user+ if avatars are enabled
1268 # Returns the avatar image tag for the given +user+ if avatars are enabled
1269 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1269 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1270 def avatar(user, options = { })
1270 def avatar(user, options = { })
1271 if Setting.gravatar_enabled?
1271 if Setting.gravatar_enabled?
1272 options.merge!(:default => Setting.gravatar_default)
1272 options.merge!(:default => Setting.gravatar_default)
1273 email = nil
1273 email = nil
1274 if user.respond_to?(:mail)
1274 if user.respond_to?(:mail)
1275 email = user.mail
1275 email = user.mail
1276 elsif user.to_s =~ %r{<(.+?)>}
1276 elsif user.to_s =~ %r{<(.+?)>}
1277 email = $1
1277 email = $1
1278 end
1278 end
1279 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1279 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1280 else
1280 else
1281 ''
1281 ''
1282 end
1282 end
1283 end
1283 end
1284
1284
1285 # Returns a link to edit user's avatar if avatars are enabled
1285 # Returns a link to edit user's avatar if avatars are enabled
1286 def avatar_edit_link(user, options={})
1286 def avatar_edit_link(user, options={})
1287 if Setting.gravatar_enabled?
1287 if Setting.gravatar_enabled?
1288 url = "https://gravatar.com"
1288 url = "https://gravatar.com"
1289 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1289 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1290 end
1290 end
1291 end
1291 end
1292
1292
1293 def sanitize_anchor_name(anchor)
1293 def sanitize_anchor_name(anchor)
1294 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1294 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1295 end
1295 end
1296
1296
1297 # Returns the javascript tags that are included in the html layout head
1297 # Returns the javascript tags that are included in the html layout head
1298 def javascript_heads
1298 def javascript_heads
1299 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1299 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1300 unless User.current.pref.warn_on_leaving_unsaved == '0'
1300 unless User.current.pref.warn_on_leaving_unsaved == '0'
1301 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1301 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1302 end
1302 end
1303 tags
1303 tags
1304 end
1304 end
1305
1305
1306 def favicon
1306 def favicon
1307 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1307 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1308 end
1308 end
1309
1309
1310 # Returns the path to the favicon
1310 # Returns the path to the favicon
1311 def favicon_path
1311 def favicon_path
1312 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1312 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1313 image_path(icon)
1313 image_path(icon)
1314 end
1314 end
1315
1315
1316 # Returns the full URL to the favicon
1316 # Returns the full URL to the favicon
1317 def favicon_url
1317 def favicon_url
1318 # TODO: use #image_url introduced in Rails4
1318 # TODO: use #image_url introduced in Rails4
1319 path = favicon_path
1319 path = favicon_path
1320 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1320 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1321 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1321 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1322 end
1322 end
1323
1323
1324 def robot_exclusion_tag
1324 def robot_exclusion_tag
1325 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1325 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1326 end
1326 end
1327
1327
1328 # Returns true if arg is expected in the API response
1328 # Returns true if arg is expected in the API response
1329 def include_in_api_response?(arg)
1329 def include_in_api_response?(arg)
1330 unless @included_in_api_response
1330 unless @included_in_api_response
1331 param = params[:include]
1331 param = params[:include]
1332 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1332 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1333 @included_in_api_response.collect!(&:strip)
1333 @included_in_api_response.collect!(&:strip)
1334 end
1334 end
1335 @included_in_api_response.include?(arg.to_s)
1335 @included_in_api_response.include?(arg.to_s)
1336 end
1336 end
1337
1337
1338 # Returns options or nil if nometa param or X-Redmine-Nometa header
1338 # Returns options or nil if nometa param or X-Redmine-Nometa header
1339 # was set in the request
1339 # was set in the request
1340 def api_meta(options)
1340 def api_meta(options)
1341 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1341 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1342 # compatibility mode for activeresource clients that raise
1342 # compatibility mode for activeresource clients that raise
1343 # an error when deserializing an array with attributes
1343 # an error when deserializing an array with attributes
1344 nil
1344 nil
1345 else
1345 else
1346 options
1346 options
1347 end
1347 end
1348 end
1348 end
1349
1349
1350 def generate_csv(&block)
1350 def generate_csv(&block)
1351 decimal_separator = l(:general_csv_decimal_separator)
1351 decimal_separator = l(:general_csv_decimal_separator)
1352 encoding = l(:general_csv_encoding)
1352 encoding = l(:general_csv_encoding)
1353 end
1353 end
1354
1354
1355 private
1355 private
1356
1356
1357 def wiki_helper
1357 def wiki_helper
1358 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1358 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1359 extend helper
1359 extend helper
1360 return self
1360 return self
1361 end
1361 end
1362
1362
1363 def link_to_content_update(text, url_params = {}, html_options = {})
1363 def link_to_content_update(text, url_params = {}, html_options = {})
1364 link_to(text, url_params, html_options)
1364 link_to(text, url_params, html_options)
1365 end
1365 end
1366 end
1366 end
@@ -1,3 +1,3
1 <p><%= f.text_field(:default_value, :size => 10) %></p>
1 <p><%= f.date_field(:default_value, :value => @custom_field.default_value, :size => 10) %></p>
2 <%= calendar_for('custom_field_default_value') %>
2 <%= calendar_for('custom_field_default_value') %>
3 <p><%= f.text_field :url_pattern, :size => 50, :label => :label_link_values_to %></p>
3 <p><%= f.text_field :url_pattern, :size => 50, :label => :label_link_values_to %></p>
@@ -1,83 +1,83
1 <%= labelled_fields_for :issue, @issue do |f| %>
1 <%= labelled_fields_for :issue, @issue do |f| %>
2
2
3 <div class="splitcontent">
3 <div class="splitcontent">
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
5 <% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %>
5 <% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %>
6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true},
6 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true},
7 :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %></p>
7 :onchange => "updateIssueFrom('#{escape_javascript update_issue_form_path(@project, @issue)}', this)" %></p>
8 <%= hidden_field_tag 'was_default_status', @issue.status_id, :id => nil if @issue.status == @issue.default_status %>
8 <%= hidden_field_tag 'was_default_status', @issue.status_id, :id => nil if @issue.status == @issue.default_status %>
9 <% else %>
9 <% else %>
10 <p><label><%= l(:field_status) %></label> <%= @issue.status %></p>
10 <p><label><%= l(:field_status) %></label> <%= @issue.status %></p>
11 <% end %>
11 <% end %>
12
12
13 <% if @issue.safe_attribute? 'priority_id' %>
13 <% if @issue.safe_attribute? 'priority_id' %>
14 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true} %></p>
14 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true} %></p>
15 <% end %>
15 <% end %>
16
16
17 <% if @issue.safe_attribute? 'assigned_to_id' %>
17 <% if @issue.safe_attribute? 'assigned_to_id' %>
18 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %></p>
18 <p><%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %></p>
19 <% end %>
19 <% end %>
20
20
21 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
21 <% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %>
22 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %>
22 <p><%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %>
23 <%= link_to(l(:label_issue_category_new),
23 <%= link_to(l(:label_issue_category_new),
24 new_project_issue_category_path(@issue.project),
24 new_project_issue_category_path(@issue.project),
25 :remote => true,
25 :remote => true,
26 :method => 'get',
26 :method => 'get',
27 :title => l(:label_issue_category_new),
27 :title => l(:label_issue_category_new),
28 :tabindex => 200,
28 :tabindex => 200,
29 :class => 'icon-only icon-add'
29 :class => 'icon-only icon-add'
30 ) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
30 ) if User.current.allowed_to?(:manage_categories, @issue.project) %></p>
31 <% end %>
31 <% end %>
32
32
33 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
33 <% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %>
34 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %>
34 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %>
35 <%= link_to(l(:label_version_new),
35 <%= link_to(l(:label_version_new),
36 new_project_version_path(@issue.project),
36 new_project_version_path(@issue.project),
37 :remote => true,
37 :remote => true,
38 :method => 'get',
38 :method => 'get',
39 :title => l(:label_version_new),
39 :title => l(:label_version_new),
40 :tabindex => 200,
40 :tabindex => 200,
41 :class => 'icon-only icon-add'
41 :class => 'icon-only icon-add'
42 ) if User.current.allowed_to?(:manage_versions, @issue.project) %>
42 ) if User.current.allowed_to?(:manage_versions, @issue.project) %>
43 </p>
43 </p>
44 <% end %>
44 <% end %>
45 </div>
45 </div>
46
46
47 <div class="splitcontentright">
47 <div class="splitcontentright">
48 <% if @issue.safe_attribute? 'parent_issue_id' %>
48 <% if @issue.safe_attribute? 'parent_issue_id' %>
49 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
49 <p id="parent_issue"><%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %></p>
50 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project, :scope => Setting.cross_project_subtasks)}')" %>
50 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @issue.project, :scope => Setting.cross_project_subtasks)}')" %>
51 <% end %>
51 <% end %>
52
52
53 <% if @issue.safe_attribute? 'start_date' %>
53 <% if @issue.safe_attribute? 'start_date' %>
54 <p id="start_date_area">
54 <p id="start_date_area">
55 <%= f.text_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
55 <%= f.date_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %>
56 <%= calendar_for('issue_start_date') %>
56 <%= calendar_for('issue_start_date') %>
57 </p>
57 </p>
58 <% end %>
58 <% end %>
59
59
60 <% if @issue.safe_attribute? 'due_date' %>
60 <% if @issue.safe_attribute? 'due_date' %>
61 <p id="due_date_area">
61 <p id="due_date_area">
62 <%= f.text_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
62 <%= f.date_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %>
63 <%= calendar_for('issue_due_date') %>
63 <%= calendar_for('issue_due_date') %>
64 </p>
64 </p>
65 <% end %>
65 <% end %>
66
66
67 <% if @issue.safe_attribute? 'estimated_hours' %>
67 <% if @issue.safe_attribute? 'estimated_hours' %>
68 <p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
68 <p><%= f.text_field :estimated_hours, :size => 3, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %></p>
69 <% end %>
69 <% end %>
70
70
71 <% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
71 <% if @issue.safe_attribute?('done_ratio') && Issue.use_field_for_done_ratio? %>
72 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
72 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %></p>
73 <% end %>
73 <% end %>
74 </div>
74 </div>
75 </div>
75 </div>
76
76
77 <% if @issue.safe_attribute? 'custom_field_values' %>
77 <% if @issue.safe_attribute? 'custom_field_values' %>
78 <%= render :partial => 'issues/form_custom_fields' %>
78 <%= render :partial => 'issues/form_custom_fields' %>
79 <% end %>
79 <% end %>
80
80
81 <% end %>
81 <% end %>
82
82
83 <% include_calendar_headers_tags %>
83 <% include_calendar_headers_tags %>
@@ -1,215 +1,215
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
1 <h2><%= @copy ? l(:button_copy) : l(:label_bulk_edit_selected_issues) %></h2>
2
2
3 <% if @saved_issues && @unsaved_issues.present? %>
3 <% if @saved_issues && @unsaved_issues.present? %>
4 <div id="errorExplanation">
4 <div id="errorExplanation">
5 <span>
5 <span>
6 <%= l(:notice_failed_to_save_issues,
6 <%= l(:notice_failed_to_save_issues,
7 :count => @unsaved_issues.size,
7 :count => @unsaved_issues.size,
8 :total => @saved_issues.size,
8 :total => @saved_issues.size,
9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
9 :ids => @unsaved_issues.map {|i| "##{i.id}"}.join(', ')) %>
10 </span>
10 </span>
11 <ul>
11 <ul>
12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
12 <% bulk_edit_error_messages(@unsaved_issues).each do |message| %>
13 <li><%= message %></li>
13 <li><%= message %></li>
14 <% end %>
14 <% end %>
15 </ul>
15 </ul>
16 </div>
16 </div>
17 <% end %>
17 <% end %>
18
18
19 <ul id="bulk-selection">
19 <ul id="bulk-selection">
20 <% @issues.each do |issue| %>
20 <% @issues.each do |issue| %>
21 <%= content_tag 'li', link_to_issue(issue) %>
21 <%= content_tag 'li', link_to_issue(issue) %>
22 <% end %>
22 <% end %>
23 </ul>
23 </ul>
24
24
25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
25 <%= form_tag(bulk_update_issues_path, :id => 'bulk_edit_form') do %>
26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join("\n").html_safe %>
26 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join("\n").html_safe %>
27 <div class="box tabular">
27 <div class="box tabular">
28 <fieldset class="attributes">
28 <fieldset class="attributes">
29 <legend><%= l(:label_change_properties) %></legend>
29 <legend><%= l(:label_change_properties) %></legend>
30
30
31 <div class="splitcontentleft">
31 <div class="splitcontentleft">
32 <% if @allowed_projects.present? %>
32 <% if @allowed_projects.present? %>
33 <p>
33 <p>
34 <label for="issue_project_id"><%= l(:field_project) %></label>
34 <label for="issue_project_id"><%= l(:field_project) %></label>
35 <%= select_tag('issue[project_id]',
35 <%= select_tag('issue[project_id]',
36 project_tree_options_for_select(@allowed_projects,
36 project_tree_options_for_select(@allowed_projects,
37 :include_blank => ((!@copy || (@projects & @allowed_projects == @projects)) ? l(:label_no_change_option) : false),
37 :include_blank => ((!@copy || (@projects & @allowed_projects == @projects)) ? l(:label_no_change_option) : false),
38 :selected => @target_project),
38 :selected => @target_project),
39 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
39 :onchange => "updateBulkEditFrom('#{escape_javascript url_for(:action => 'bulk_edit', :format => 'js')}')") %>
40 </p>
40 </p>
41 <% end %>
41 <% end %>
42 <p>
42 <p>
43 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
43 <label for="issue_tracker_id"><%= l(:field_tracker) %></label>
44 <%= select_tag('issue[tracker_id]',
44 <%= select_tag('issue[tracker_id]',
45 content_tag('option', l(:label_no_change_option), :value => '') +
45 content_tag('option', l(:label_no_change_option), :value => '') +
46 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
46 options_from_collection_for_select(@trackers, :id, :name, @issue_params[:tracker_id])) %>
47 </p>
47 </p>
48 <% if @available_statuses.any? %>
48 <% if @available_statuses.any? %>
49 <p>
49 <p>
50 <label for='issue_status_id'><%= l(:field_status) %></label>
50 <label for='issue_status_id'><%= l(:field_status) %></label>
51 <%= select_tag('issue[status_id]',
51 <%= select_tag('issue[status_id]',
52 content_tag('option', l(:label_no_change_option), :value => '') +
52 content_tag('option', l(:label_no_change_option), :value => '') +
53 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
53 options_from_collection_for_select(@available_statuses, :id, :name, @issue_params[:status_id])) %>
54 </p>
54 </p>
55 <% end %>
55 <% end %>
56
56
57 <% if @safe_attributes.include?('priority_id') -%>
57 <% if @safe_attributes.include?('priority_id') -%>
58 <p>
58 <p>
59 <label for='issue_priority_id'><%= l(:field_priority) %></label>
59 <label for='issue_priority_id'><%= l(:field_priority) %></label>
60 <%= select_tag('issue[priority_id]',
60 <%= select_tag('issue[priority_id]',
61 content_tag('option', l(:label_no_change_option), :value => '') +
61 content_tag('option', l(:label_no_change_option), :value => '') +
62 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
62 options_from_collection_for_select(IssuePriority.active, :id, :name, @issue_params[:priority_id])) %>
63 </p>
63 </p>
64 <% end %>
64 <% end %>
65
65
66 <% if @safe_attributes.include?('assigned_to_id') -%>
66 <% if @safe_attributes.include?('assigned_to_id') -%>
67 <p>
67 <p>
68 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
68 <label for='issue_assigned_to_id'><%= l(:field_assigned_to) %></label>
69 <%= select_tag('issue[assigned_to_id]',
69 <%= select_tag('issue[assigned_to_id]',
70 content_tag('option', l(:label_no_change_option), :value => '') +
70 content_tag('option', l(:label_no_change_option), :value => '') +
71 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
71 content_tag('option', l(:label_nobody), :value => 'none', :selected => (@issue_params[:assigned_to_id] == 'none')) +
72 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
72 principals_options_for_select(@assignables, @issue_params[:assigned_to_id])) %>
73 </p>
73 </p>
74 <% end %>
74 <% end %>
75
75
76 <% if @safe_attributes.include?('category_id') -%>
76 <% if @safe_attributes.include?('category_id') -%>
77 <p>
77 <p>
78 <label for='issue_category_id'><%= l(:field_category) %></label>
78 <label for='issue_category_id'><%= l(:field_category) %></label>
79 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
79 <%= select_tag('issue[category_id]', content_tag('option', l(:label_no_change_option), :value => '') +
80 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
80 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:category_id] == 'none')) +
81 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
81 options_from_collection_for_select(@categories, :id, :name, @issue_params[:category_id])) %>
82 </p>
82 </p>
83 <% end %>
83 <% end %>
84
84
85 <% if @safe_attributes.include?('fixed_version_id') -%>
85 <% if @safe_attributes.include?('fixed_version_id') -%>
86 <p>
86 <p>
87 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
87 <label for='issue_fixed_version_id'><%= l(:field_fixed_version) %></label>
88 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
88 <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') +
89 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
89 content_tag('option', l(:label_none), :value => 'none', :selected => (@issue_params[:fixed_version_id] == 'none')) +
90 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
90 version_options_for_select(@versions.sort, @issue_params[:fixed_version_id])) %>
91 </p>
91 </p>
92 <% end %>
92 <% end %>
93
93
94 <% @custom_fields.each do |custom_field| %>
94 <% @custom_fields.each do |custom_field| %>
95 <p>
95 <p>
96 <label><%= custom_field.name %></label>
96 <label><%= custom_field.name %></label>
97 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @issues, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
97 <%= custom_field_tag_for_bulk_edit('issue', custom_field, @issues, @issue_params[:custom_field_values][custom_field.id.to_s]) %>
98 </p>
98 </p>
99 <% end %>
99 <% end %>
100
100
101 <% if @copy && Setting.link_copied_issue == 'ask' %>
101 <% if @copy && Setting.link_copied_issue == 'ask' %>
102 <p>
102 <p>
103 <label for='link_copy'><%= l(:label_link_copied_issue) %></label>
103 <label for='link_copy'><%= l(:label_link_copied_issue) %></label>
104 <%= hidden_field_tag 'link_copy', '0' %>
104 <%= hidden_field_tag 'link_copy', '0' %>
105 <%= check_box_tag 'link_copy', '1', params[:link_copy] != 0 %>
105 <%= check_box_tag 'link_copy', '1', params[:link_copy] != 0 %>
106 </p>
106 </p>
107 <% end %>
107 <% end %>
108
108
109 <% if @copy && @attachments_present %>
109 <% if @copy && @attachments_present %>
110 <%= hidden_field_tag 'copy_attachments', '0' %>
110 <%= hidden_field_tag 'copy_attachments', '0' %>
111 <p>
111 <p>
112 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
112 <label for='copy_attachments'><%= l(:label_copy_attachments) %></label>
113 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
113 <%= check_box_tag 'copy_attachments', '1', params[:copy_attachments] != '0' %>
114 </p>
114 </p>
115 <% end %>
115 <% end %>
116
116
117 <% if @copy && @subtasks_present %>
117 <% if @copy && @subtasks_present %>
118 <%= hidden_field_tag 'copy_subtasks', '0' %>
118 <%= hidden_field_tag 'copy_subtasks', '0' %>
119 <p>
119 <p>
120 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
120 <label for='copy_subtasks'><%= l(:label_copy_subtasks) %></label>
121 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
121 <%= check_box_tag 'copy_subtasks', '1', params[:copy_subtasks] != '0' %>
122 </p>
122 </p>
123 <% end %>
123 <% end %>
124
124
125 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
125 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
126 </div>
126 </div>
127
127
128 <div class="splitcontentright">
128 <div class="splitcontentright">
129 <% if @safe_attributes.include?('is_private') %>
129 <% if @safe_attributes.include?('is_private') %>
130 <p>
130 <p>
131 <label for='issue_is_private'><%= l(:field_is_private) %></label>
131 <label for='issue_is_private'><%= l(:field_is_private) %></label>
132 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
132 <%= select_tag('issue[is_private]', content_tag('option', l(:label_no_change_option), :value => '') +
133 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
133 content_tag('option', l(:general_text_Yes), :value => '1', :selected => (@issue_params[:is_private] == '1')) +
134 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
134 content_tag('option', l(:general_text_No), :value => '0', :selected => (@issue_params[:is_private] == '0'))) %>
135 </p>
135 </p>
136 <% end %>
136 <% end %>
137
137
138 <% if @safe_attributes.include?('parent_issue_id') && @project %>
138 <% if @safe_attributes.include?('parent_issue_id') && @project %>
139 <p>
139 <p>
140 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
140 <label for='issue_parent_issue_id'><%= l(:field_parent_issue) %></label>
141 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
141 <%= text_field_tag 'issue[parent_issue_id]', '', :size => 10, :value => @issue_params[:parent_issue_id] %>
142 <label class="inline"><%= check_box_tag 'issue[parent_issue_id]', 'none', (@issue_params[:parent_issue_id] == 'none'), :id => nil, :data => {:disables => '#issue_parent_issue_id'} %><%= l(:button_clear) %></label>
142 <label class="inline"><%= check_box_tag 'issue[parent_issue_id]', 'none', (@issue_params[:parent_issue_id] == 'none'), :id => nil, :data => {:disables => '#issue_parent_issue_id'} %><%= l(:button_clear) %></label>
143 </p>
143 </p>
144 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => Setting.cross_project_subtasks)}')" %>
144 <%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path(:project_id => @project, :scope => Setting.cross_project_subtasks)}')" %>
145 <% end %>
145 <% end %>
146
146
147 <% if @safe_attributes.include?('start_date') %>
147 <% if @safe_attributes.include?('start_date') %>
148 <p>
148 <p>
149 <label for='issue_start_date'><%= l(:field_start_date) %></label>
149 <label for='issue_start_date'><%= l(:field_start_date) %></label>
150 <%= text_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
150 <%= date_field_tag 'issue[start_date]', '', :value => @issue_params[:start_date], :size => 10 %><%= calendar_for('issue_start_date') %>
151 <label class="inline"><%= check_box_tag 'issue[start_date]', 'none', (@issue_params[:start_date] == 'none'), :id => nil, :data => {:disables => '#issue_start_date'} %><%= l(:button_clear) %></label>
151 <label class="inline"><%= check_box_tag 'issue[start_date]', 'none', (@issue_params[:start_date] == 'none'), :id => nil, :data => {:disables => '#issue_start_date'} %><%= l(:button_clear) %></label>
152 </p>
152 </p>
153 <% end %>
153 <% end %>
154
154
155 <% if @safe_attributes.include?('due_date') %>
155 <% if @safe_attributes.include?('due_date') %>
156 <p>
156 <p>
157 <label for='issue_due_date'><%= l(:field_due_date) %></label>
157 <label for='issue_due_date'><%= l(:field_due_date) %></label>
158 <%= text_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
158 <%= date_field_tag 'issue[due_date]', '', :value => @issue_params[:due_date], :size => 10 %><%= calendar_for('issue_due_date') %>
159 <label class="inline"><%= check_box_tag 'issue[due_date]', 'none', (@issue_params[:due_date] == 'none'), :id => nil, :data => {:disables => '#issue_due_date'} %><%= l(:button_clear) %></label>
159 <label class="inline"><%= check_box_tag 'issue[due_date]', 'none', (@issue_params[:due_date] == 'none'), :id => nil, :data => {:disables => '#issue_due_date'} %><%= l(:button_clear) %></label>
160 </p>
160 </p>
161 <% end %>
161 <% end %>
162
162
163 <% if @safe_attributes.include?('estimated_hours') %>
163 <% if @safe_attributes.include?('estimated_hours') %>
164 <p>
164 <p>
165 <label for='issue_estimated_hours'><%= l(:field_estimated_hours) %></label>
165 <label for='issue_estimated_hours'><%= l(:field_estimated_hours) %></label>
166 <%= text_field_tag 'issue[estimated_hours]', '', :value => @issue_params[:estimated_hours], :size => 10 %>
166 <%= text_field_tag 'issue[estimated_hours]', '', :value => @issue_params[:estimated_hours], :size => 10 %>
167 <label class="inline"><%= check_box_tag 'issue[estimated_hours]', 'none', (@issue_params[:estimated_hours] == 'none'), :id => nil, :data => {:disables => '#issue_estimated_hours'} %><%= l(:button_clear) %></label>
167 <label class="inline"><%= check_box_tag 'issue[estimated_hours]', 'none', (@issue_params[:estimated_hours] == 'none'), :id => nil, :data => {:disables => '#issue_estimated_hours'} %><%= l(:button_clear) %></label>
168 </p>
168 </p>
169 <% end %>
169 <% end %>
170
170
171 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
171 <% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
172 <p>
172 <p>
173 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
173 <label for='issue_done_ratio'><%= l(:field_done_ratio) %></label>
174 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }, @issue_params[:done_ratio]) %>
174 <%= select_tag 'issue[done_ratio]', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }, @issue_params[:done_ratio]) %>
175 </p>
175 </p>
176 <% end %>
176 <% end %>
177 </div>
177 </div>
178 </fieldset>
178 </fieldset>
179
179
180 <fieldset>
180 <fieldset>
181 <legend><%= l(:field_notes) %></legend>
181 <legend><%= l(:field_notes) %></legend>
182 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
182 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
183 <%= wikitoolbar_for 'notes' %>
183 <%= wikitoolbar_for 'notes' %>
184 </fieldset>
184 </fieldset>
185 </div>
185 </div>
186
186
187 <p>
187 <p>
188 <% if @copy %>
188 <% if @copy %>
189 <%= hidden_field_tag 'copy', '1' %>
189 <%= hidden_field_tag 'copy', '1' %>
190 <%= submit_tag l(:button_copy) %>
190 <%= submit_tag l(:button_copy) %>
191 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
191 <%= submit_tag l(:button_copy_and_follow), :name => 'follow' %>
192 <% elsif @target_project %>
192 <% elsif @target_project %>
193 <%= submit_tag l(:button_move) %>
193 <%= submit_tag l(:button_move) %>
194 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
194 <%= submit_tag l(:button_move_and_follow), :name => 'follow' %>
195 <% else %>
195 <% else %>
196 <%= submit_tag l(:button_submit) %>
196 <%= submit_tag l(:button_submit) %>
197 <% end %>
197 <% end %>
198 </p>
198 </p>
199
199
200 <% end %>
200 <% end %>
201
201
202 <%= javascript_tag do %>
202 <%= javascript_tag do %>
203 $(window).load(function(){
203 $(window).load(function(){
204 $(document).on('change', 'input[data-disables]', function(){
204 $(document).on('change', 'input[data-disables]', function(){
205 if ($(this).prop('checked')){
205 if ($(this).prop('checked')){
206 $($(this).data('disables')).attr('disabled', true).val('');
206 $($(this).data('disables')).attr('disabled', true).val('');
207 } else {
207 } else {
208 $($(this).data('disables')).attr('disabled', false);
208 $($(this).data('disables')).attr('disabled', false);
209 }
209 }
210 });
210 });
211 });
211 });
212 $(document).ready(function(){
212 $(document).ready(function(){
213 $('input[data-disables]').trigger('change');
213 $('input[data-disables]').trigger('change');
214 });
214 });
215 <% end %>
215 <% end %>
@@ -1,49 +1,49
1 <%= error_messages_for 'time_entry' %>
1 <%= error_messages_for 'time_entry' %>
2 <%= back_url_hidden_field_tag %>
2 <%= back_url_hidden_field_tag %>
3
3
4 <div class="box tabular">
4 <div class="box tabular">
5 <% if @time_entry.new_record? %>
5 <% if @time_entry.new_record? %>
6 <% if params[:project_id] %>
6 <% if params[:project_id] %>
7 <%= hidden_field_tag 'project_id', params[:project_id] %>
7 <%= hidden_field_tag 'project_id', params[:project_id] %>
8 <% elsif params[:issue_id] %>
8 <% elsif params[:issue_id] %>
9 <%= hidden_field_tag 'issue_id', params[:issue_id] %>
9 <%= hidden_field_tag 'issue_id', params[:issue_id] %>
10 <% else %>
10 <% else %>
11 <p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).to_a, :selected => @time_entry.project, :include_blank => true) %></p>
11 <p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).to_a, :selected => @time_entry.project, :include_blank => true) %></p>
12 <% end %>
12 <% end %>
13 <% end %>
13 <% end %>
14 <p>
14 <p>
15 <%= f.text_field :issue_id, :size => 6 %>
15 <%= f.text_field :issue_id, :size => 6 %>
16 <% if @time_entry.issue.try(:visible?) %>
16 <% if @time_entry.issue.try(:visible?) %>
17 <span id="time_entry_issue"><%= "#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}" %></span>
17 <span id="time_entry_issue"><%= "#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}" %></span>
18 <% end %>
18 <% end %>
19 </p>
19 </p>
20 <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
20 <p><%= f.date_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
21 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
21 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
22 <p><%= f.text_field :comments, :size => 100, :maxlength => 1024 %></p>
22 <p><%= f.text_field :comments, :size => 100, :maxlength => 1024 %></p>
23 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
23 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
24 <% @time_entry.custom_field_values.each do |value| %>
24 <% @time_entry.custom_field_values.each do |value| %>
25 <p><%= custom_field_tag_with_label :time_entry, value %></p>
25 <p><%= custom_field_tag_with_label :time_entry, value %></p>
26 <% end %>
26 <% end %>
27 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
27 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
28 </div>
28 </div>
29
29
30 <%= javascript_tag do %>
30 <%= javascript_tag do %>
31 <% if @time_entry.new_record? %>
31 <% if @time_entry.new_record? %>
32 $(document).ready(function(){
32 $(document).ready(function(){
33 $('#time_entry_project_id, #time_entry_issue_id').change(function(){
33 $('#time_entry_project_id, #time_entry_issue_id').change(function(){
34 $.ajax({
34 $.ajax({
35 url: '<%= escape_javascript new_time_entry_path(:format => 'js') %>',
35 url: '<%= escape_javascript new_time_entry_path(:format => 'js') %>',
36 type: 'post',
36 type: 'post',
37 data: $('#new_time_entry').serialize()
37 data: $('#new_time_entry').serialize()
38 });
38 });
39 });
39 });
40 });
40 });
41 <% end %>
41 <% end %>
42
42
43 observeAutocompleteField('time_entry_issue_id', '<%= escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (@project ? nil : 'all'))%>', {
43 observeAutocompleteField('time_entry_issue_id', '<%= escape_javascript auto_complete_issues_path(:project_id => @project, :scope => (@project ? nil : 'all'))%>', {
44 select: function(event, ui) {
44 select: function(event, ui) {
45 $('#time_entry_issue').text(ui.item.label);
45 $('#time_entry_issue').text(ui.item.label);
46 $('#time_entry_issue_id').blur();
46 $('#time_entry_issue_id').blur();
47 }
47 }
48 });
48 });
49 <% end %>
49 <% end %>
@@ -1,50 +1,50
1 <h2><%= l(:label_bulk_edit_selected_time_entries) %></h2>
1 <h2><%= l(:label_bulk_edit_selected_time_entries) %></h2>
2
2
3 <ul id="bulk-selection">
3 <ul id="bulk-selection">
4 <% @time_entries.each do |entry| %>
4 <% @time_entries.each do |entry| %>
5 <%= content_tag 'li',
5 <%= content_tag 'li',
6 link_to("#{format_date(entry.spent_on)} - #{entry.project}: #{l(:label_f_hour_plural, :value => entry.hours)}", edit_time_entry_path(entry)) %>
6 link_to("#{format_date(entry.spent_on)} - #{entry.project}: #{l(:label_f_hour_plural, :value => entry.hours)}", edit_time_entry_path(entry)) %>
7 <% end %>
7 <% end %>
8 </ul>
8 </ul>
9
9
10 <%= form_tag(bulk_update_time_entries_path, :id => 'bulk_edit_form') do %>
10 <%= form_tag(bulk_update_time_entries_path, :id => 'bulk_edit_form') do %>
11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join.html_safe %>
11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id, :id => nil)}.join.html_safe %>
12 <div class="box tabular">
12 <div class="box tabular">
13 <div>
13 <div>
14 <p>
14 <p>
15 <label><%= l(:field_issue) %></label>
15 <label><%= l(:field_issue) %></label>
16 <%= text_field :time_entry, :issue_id, :size => 6 %>
16 <%= text_field :time_entry, :issue_id, :size => 6 %>
17 </p>
17 </p>
18
18
19 <p>
19 <p>
20 <label><%= l(:field_spent_on) %></label>
20 <label><%= l(:field_spent_on) %></label>
21 <%= text_field :time_entry, :spent_on, :size => 10 %><%= calendar_for('time_entry_spent_on') %>
21 <%= date_field :time_entry, :spent_on, :size => 10 %><%= calendar_for('time_entry_spent_on') %>
22 </p>
22 </p>
23
23
24 <p>
24 <p>
25 <label><%= l(:field_hours) %></label>
25 <label><%= l(:field_hours) %></label>
26 <%= text_field :time_entry, :hours, :size => 6 %>
26 <%= text_field :time_entry, :hours, :size => 6 %>
27 </p>
27 </p>
28
28
29 <% if @available_activities.any? %>
29 <% if @available_activities.any? %>
30 <p>
30 <p>
31 <label><%= l(:field_activity) %></label>
31 <label><%= l(:field_activity) %></label>
32 <%= select_tag('time_entry[activity_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@available_activities, :id, :name)) %>
32 <%= select_tag('time_entry[activity_id]', content_tag('option', l(:label_no_change_option), :value => '') + options_from_collection_for_select(@available_activities, :id, :name)) %>
33 </p>
33 </p>
34 <% end %>
34 <% end %>
35
35
36 <p>
36 <p>
37 <label><%= l(:field_comments) %></label>
37 <label><%= l(:field_comments) %></label>
38 <%= text_field(:time_entry, :comments, :size => 100) %>
38 <%= text_field(:time_entry, :comments, :size => 100) %>
39 </p>
39 </p>
40
40
41 <% @custom_fields.each do |custom_field| %>
41 <% @custom_fields.each do |custom_field| %>
42 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('time_entry', custom_field, @time_entries) %></p>
42 <p><label><%= h(custom_field.name) %></label> <%= custom_field_tag_for_bulk_edit('time_entry', custom_field, @time_entries) %></p>
43 <% end %>
43 <% end %>
44
44
45 <%= call_hook(:view_time_entries_bulk_edit_details_bottom, { :time_entries => @time_entries }) %>
45 <%= call_hook(:view_time_entries_bulk_edit_details_bottom, { :time_entries => @time_entries }) %>
46 </div>
46 </div>
47 </div>
47 </div>
48
48
49 <p><%= submit_tag l(:button_submit) %></p>
49 <p><%= submit_tag l(:button_submit) %></p>
50 <% end %>
50 <% end %>
@@ -1,16 +1,16
1 <%= back_url_hidden_field_tag %>
1 <%= back_url_hidden_field_tag %>
2 <%= error_messages_for 'version' %>
2 <%= error_messages_for 'version' %>
3
3
4 <div class="box tabular">
4 <div class="box tabular">
5 <p><%= f.text_field :name, :size => 60, :required => true %></p>
5 <p><%= f.text_field :name, :size => 60, :required => true %></p>
6 <p><%= f.text_field :description, :size => 60 %></p>
6 <p><%= f.text_field :description, :size => 60 %></p>
7 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
7 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
8 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
8 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
9 <p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
9 <p><%= f.date_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
10 <p><%= f.select :sharing, @version.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %></p>
10 <p><%= f.select :sharing, @version.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %></p>
11
11
12 <% @version.custom_field_values.each do |value| %>
12 <% @version.custom_field_values.each do |value| %>
13 <p><%= custom_field_tag_with_label :version, value %></p>
13 <p><%= custom_field_tag_with_label :version, value %></p>
14 <% end %>
14 <% end %>
15
15
16 </div>
16 </div>
@@ -1,805 +1,805
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 module Redmine
18 module Redmine
19 module FieldFormat
19 module FieldFormat
20 def self.add(name, klass)
20 def self.add(name, klass)
21 all[name.to_s] = klass.instance
21 all[name.to_s] = klass.instance
22 end
22 end
23
23
24 def self.delete(name)
24 def self.delete(name)
25 all.delete(name.to_s)
25 all.delete(name.to_s)
26 end
26 end
27
27
28 def self.all
28 def self.all
29 @formats ||= Hash.new(Base.instance)
29 @formats ||= Hash.new(Base.instance)
30 end
30 end
31
31
32 def self.available_formats
32 def self.available_formats
33 all.keys
33 all.keys
34 end
34 end
35
35
36 def self.find(name)
36 def self.find(name)
37 all[name.to_s]
37 all[name.to_s]
38 end
38 end
39
39
40 # Return an array of custom field formats which can be used in select_tag
40 # Return an array of custom field formats which can be used in select_tag
41 def self.as_select(class_name=nil)
41 def self.as_select(class_name=nil)
42 formats = all.values.select do |format|
42 formats = all.values.select do |format|
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 end
44 end
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 end
46 end
47
47
48 class Base
48 class Base
49 include Singleton
49 include Singleton
50 include Redmine::I18n
50 include Redmine::I18n
51 include ERB::Util
51 include ERB::Util
52
52
53 class_attribute :format_name
53 class_attribute :format_name
54 self.format_name = nil
54 self.format_name = nil
55
55
56 # Set this to true if the format supports multiple values
56 # Set this to true if the format supports multiple values
57 class_attribute :multiple_supported
57 class_attribute :multiple_supported
58 self.multiple_supported = false
58 self.multiple_supported = false
59
59
60 # Set this to true if the format supports textual search on custom values
60 # Set this to true if the format supports textual search on custom values
61 class_attribute :searchable_supported
61 class_attribute :searchable_supported
62 self.searchable_supported = false
62 self.searchable_supported = false
63
63
64 # Set this to true if field values can be summed up
64 # Set this to true if field values can be summed up
65 class_attribute :totalable_supported
65 class_attribute :totalable_supported
66 self.totalable_supported = false
66 self.totalable_supported = false
67
67
68 # Restricts the classes that the custom field can be added to
68 # Restricts the classes that the custom field can be added to
69 # Set to nil for no restrictions
69 # Set to nil for no restrictions
70 class_attribute :customized_class_names
70 class_attribute :customized_class_names
71 self.customized_class_names = nil
71 self.customized_class_names = nil
72
72
73 # Name of the partial for editing the custom field
73 # Name of the partial for editing the custom field
74 class_attribute :form_partial
74 class_attribute :form_partial
75 self.form_partial = nil
75 self.form_partial = nil
76
76
77 class_attribute :change_as_diff
77 class_attribute :change_as_diff
78 self.change_as_diff = false
78 self.change_as_diff = false
79
79
80 def self.add(name)
80 def self.add(name)
81 self.format_name = name
81 self.format_name = name
82 Redmine::FieldFormat.add(name, self)
82 Redmine::FieldFormat.add(name, self)
83 end
83 end
84 private_class_method :add
84 private_class_method :add
85
85
86 def self.field_attributes(*args)
86 def self.field_attributes(*args)
87 CustomField.store_accessor :format_store, *args
87 CustomField.store_accessor :format_store, *args
88 end
88 end
89
89
90 field_attributes :url_pattern
90 field_attributes :url_pattern
91
91
92 def name
92 def name
93 self.class.format_name
93 self.class.format_name
94 end
94 end
95
95
96 def label
96 def label
97 "label_#{name}"
97 "label_#{name}"
98 end
98 end
99
99
100 def cast_custom_value(custom_value)
100 def cast_custom_value(custom_value)
101 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
101 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
102 end
102 end
103
103
104 def cast_value(custom_field, value, customized=nil)
104 def cast_value(custom_field, value, customized=nil)
105 if value.blank?
105 if value.blank?
106 nil
106 nil
107 elsif value.is_a?(Array)
107 elsif value.is_a?(Array)
108 casted = value.map do |v|
108 casted = value.map do |v|
109 cast_single_value(custom_field, v, customized)
109 cast_single_value(custom_field, v, customized)
110 end
110 end
111 casted.compact.sort
111 casted.compact.sort
112 else
112 else
113 cast_single_value(custom_field, value, customized)
113 cast_single_value(custom_field, value, customized)
114 end
114 end
115 end
115 end
116
116
117 def cast_single_value(custom_field, value, customized=nil)
117 def cast_single_value(custom_field, value, customized=nil)
118 value.to_s
118 value.to_s
119 end
119 end
120
120
121 def target_class
121 def target_class
122 nil
122 nil
123 end
123 end
124
124
125 def possible_custom_value_options(custom_value)
125 def possible_custom_value_options(custom_value)
126 possible_values_options(custom_value.custom_field, custom_value.customized)
126 possible_values_options(custom_value.custom_field, custom_value.customized)
127 end
127 end
128
128
129 def possible_values_options(custom_field, object=nil)
129 def possible_values_options(custom_field, object=nil)
130 []
130 []
131 end
131 end
132
132
133 def value_from_keyword(custom_field, keyword, object)
133 def value_from_keyword(custom_field, keyword, object)
134 possible_values_options = possible_values_options(custom_field, object)
134 possible_values_options = possible_values_options(custom_field, object)
135 if possible_values_options.present?
135 if possible_values_options.present?
136 keyword = keyword.to_s
136 keyword = keyword.to_s
137 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
137 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
138 if v.is_a?(Array)
138 if v.is_a?(Array)
139 v.last
139 v.last
140 else
140 else
141 v
141 v
142 end
142 end
143 end
143 end
144 else
144 else
145 keyword
145 keyword
146 end
146 end
147 end
147 end
148
148
149 # Returns the validation errors for custom_field
149 # Returns the validation errors for custom_field
150 # Should return an empty array if custom_field is valid
150 # Should return an empty array if custom_field is valid
151 def validate_custom_field(custom_field)
151 def validate_custom_field(custom_field)
152 []
152 []
153 end
153 end
154
154
155 # Returns the validation error messages for custom_value
155 # Returns the validation error messages for custom_value
156 # Should return an empty array if custom_value is valid
156 # Should return an empty array if custom_value is valid
157 def validate_custom_value(custom_value)
157 def validate_custom_value(custom_value)
158 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
158 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
159 errors = values.map do |value|
159 errors = values.map do |value|
160 validate_single_value(custom_value.custom_field, value, custom_value.customized)
160 validate_single_value(custom_value.custom_field, value, custom_value.customized)
161 end
161 end
162 errors.flatten.uniq
162 errors.flatten.uniq
163 end
163 end
164
164
165 def validate_single_value(custom_field, value, customized=nil)
165 def validate_single_value(custom_field, value, customized=nil)
166 []
166 []
167 end
167 end
168
168
169 def formatted_custom_value(view, custom_value, html=false)
169 def formatted_custom_value(view, custom_value, html=false)
170 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
170 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
171 end
171 end
172
172
173 def formatted_value(view, custom_field, value, customized=nil, html=false)
173 def formatted_value(view, custom_field, value, customized=nil, html=false)
174 casted = cast_value(custom_field, value, customized)
174 casted = cast_value(custom_field, value, customized)
175 if html && custom_field.url_pattern.present?
175 if html && custom_field.url_pattern.present?
176 texts_and_urls = Array.wrap(casted).map do |single_value|
176 texts_and_urls = Array.wrap(casted).map do |single_value|
177 text = view.format_object(single_value, false).to_s
177 text = view.format_object(single_value, false).to_s
178 url = url_from_pattern(custom_field, single_value, customized)
178 url = url_from_pattern(custom_field, single_value, customized)
179 [text, url]
179 [text, url]
180 end
180 end
181 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
181 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
182 links.join(', ').html_safe
182 links.join(', ').html_safe
183 else
183 else
184 casted
184 casted
185 end
185 end
186 end
186 end
187
187
188 # Returns an URL generated with the custom field URL pattern
188 # Returns an URL generated with the custom field URL pattern
189 # and variables substitution:
189 # and variables substitution:
190 # %value% => the custom field value
190 # %value% => the custom field value
191 # %id% => id of the customized object
191 # %id% => id of the customized object
192 # %project_id% => id of the project of the customized object if defined
192 # %project_id% => id of the project of the customized object if defined
193 # %project_identifier% => identifier of the project of the customized object if defined
193 # %project_identifier% => identifier of the project of the customized object if defined
194 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
194 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
195 def url_from_pattern(custom_field, value, customized)
195 def url_from_pattern(custom_field, value, customized)
196 url = custom_field.url_pattern.to_s.dup
196 url = custom_field.url_pattern.to_s.dup
197 url.gsub!('%value%') {value.to_s}
197 url.gsub!('%value%') {value.to_s}
198 url.gsub!('%id%') {customized.id.to_s}
198 url.gsub!('%id%') {customized.id.to_s}
199 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
199 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
200 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
200 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
201 if custom_field.regexp.present?
201 if custom_field.regexp.present?
202 url.gsub!(%r{%m(\d+)%}) do
202 url.gsub!(%r{%m(\d+)%}) do
203 m = $1.to_i
203 m = $1.to_i
204 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
204 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
205 matches[m].to_s
205 matches[m].to_s
206 end
206 end
207 end
207 end
208 end
208 end
209 url
209 url
210 end
210 end
211 protected :url_from_pattern
211 protected :url_from_pattern
212
212
213 def edit_tag(view, tag_id, tag_name, custom_value, options={})
213 def edit_tag(view, tag_id, tag_name, custom_value, options={})
214 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
214 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
215 end
215 end
216
216
217 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
217 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
218 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
218 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
219 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
219 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
220 end
220 end
221
221
222 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
222 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
223 if custom_field.is_required?
223 if custom_field.is_required?
224 ''.html_safe
224 ''.html_safe
225 else
225 else
226 view.content_tag('label',
226 view.content_tag('label',
227 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
227 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
228 :class => 'inline'
228 :class => 'inline'
229 )
229 )
230 end
230 end
231 end
231 end
232 protected :bulk_clear_tag
232 protected :bulk_clear_tag
233
233
234 def query_filter_options(custom_field, query)
234 def query_filter_options(custom_field, query)
235 {:type => :string}
235 {:type => :string}
236 end
236 end
237
237
238 def before_custom_field_save(custom_field)
238 def before_custom_field_save(custom_field)
239 end
239 end
240
240
241 # Returns a ORDER BY clause that can used to sort customized
241 # Returns a ORDER BY clause that can used to sort customized
242 # objects by their value of the custom field.
242 # objects by their value of the custom field.
243 # Returns nil if the custom field can not be used for sorting.
243 # Returns nil if the custom field can not be used for sorting.
244 def order_statement(custom_field)
244 def order_statement(custom_field)
245 # COALESCE is here to make sure that blank and NULL values are sorted equally
245 # COALESCE is here to make sure that blank and NULL values are sorted equally
246 "COALESCE(#{join_alias custom_field}.value, '')"
246 "COALESCE(#{join_alias custom_field}.value, '')"
247 end
247 end
248
248
249 # Returns a GROUP BY clause that can used to group by custom value
249 # Returns a GROUP BY clause that can used to group by custom value
250 # Returns nil if the custom field can not be used for grouping.
250 # Returns nil if the custom field can not be used for grouping.
251 def group_statement(custom_field)
251 def group_statement(custom_field)
252 nil
252 nil
253 end
253 end
254
254
255 # Returns a JOIN clause that is added to the query when sorting by custom values
255 # Returns a JOIN clause that is added to the query when sorting by custom values
256 def join_for_order_statement(custom_field)
256 def join_for_order_statement(custom_field)
257 alias_name = join_alias(custom_field)
257 alias_name = join_alias(custom_field)
258
258
259 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
259 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
260 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
260 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
261 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
261 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
262 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
262 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
263 " AND (#{custom_field.visibility_by_project_condition})" +
263 " AND (#{custom_field.visibility_by_project_condition})" +
264 " AND #{alias_name}.value <> ''" +
264 " AND #{alias_name}.value <> ''" +
265 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
265 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
266 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
266 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
267 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
267 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
268 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
268 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
269 end
269 end
270
270
271 def join_alias(custom_field)
271 def join_alias(custom_field)
272 "cf_#{custom_field.id}"
272 "cf_#{custom_field.id}"
273 end
273 end
274 protected :join_alias
274 protected :join_alias
275 end
275 end
276
276
277 class Unbounded < Base
277 class Unbounded < Base
278 def validate_single_value(custom_field, value, customized=nil)
278 def validate_single_value(custom_field, value, customized=nil)
279 errs = super
279 errs = super
280 value = value.to_s
280 value = value.to_s
281 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
281 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
282 errs << ::I18n.t('activerecord.errors.messages.invalid')
282 errs << ::I18n.t('activerecord.errors.messages.invalid')
283 end
283 end
284 if custom_field.min_length && value.length < custom_field.min_length
284 if custom_field.min_length && value.length < custom_field.min_length
285 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
285 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
286 end
286 end
287 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
287 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
288 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
288 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
289 end
289 end
290 errs
290 errs
291 end
291 end
292 end
292 end
293
293
294 class StringFormat < Unbounded
294 class StringFormat < Unbounded
295 add 'string'
295 add 'string'
296 self.searchable_supported = true
296 self.searchable_supported = true
297 self.form_partial = 'custom_fields/formats/string'
297 self.form_partial = 'custom_fields/formats/string'
298 field_attributes :text_formatting
298 field_attributes :text_formatting
299
299
300 def formatted_value(view, custom_field, value, customized=nil, html=false)
300 def formatted_value(view, custom_field, value, customized=nil, html=false)
301 if html
301 if html
302 if custom_field.url_pattern.present?
302 if custom_field.url_pattern.present?
303 super
303 super
304 elsif custom_field.text_formatting == 'full'
304 elsif custom_field.text_formatting == 'full'
305 view.textilizable(value, :object => customized)
305 view.textilizable(value, :object => customized)
306 else
306 else
307 value.to_s
307 value.to_s
308 end
308 end
309 else
309 else
310 value.to_s
310 value.to_s
311 end
311 end
312 end
312 end
313 end
313 end
314
314
315 class TextFormat < Unbounded
315 class TextFormat < Unbounded
316 add 'text'
316 add 'text'
317 self.searchable_supported = true
317 self.searchable_supported = true
318 self.form_partial = 'custom_fields/formats/text'
318 self.form_partial = 'custom_fields/formats/text'
319 self.change_as_diff = true
319 self.change_as_diff = true
320
320
321 def formatted_value(view, custom_field, value, customized=nil, html=false)
321 def formatted_value(view, custom_field, value, customized=nil, html=false)
322 if html
322 if html
323 if value.present?
323 if value.present?
324 if custom_field.text_formatting == 'full'
324 if custom_field.text_formatting == 'full'
325 view.textilizable(value, :object => customized)
325 view.textilizable(value, :object => customized)
326 else
326 else
327 view.simple_format(html_escape(value))
327 view.simple_format(html_escape(value))
328 end
328 end
329 else
329 else
330 ''
330 ''
331 end
331 end
332 else
332 else
333 value.to_s
333 value.to_s
334 end
334 end
335 end
335 end
336
336
337 def edit_tag(view, tag_id, tag_name, custom_value, options={})
337 def edit_tag(view, tag_id, tag_name, custom_value, options={})
338 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
338 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
339 end
339 end
340
340
341 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
341 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
342 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
342 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
343 '<br />'.html_safe +
343 '<br />'.html_safe +
344 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
344 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
345 end
345 end
346
346
347 def query_filter_options(custom_field, query)
347 def query_filter_options(custom_field, query)
348 {:type => :text}
348 {:type => :text}
349 end
349 end
350 end
350 end
351
351
352 class LinkFormat < StringFormat
352 class LinkFormat < StringFormat
353 add 'link'
353 add 'link'
354 self.searchable_supported = false
354 self.searchable_supported = false
355 self.form_partial = 'custom_fields/formats/link'
355 self.form_partial = 'custom_fields/formats/link'
356
356
357 def formatted_value(view, custom_field, value, customized=nil, html=false)
357 def formatted_value(view, custom_field, value, customized=nil, html=false)
358 if html && value.present?
358 if html && value.present?
359 if custom_field.url_pattern.present?
359 if custom_field.url_pattern.present?
360 url = url_from_pattern(custom_field, value, customized)
360 url = url_from_pattern(custom_field, value, customized)
361 else
361 else
362 url = value.to_s
362 url = value.to_s
363 unless url =~ %r{\A[a-z]+://}i
363 unless url =~ %r{\A[a-z]+://}i
364 # no protocol found, use http by default
364 # no protocol found, use http by default
365 url = "http://" + url
365 url = "http://" + url
366 end
366 end
367 end
367 end
368 view.link_to value.to_s.truncate(40), url
368 view.link_to value.to_s.truncate(40), url
369 else
369 else
370 value.to_s
370 value.to_s
371 end
371 end
372 end
372 end
373 end
373 end
374
374
375 class Numeric < Unbounded
375 class Numeric < Unbounded
376 self.form_partial = 'custom_fields/formats/numeric'
376 self.form_partial = 'custom_fields/formats/numeric'
377 self.totalable_supported = true
377 self.totalable_supported = true
378
378
379 def order_statement(custom_field)
379 def order_statement(custom_field)
380 # Make the database cast values into numeric
380 # Make the database cast values into numeric
381 # Postgresql will raise an error if a value can not be casted!
381 # Postgresql will raise an error if a value can not be casted!
382 # CustomValue validations should ensure that it doesn't occur
382 # CustomValue validations should ensure that it doesn't occur
383 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
383 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
384 end
384 end
385
385
386 # Returns totals for the given scope
386 # Returns totals for the given scope
387 def total_for_scope(custom_field, scope)
387 def total_for_scope(custom_field, scope)
388 scope.joins(:custom_values).
388 scope.joins(:custom_values).
389 where(:custom_values => {:custom_field_id => custom_field.id}).
389 where(:custom_values => {:custom_field_id => custom_field.id}).
390 where.not(:custom_values => {:value => ''}).
390 where.not(:custom_values => {:value => ''}).
391 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
391 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
392 end
392 end
393
393
394 def cast_total_value(custom_field, value)
394 def cast_total_value(custom_field, value)
395 cast_single_value(custom_field, value)
395 cast_single_value(custom_field, value)
396 end
396 end
397 end
397 end
398
398
399 class IntFormat < Numeric
399 class IntFormat < Numeric
400 add 'int'
400 add 'int'
401
401
402 def label
402 def label
403 "label_integer"
403 "label_integer"
404 end
404 end
405
405
406 def cast_single_value(custom_field, value, customized=nil)
406 def cast_single_value(custom_field, value, customized=nil)
407 value.to_i
407 value.to_i
408 end
408 end
409
409
410 def validate_single_value(custom_field, value, customized=nil)
410 def validate_single_value(custom_field, value, customized=nil)
411 errs = super
411 errs = super
412 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
412 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
413 errs
413 errs
414 end
414 end
415
415
416 def query_filter_options(custom_field, query)
416 def query_filter_options(custom_field, query)
417 {:type => :integer}
417 {:type => :integer}
418 end
418 end
419
419
420 def group_statement(custom_field)
420 def group_statement(custom_field)
421 order_statement(custom_field)
421 order_statement(custom_field)
422 end
422 end
423 end
423 end
424
424
425 class FloatFormat < Numeric
425 class FloatFormat < Numeric
426 add 'float'
426 add 'float'
427
427
428 def cast_single_value(custom_field, value, customized=nil)
428 def cast_single_value(custom_field, value, customized=nil)
429 value.to_f
429 value.to_f
430 end
430 end
431
431
432 def cast_total_value(custom_field, value)
432 def cast_total_value(custom_field, value)
433 value.to_f.round(2)
433 value.to_f.round(2)
434 end
434 end
435
435
436 def validate_single_value(custom_field, value, customized=nil)
436 def validate_single_value(custom_field, value, customized=nil)
437 errs = super
437 errs = super
438 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
438 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
439 errs
439 errs
440 end
440 end
441
441
442 def query_filter_options(custom_field, query)
442 def query_filter_options(custom_field, query)
443 {:type => :float}
443 {:type => :float}
444 end
444 end
445 end
445 end
446
446
447 class DateFormat < Unbounded
447 class DateFormat < Unbounded
448 add 'date'
448 add 'date'
449 self.form_partial = 'custom_fields/formats/date'
449 self.form_partial = 'custom_fields/formats/date'
450
450
451 def cast_single_value(custom_field, value, customized=nil)
451 def cast_single_value(custom_field, value, customized=nil)
452 value.to_date rescue nil
452 value.to_date rescue nil
453 end
453 end
454
454
455 def validate_single_value(custom_field, value, customized=nil)
455 def validate_single_value(custom_field, value, customized=nil)
456 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
456 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
457 []
457 []
458 else
458 else
459 [::I18n.t('activerecord.errors.messages.not_a_date')]
459 [::I18n.t('activerecord.errors.messages.not_a_date')]
460 end
460 end
461 end
461 end
462
462
463 def edit_tag(view, tag_id, tag_name, custom_value, options={})
463 def edit_tag(view, tag_id, tag_name, custom_value, options={})
464 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
464 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
465 view.calendar_for(tag_id)
465 view.calendar_for(tag_id)
466 end
466 end
467
467
468 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
468 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
469 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
469 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
470 view.calendar_for(tag_id) +
470 view.calendar_for(tag_id) +
471 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
471 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
472 end
472 end
473
473
474 def query_filter_options(custom_field, query)
474 def query_filter_options(custom_field, query)
475 {:type => :date}
475 {:type => :date}
476 end
476 end
477
477
478 def group_statement(custom_field)
478 def group_statement(custom_field)
479 order_statement(custom_field)
479 order_statement(custom_field)
480 end
480 end
481 end
481 end
482
482
483 class List < Base
483 class List < Base
484 self.multiple_supported = true
484 self.multiple_supported = true
485 field_attributes :edit_tag_style
485 field_attributes :edit_tag_style
486
486
487 def edit_tag(view, tag_id, tag_name, custom_value, options={})
487 def edit_tag(view, tag_id, tag_name, custom_value, options={})
488 if custom_value.custom_field.edit_tag_style == 'check_box'
488 if custom_value.custom_field.edit_tag_style == 'check_box'
489 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
489 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
490 else
490 else
491 select_edit_tag(view, tag_id, tag_name, custom_value, options)
491 select_edit_tag(view, tag_id, tag_name, custom_value, options)
492 end
492 end
493 end
493 end
494
494
495 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
495 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
496 opts = []
496 opts = []
497 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
497 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
498 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
498 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
499 opts += possible_values_options(custom_field, objects)
499 opts += possible_values_options(custom_field, objects)
500 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
500 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
501 end
501 end
502
502
503 def query_filter_options(custom_field, query)
503 def query_filter_options(custom_field, query)
504 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
504 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
505 end
505 end
506
506
507 protected
507 protected
508
508
509 # Returns the values that are available in the field filter
509 # Returns the values that are available in the field filter
510 def query_filter_values(custom_field, query)
510 def query_filter_values(custom_field, query)
511 possible_values_options(custom_field, query.project)
511 possible_values_options(custom_field, query.project)
512 end
512 end
513
513
514 # Renders the edit tag as a select tag
514 # Renders the edit tag as a select tag
515 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
515 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
516 blank_option = ''.html_safe
516 blank_option = ''.html_safe
517 unless custom_value.custom_field.multiple?
517 unless custom_value.custom_field.multiple?
518 if custom_value.custom_field.is_required?
518 if custom_value.custom_field.is_required?
519 unless custom_value.custom_field.default_value.present?
519 unless custom_value.custom_field.default_value.present?
520 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
520 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
521 end
521 end
522 else
522 else
523 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
523 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
524 end
524 end
525 end
525 end
526 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
526 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
527 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
527 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
528 if custom_value.custom_field.multiple?
528 if custom_value.custom_field.multiple?
529 s << view.hidden_field_tag(tag_name, '')
529 s << view.hidden_field_tag(tag_name, '')
530 end
530 end
531 s
531 s
532 end
532 end
533
533
534 # Renders the edit tag as check box or radio tags
534 # Renders the edit tag as check box or radio tags
535 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
535 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
536 opts = []
536 opts = []
537 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
537 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
538 opts << ["(#{l(:label_none)})", '']
538 opts << ["(#{l(:label_none)})", '']
539 end
539 end
540 opts += possible_custom_value_options(custom_value)
540 opts += possible_custom_value_options(custom_value)
541 s = ''.html_safe
541 s = ''.html_safe
542 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
542 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
543 opts.each do |label, value|
543 opts.each do |label, value|
544 value ||= label
544 value ||= label
545 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
545 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
546 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
546 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
547 # set the id on the first tag only
547 # set the id on the first tag only
548 tag_id = nil
548 tag_id = nil
549 s << view.content_tag('label', tag + ' ' + label)
549 s << view.content_tag('label', tag + ' ' + label)
550 end
550 end
551 if custom_value.custom_field.multiple?
551 if custom_value.custom_field.multiple?
552 s << view.hidden_field_tag(tag_name, '')
552 s << view.hidden_field_tag(tag_name, '')
553 end
553 end
554 css = "#{options[:class]} check_box_group"
554 css = "#{options[:class]} check_box_group"
555 view.content_tag('span', s, options.merge(:class => css))
555 view.content_tag('span', s, options.merge(:class => css))
556 end
556 end
557 end
557 end
558
558
559 class ListFormat < List
559 class ListFormat < List
560 add 'list'
560 add 'list'
561 self.searchable_supported = true
561 self.searchable_supported = true
562 self.form_partial = 'custom_fields/formats/list'
562 self.form_partial = 'custom_fields/formats/list'
563
563
564 def possible_custom_value_options(custom_value)
564 def possible_custom_value_options(custom_value)
565 options = possible_values_options(custom_value.custom_field)
565 options = possible_values_options(custom_value.custom_field)
566 missing = [custom_value.value].flatten.reject(&:blank?) - options
566 missing = [custom_value.value].flatten.reject(&:blank?) - options
567 if missing.any?
567 if missing.any?
568 options += missing
568 options += missing
569 end
569 end
570 options
570 options
571 end
571 end
572
572
573 def possible_values_options(custom_field, object=nil)
573 def possible_values_options(custom_field, object=nil)
574 custom_field.possible_values
574 custom_field.possible_values
575 end
575 end
576
576
577 def validate_custom_field(custom_field)
577 def validate_custom_field(custom_field)
578 errors = []
578 errors = []
579 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
579 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
580 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
580 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
581 errors
581 errors
582 end
582 end
583
583
584 def validate_custom_value(custom_value)
584 def validate_custom_value(custom_value)
585 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
585 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
586 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
586 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
587 if invalid_values.any?
587 if invalid_values.any?
588 [::I18n.t('activerecord.errors.messages.inclusion')]
588 [::I18n.t('activerecord.errors.messages.inclusion')]
589 else
589 else
590 []
590 []
591 end
591 end
592 end
592 end
593
593
594 def group_statement(custom_field)
594 def group_statement(custom_field)
595 order_statement(custom_field)
595 order_statement(custom_field)
596 end
596 end
597 end
597 end
598
598
599 class BoolFormat < List
599 class BoolFormat < List
600 add 'bool'
600 add 'bool'
601 self.multiple_supported = false
601 self.multiple_supported = false
602 self.form_partial = 'custom_fields/formats/bool'
602 self.form_partial = 'custom_fields/formats/bool'
603
603
604 def label
604 def label
605 "label_boolean"
605 "label_boolean"
606 end
606 end
607
607
608 def cast_single_value(custom_field, value, customized=nil)
608 def cast_single_value(custom_field, value, customized=nil)
609 value == '1' ? true : false
609 value == '1' ? true : false
610 end
610 end
611
611
612 def possible_values_options(custom_field, object=nil)
612 def possible_values_options(custom_field, object=nil)
613 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
613 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
614 end
614 end
615
615
616 def group_statement(custom_field)
616 def group_statement(custom_field)
617 order_statement(custom_field)
617 order_statement(custom_field)
618 end
618 end
619
619
620 def edit_tag(view, tag_id, tag_name, custom_value, options={})
620 def edit_tag(view, tag_id, tag_name, custom_value, options={})
621 case custom_value.custom_field.edit_tag_style
621 case custom_value.custom_field.edit_tag_style
622 when 'check_box'
622 when 'check_box'
623 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
623 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
624 when 'radio'
624 when 'radio'
625 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
625 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
626 else
626 else
627 select_edit_tag(view, tag_id, tag_name, custom_value, options)
627 select_edit_tag(view, tag_id, tag_name, custom_value, options)
628 end
628 end
629 end
629 end
630
630
631 # Renders the edit tag as a simple check box
631 # Renders the edit tag as a simple check box
632 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
632 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
633 s = ''.html_safe
633 s = ''.html_safe
634 s << view.hidden_field_tag(tag_name, '0', :id => nil)
634 s << view.hidden_field_tag(tag_name, '0', :id => nil)
635 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
635 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
636 view.content_tag('span', s, options)
636 view.content_tag('span', s, options)
637 end
637 end
638 end
638 end
639
639
640 class RecordList < List
640 class RecordList < List
641 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
641 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
642
642
643 def cast_single_value(custom_field, value, customized=nil)
643 def cast_single_value(custom_field, value, customized=nil)
644 target_class.find_by_id(value.to_i) if value.present?
644 target_class.find_by_id(value.to_i) if value.present?
645 end
645 end
646
646
647 def target_class
647 def target_class
648 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
648 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
649 end
649 end
650
650
651 def reset_target_class
651 def reset_target_class
652 @target_class = nil
652 @target_class = nil
653 end
653 end
654
654
655 def possible_custom_value_options(custom_value)
655 def possible_custom_value_options(custom_value)
656 options = possible_values_options(custom_value.custom_field, custom_value.customized)
656 options = possible_values_options(custom_value.custom_field, custom_value.customized)
657 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
657 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
658 if missing.any?
658 if missing.any?
659 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
659 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
660 end
660 end
661 options
661 options
662 end
662 end
663
663
664 def order_statement(custom_field)
664 def order_statement(custom_field)
665 if target_class.respond_to?(:fields_for_order_statement)
665 if target_class.respond_to?(:fields_for_order_statement)
666 target_class.fields_for_order_statement(value_join_alias(custom_field))
666 target_class.fields_for_order_statement(value_join_alias(custom_field))
667 end
667 end
668 end
668 end
669
669
670 def group_statement(custom_field)
670 def group_statement(custom_field)
671 "COALESCE(#{join_alias custom_field}.value, '')"
671 "COALESCE(#{join_alias custom_field}.value, '')"
672 end
672 end
673
673
674 def join_for_order_statement(custom_field)
674 def join_for_order_statement(custom_field)
675 alias_name = join_alias(custom_field)
675 alias_name = join_alias(custom_field)
676
676
677 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
677 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
678 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
678 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
679 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
679 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
680 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
680 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
681 " AND (#{custom_field.visibility_by_project_condition})" +
681 " AND (#{custom_field.visibility_by_project_condition})" +
682 " AND #{alias_name}.value <> ''" +
682 " AND #{alias_name}.value <> ''" +
683 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
683 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
684 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
684 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
685 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
685 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
686 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
686 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
687 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
687 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
688 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
688 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
689 end
689 end
690
690
691 def value_join_alias(custom_field)
691 def value_join_alias(custom_field)
692 join_alias(custom_field) + "_" + custom_field.field_format
692 join_alias(custom_field) + "_" + custom_field.field_format
693 end
693 end
694 protected :value_join_alias
694 protected :value_join_alias
695 end
695 end
696
696
697 class EnumerationFormat < RecordList
697 class EnumerationFormat < RecordList
698 add 'enumeration'
698 add 'enumeration'
699 self.form_partial = 'custom_fields/formats/enumeration'
699 self.form_partial = 'custom_fields/formats/enumeration'
700
700
701 def label
701 def label
702 "label_field_format_enumeration"
702 "label_field_format_enumeration"
703 end
703 end
704
704
705 def target_class
705 def target_class
706 @target_class ||= CustomFieldEnumeration
706 @target_class ||= CustomFieldEnumeration
707 end
707 end
708
708
709 def possible_values_options(custom_field, object=nil)
709 def possible_values_options(custom_field, object=nil)
710 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
710 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
711 end
711 end
712
712
713 def possible_values_records(custom_field, object=nil)
713 def possible_values_records(custom_field, object=nil)
714 custom_field.enumerations.active
714 custom_field.enumerations.active
715 end
715 end
716
716
717 def value_from_keyword(custom_field, keyword, object)
717 def value_from_keyword(custom_field, keyword, object)
718 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword)
718 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword)
719 value ? value.id : nil
719 value ? value.id : nil
720 end
720 end
721 end
721 end
722
722
723 class UserFormat < RecordList
723 class UserFormat < RecordList
724 add 'user'
724 add 'user'
725 self.form_partial = 'custom_fields/formats/user'
725 self.form_partial = 'custom_fields/formats/user'
726 field_attributes :user_role
726 field_attributes :user_role
727
727
728 def possible_values_options(custom_field, object=nil)
728 def possible_values_options(custom_field, object=nil)
729 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
729 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
730 end
730 end
731
731
732 def possible_values_records(custom_field, object=nil)
732 def possible_values_records(custom_field, object=nil)
733 if object.is_a?(Array)
733 if object.is_a?(Array)
734 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
734 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
735 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
735 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
736 elsif object.respond_to?(:project) && object.project
736 elsif object.respond_to?(:project) && object.project
737 scope = object.project.users
737 scope = object.project.users
738 if custom_field.user_role.is_a?(Array)
738 if custom_field.user_role.is_a?(Array)
739 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
739 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
740 if role_ids.any?
740 if role_ids.any?
741 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
741 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
742 end
742 end
743 end
743 end
744 scope.sorted
744 scope.sorted
745 else
745 else
746 []
746 []
747 end
747 end
748 end
748 end
749
749
750 def value_from_keyword(custom_field, keyword, object)
750 def value_from_keyword(custom_field, keyword, object)
751 users = possible_values_records(custom_field, object).to_a
751 users = possible_values_records(custom_field, object).to_a
752 user = Principal.detect_by_keyword(users, keyword)
752 user = Principal.detect_by_keyword(users, keyword)
753 user ? user.id : nil
753 user ? user.id : nil
754 end
754 end
755
755
756 def before_custom_field_save(custom_field)
756 def before_custom_field_save(custom_field)
757 super
757 super
758 if custom_field.user_role.is_a?(Array)
758 if custom_field.user_role.is_a?(Array)
759 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
759 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
760 end
760 end
761 end
761 end
762 end
762 end
763
763
764 class VersionFormat < RecordList
764 class VersionFormat < RecordList
765 add 'version'
765 add 'version'
766 self.form_partial = 'custom_fields/formats/version'
766 self.form_partial = 'custom_fields/formats/version'
767 field_attributes :version_status
767 field_attributes :version_status
768
768
769 def possible_values_options(custom_field, object=nil)
769 def possible_values_options(custom_field, object=nil)
770 versions_options(custom_field, object)
770 versions_options(custom_field, object)
771 end
771 end
772
772
773 def before_custom_field_save(custom_field)
773 def before_custom_field_save(custom_field)
774 super
774 super
775 if custom_field.version_status.is_a?(Array)
775 if custom_field.version_status.is_a?(Array)
776 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
776 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
777 end
777 end
778 end
778 end
779
779
780 protected
780 protected
781
781
782 def query_filter_values(custom_field, query)
782 def query_filter_values(custom_field, query)
783 versions_options(custom_field, query.project, true)
783 versions_options(custom_field, query.project, true)
784 end
784 end
785
785
786 def versions_options(custom_field, object, all_statuses=false)
786 def versions_options(custom_field, object, all_statuses=false)
787 if object.is_a?(Array)
787 if object.is_a?(Array)
788 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
788 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
789 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
789 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
790 elsif object.respond_to?(:project) && object.project
790 elsif object.respond_to?(:project) && object.project
791 scope = object.project.shared_versions
791 scope = object.project.shared_versions
792 if !all_statuses && custom_field.version_status.is_a?(Array)
792 if !all_statuses && custom_field.version_status.is_a?(Array)
793 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
793 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
794 if statuses.any?
794 if statuses.any?
795 scope = scope.where(:status => statuses.map(&:to_s))
795 scope = scope.where(:status => statuses.map(&:to_s))
796 end
796 end
797 end
797 end
798 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
798 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
799 else
799 else
800 []
800 []
801 end
801 end
802 end
802 end
803 end
803 end
804 end
804 end
805 end
805 end
@@ -1,746 +1,773
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 function checkAll(id, checked) {
4 function checkAll(id, checked) {
5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
6 }
6 }
7
7
8 function toggleCheckboxesBySelector(selector) {
8 function toggleCheckboxesBySelector(selector) {
9 var all_checked = true;
9 var all_checked = true;
10 $(selector).each(function(index) {
10 $(selector).each(function(index) {
11 if (!$(this).is(':checked')) { all_checked = false; }
11 if (!$(this).is(':checked')) { all_checked = false; }
12 });
12 });
13 $(selector).prop('checked', !all_checked);
13 $(selector).prop('checked', !all_checked);
14 }
14 }
15
15
16 function showAndScrollTo(id, focus) {
16 function showAndScrollTo(id, focus) {
17 $('#'+id).show();
17 $('#'+id).show();
18 if (focus !== null) {
18 if (focus !== null) {
19 $('#'+focus).focus();
19 $('#'+focus).focus();
20 }
20 }
21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
22 }
22 }
23
23
24 function toggleRowGroup(el) {
24 function toggleRowGroup(el) {
25 var tr = $(el).parents('tr').first();
25 var tr = $(el).parents('tr').first();
26 var n = tr.next();
26 var n = tr.next();
27 tr.toggleClass('open');
27 tr.toggleClass('open');
28 while (n.length && !n.hasClass('group')) {
28 while (n.length && !n.hasClass('group')) {
29 n.toggle();
29 n.toggle();
30 n = n.next('tr');
30 n = n.next('tr');
31 }
31 }
32 }
32 }
33
33
34 function collapseAllRowGroups(el) {
34 function collapseAllRowGroups(el) {
35 var tbody = $(el).parents('tbody').first();
35 var tbody = $(el).parents('tbody').first();
36 tbody.children('tr').each(function(index) {
36 tbody.children('tr').each(function(index) {
37 if ($(this).hasClass('group')) {
37 if ($(this).hasClass('group')) {
38 $(this).removeClass('open');
38 $(this).removeClass('open');
39 } else {
39 } else {
40 $(this).hide();
40 $(this).hide();
41 }
41 }
42 });
42 });
43 }
43 }
44
44
45 function expandAllRowGroups(el) {
45 function expandAllRowGroups(el) {
46 var tbody = $(el).parents('tbody').first();
46 var tbody = $(el).parents('tbody').first();
47 tbody.children('tr').each(function(index) {
47 tbody.children('tr').each(function(index) {
48 if ($(this).hasClass('group')) {
48 if ($(this).hasClass('group')) {
49 $(this).addClass('open');
49 $(this).addClass('open');
50 } else {
50 } else {
51 $(this).show();
51 $(this).show();
52 }
52 }
53 });
53 });
54 }
54 }
55
55
56 function toggleAllRowGroups(el) {
56 function toggleAllRowGroups(el) {
57 var tr = $(el).parents('tr').first();
57 var tr = $(el).parents('tr').first();
58 if (tr.hasClass('open')) {
58 if (tr.hasClass('open')) {
59 collapseAllRowGroups(el);
59 collapseAllRowGroups(el);
60 } else {
60 } else {
61 expandAllRowGroups(el);
61 expandAllRowGroups(el);
62 }
62 }
63 }
63 }
64
64
65 function toggleFieldset(el) {
65 function toggleFieldset(el) {
66 var fieldset = $(el).parents('fieldset').first();
66 var fieldset = $(el).parents('fieldset').first();
67 fieldset.toggleClass('collapsed');
67 fieldset.toggleClass('collapsed');
68 fieldset.children('div').toggle();
68 fieldset.children('div').toggle();
69 }
69 }
70
70
71 function hideFieldset(el) {
71 function hideFieldset(el) {
72 var fieldset = $(el).parents('fieldset').first();
72 var fieldset = $(el).parents('fieldset').first();
73 fieldset.toggleClass('collapsed');
73 fieldset.toggleClass('collapsed');
74 fieldset.children('div').hide();
74 fieldset.children('div').hide();
75 }
75 }
76
76
77 // columns selection
77 // columns selection
78 function moveOptions(theSelFrom, theSelTo) {
78 function moveOptions(theSelFrom, theSelTo) {
79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
80 }
80 }
81
81
82 function moveOptionUp(theSel) {
82 function moveOptionUp(theSel) {
83 $(theSel).find('option:selected').each(function(){
83 $(theSel).find('option:selected').each(function(){
84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
85 });
85 });
86 }
86 }
87
87
88 function moveOptionTop(theSel) {
88 function moveOptionTop(theSel) {
89 $(theSel).find('option:selected').detach().prependTo($(theSel));
89 $(theSel).find('option:selected').detach().prependTo($(theSel));
90 }
90 }
91
91
92 function moveOptionDown(theSel) {
92 function moveOptionDown(theSel) {
93 $($(theSel).find('option:selected').get().reverse()).each(function(){
93 $($(theSel).find('option:selected').get().reverse()).each(function(){
94 $(this).next(':not(:selected)').detach().insertBefore($(this));
94 $(this).next(':not(:selected)').detach().insertBefore($(this));
95 });
95 });
96 }
96 }
97
97
98 function moveOptionBottom(theSel) {
98 function moveOptionBottom(theSel) {
99 $(theSel).find('option:selected').detach().appendTo($(theSel));
99 $(theSel).find('option:selected').detach().appendTo($(theSel));
100 }
100 }
101
101
102 function initFilters() {
102 function initFilters() {
103 $('#add_filter_select').change(function() {
103 $('#add_filter_select').change(function() {
104 addFilter($(this).val(), '', []);
104 addFilter($(this).val(), '', []);
105 });
105 });
106 $('#filters-table td.field input[type=checkbox]').each(function() {
106 $('#filters-table td.field input[type=checkbox]').each(function() {
107 toggleFilter($(this).val());
107 toggleFilter($(this).val());
108 });
108 });
109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
110 toggleFilter($(this).val());
110 toggleFilter($(this).val());
111 });
111 });
112 $('#filters-table').on('click', '.toggle-multiselect', function() {
112 $('#filters-table').on('click', '.toggle-multiselect', function() {
113 toggleMultiSelect($(this).siblings('select'));
113 toggleMultiSelect($(this).siblings('select'));
114 });
114 });
115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
116 if (e.keyCode == 13) $(this).closest('form').submit();
116 if (e.keyCode == 13) $(this).closest('form').submit();
117 });
117 });
118 }
118 }
119
119
120 function addFilter(field, operator, values) {
120 function addFilter(field, operator, values) {
121 var fieldId = field.replace('.', '_');
121 var fieldId = field.replace('.', '_');
122 var tr = $('#tr_'+fieldId);
122 var tr = $('#tr_'+fieldId);
123 if (tr.length > 0) {
123 if (tr.length > 0) {
124 tr.show();
124 tr.show();
125 } else {
125 } else {
126 buildFilterRow(field, operator, values);
126 buildFilterRow(field, operator, values);
127 }
127 }
128 $('#cb_'+fieldId).prop('checked', true);
128 $('#cb_'+fieldId).prop('checked', true);
129 toggleFilter(field);
129 toggleFilter(field);
130 $('#add_filter_select').val('').find('option').each(function() {
130 $('#add_filter_select').val('').find('option').each(function() {
131 if ($(this).attr('value') == field) {
131 if ($(this).attr('value') == field) {
132 $(this).attr('disabled', true);
132 $(this).attr('disabled', true);
133 }
133 }
134 });
134 });
135 }
135 }
136
136
137 function buildFilterRow(field, operator, values) {
137 function buildFilterRow(field, operator, values) {
138 var fieldId = field.replace('.', '_');
138 var fieldId = field.replace('.', '_');
139 var filterTable = $("#filters-table");
139 var filterTable = $("#filters-table");
140 var filterOptions = availableFilters[field];
140 var filterOptions = availableFilters[field];
141 if (!filterOptions) return;
141 if (!filterOptions) return;
142 var operators = operatorByType[filterOptions['type']];
142 var operators = operatorByType[filterOptions['type']];
143 var filterValues = filterOptions['values'];
143 var filterValues = filterOptions['values'];
144 var i, select;
144 var i, select;
145
145
146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
149 '<td class="values"></td>'
149 '<td class="values"></td>'
150 );
150 );
151 filterTable.append(tr);
151 filterTable.append(tr);
152
152
153 select = tr.find('td.operator select');
153 select = tr.find('td.operator select');
154 for (i = 0; i < operators.length; i++) {
154 for (i = 0; i < operators.length; i++) {
155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
156 if (operators[i] == operator) { option.attr('selected', true); }
156 if (operators[i] == operator) { option.attr('selected', true); }
157 select.append(option);
157 select.append(option);
158 }
158 }
159 select.change(function(){ toggleOperator(field); });
159 select.change(function(){ toggleOperator(field); });
160
160
161 switch (filterOptions['type']) {
161 switch (filterOptions['type']) {
162 case "list":
162 case "list":
163 case "list_optional":
163 case "list_optional":
164 case "list_status":
164 case "list_status":
165 case "list_subprojects":
165 case "list_subprojects":
166 tr.find('td.values').append(
166 tr.find('td.values').append(
167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
169 );
169 );
170 select = tr.find('td.values select');
170 select = tr.find('td.values select');
171 if (values.length > 1) { select.attr('multiple', true); }
171 if (values.length > 1) { select.attr('multiple', true); }
172 for (i = 0; i < filterValues.length; i++) {
172 for (i = 0; i < filterValues.length; i++) {
173 var filterValue = filterValues[i];
173 var filterValue = filterValues[i];
174 var option = $('<option>');
174 var option = $('<option>');
175 if ($.isArray(filterValue)) {
175 if ($.isArray(filterValue)) {
176 option.val(filterValue[1]).text(filterValue[0]);
176 option.val(filterValue[1]).text(filterValue[0]);
177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
178 } else {
178 } else {
179 option.val(filterValue).text(filterValue);
179 option.val(filterValue).text(filterValue);
180 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
180 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
181 }
181 }
182 select.append(option);
182 select.append(option);
183 }
183 }
184 break;
184 break;
185 case "date":
185 case "date":
186 case "date_past":
186 case "date_past":
187 tr.find('td.values').append(
187 tr.find('td.values').append(
188 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
188 '<span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
189 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
189 ' <span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
190 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
190 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
191 );
191 );
192 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
192 $('#values_'+fieldId+'_1').val(values[0]).datepickerFallback(datepickerOptions);
193 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
193 $('#values_'+fieldId+'_2').val(values[1]).datepickerFallback(datepickerOptions);
194 $('#values_'+fieldId).val(values[0]);
194 $('#values_'+fieldId).val(values[0]);
195 break;
195 break;
196 case "string":
196 case "string":
197 case "text":
197 case "text":
198 tr.find('td.values').append(
198 tr.find('td.values').append(
199 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
199 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
200 );
200 );
201 $('#values_'+fieldId).val(values[0]);
201 $('#values_'+fieldId).val(values[0]);
202 break;
202 break;
203 case "relation":
203 case "relation":
204 tr.find('td.values').append(
204 tr.find('td.values').append(
205 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
205 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
206 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
206 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
207 );
207 );
208 $('#values_'+fieldId).val(values[0]);
208 $('#values_'+fieldId).val(values[0]);
209 select = tr.find('td.values select');
209 select = tr.find('td.values select');
210 for (i = 0; i < allProjects.length; i++) {
210 for (i = 0; i < allProjects.length; i++) {
211 var filterValue = allProjects[i];
211 var filterValue = allProjects[i];
212 var option = $('<option>');
212 var option = $('<option>');
213 option.val(filterValue[1]).text(filterValue[0]);
213 option.val(filterValue[1]).text(filterValue[0]);
214 if (values[0] == filterValue[1]) { option.attr('selected', true); }
214 if (values[0] == filterValue[1]) { option.attr('selected', true); }
215 select.append(option);
215 select.append(option);
216 }
216 }
217 break;
217 break;
218 case "integer":
218 case "integer":
219 case "float":
219 case "float":
220 case "tree":
220 case "tree":
221 tr.find('td.values').append(
221 tr.find('td.values').append(
222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="14" class="value" /></span>' +
222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="14" class="value" /></span>' +
223 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="14" class="value" /></span>'
223 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="14" class="value" /></span>'
224 );
224 );
225 $('#values_'+fieldId+'_1').val(values[0]);
225 $('#values_'+fieldId+'_1').val(values[0]);
226 $('#values_'+fieldId+'_2').val(values[1]);
226 $('#values_'+fieldId+'_2').val(values[1]);
227 break;
227 break;
228 }
228 }
229 }
229 }
230
230
231 function toggleFilter(field) {
231 function toggleFilter(field) {
232 var fieldId = field.replace('.', '_');
232 var fieldId = field.replace('.', '_');
233 if ($('#cb_' + fieldId).is(':checked')) {
233 if ($('#cb_' + fieldId).is(':checked')) {
234 $("#operators_" + fieldId).show().removeAttr('disabled');
234 $("#operators_" + fieldId).show().removeAttr('disabled');
235 toggleOperator(field);
235 toggleOperator(field);
236 } else {
236 } else {
237 $("#operators_" + fieldId).hide().attr('disabled', true);
237 $("#operators_" + fieldId).hide().attr('disabled', true);
238 enableValues(field, []);
238 enableValues(field, []);
239 }
239 }
240 }
240 }
241
241
242 function enableValues(field, indexes) {
242 function enableValues(field, indexes) {
243 var fieldId = field.replace('.', '_');
243 var fieldId = field.replace('.', '_');
244 $('#tr_'+fieldId+' td.values .value').each(function(index) {
244 $('#tr_'+fieldId+' td.values .value').each(function(index) {
245 if ($.inArray(index, indexes) >= 0) {
245 if ($.inArray(index, indexes) >= 0) {
246 $(this).removeAttr('disabled');
246 $(this).removeAttr('disabled');
247 $(this).parents('span').first().show();
247 $(this).parents('span').first().show();
248 } else {
248 } else {
249 $(this).val('');
249 $(this).val('');
250 $(this).attr('disabled', true);
250 $(this).attr('disabled', true);
251 $(this).parents('span').first().hide();
251 $(this).parents('span').first().hide();
252 }
252 }
253
253
254 if ($(this).hasClass('group')) {
254 if ($(this).hasClass('group')) {
255 $(this).addClass('open');
255 $(this).addClass('open');
256 } else {
256 } else {
257 $(this).show();
257 $(this).show();
258 }
258 }
259 });
259 });
260 }
260 }
261
261
262 function toggleOperator(field) {
262 function toggleOperator(field) {
263 var fieldId = field.replace('.', '_');
263 var fieldId = field.replace('.', '_');
264 var operator = $("#operators_" + fieldId);
264 var operator = $("#operators_" + fieldId);
265 switch (operator.val()) {
265 switch (operator.val()) {
266 case "!*":
266 case "!*":
267 case "*":
267 case "*":
268 case "t":
268 case "t":
269 case "ld":
269 case "ld":
270 case "w":
270 case "w":
271 case "lw":
271 case "lw":
272 case "l2w":
272 case "l2w":
273 case "m":
273 case "m":
274 case "lm":
274 case "lm":
275 case "y":
275 case "y":
276 case "o":
276 case "o":
277 case "c":
277 case "c":
278 case "*o":
278 case "*o":
279 case "!o":
279 case "!o":
280 enableValues(field, []);
280 enableValues(field, []);
281 break;
281 break;
282 case "><":
282 case "><":
283 enableValues(field, [0,1]);
283 enableValues(field, [0,1]);
284 break;
284 break;
285 case "<t+":
285 case "<t+":
286 case ">t+":
286 case ">t+":
287 case "><t+":
287 case "><t+":
288 case "t+":
288 case "t+":
289 case ">t-":
289 case ">t-":
290 case "<t-":
290 case "<t-":
291 case "><t-":
291 case "><t-":
292 case "t-":
292 case "t-":
293 enableValues(field, [2]);
293 enableValues(field, [2]);
294 break;
294 break;
295 case "=p":
295 case "=p":
296 case "=!p":
296 case "=!p":
297 case "!p":
297 case "!p":
298 enableValues(field, [1]);
298 enableValues(field, [1]);
299 break;
299 break;
300 default:
300 default:
301 enableValues(field, [0]);
301 enableValues(field, [0]);
302 break;
302 break;
303 }
303 }
304 }
304 }
305
305
306 function toggleMultiSelect(el) {
306 function toggleMultiSelect(el) {
307 if (el.attr('multiple')) {
307 if (el.attr('multiple')) {
308 el.removeAttr('multiple');
308 el.removeAttr('multiple');
309 el.attr('size', 1);
309 el.attr('size', 1);
310 } else {
310 } else {
311 el.attr('multiple', true);
311 el.attr('multiple', true);
312 if (el.children().length > 10)
312 if (el.children().length > 10)
313 el.attr('size', 10);
313 el.attr('size', 10);
314 else
314 else
315 el.attr('size', 4);
315 el.attr('size', 4);
316 }
316 }
317 }
317 }
318
318
319 function showTab(name, url) {
319 function showTab(name, url) {
320 $('#tab-content-' + name).parent().find('.tab-content').hide();
320 $('#tab-content-' + name).parent().find('.tab-content').hide();
321 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
321 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
322 $('#tab-content-' + name).show();
322 $('#tab-content-' + name).show();
323 $('#tab-' + name).addClass('selected');
323 $('#tab-' + name).addClass('selected');
324 //replaces current URL with the "href" attribute of the current link
324 //replaces current URL with the "href" attribute of the current link
325 //(only triggered if supported by browser)
325 //(only triggered if supported by browser)
326 if ("replaceState" in window.history) {
326 if ("replaceState" in window.history) {
327 window.history.replaceState(null, document.title, url);
327 window.history.replaceState(null, document.title, url);
328 }
328 }
329 return false;
329 return false;
330 }
330 }
331
331
332 function moveTabRight(el) {
332 function moveTabRight(el) {
333 var lis = $(el).parents('div.tabs').first().find('ul').children();
333 var lis = $(el).parents('div.tabs').first().find('ul').children();
334 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
334 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
335 var tabsWidth = 0;
335 var tabsWidth = 0;
336 var i = 0;
336 var i = 0;
337 lis.each(function() {
337 lis.each(function() {
338 if ($(this).is(':visible')) {
338 if ($(this).is(':visible')) {
339 tabsWidth += $(this).outerWidth(true);
339 tabsWidth += $(this).outerWidth(true);
340 }
340 }
341 });
341 });
342 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
342 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
343 $(el).siblings('.tab-left').removeClass('disabled');
343 $(el).siblings('.tab-left').removeClass('disabled');
344 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
344 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
345 var w = lis.eq(i).width();
345 var w = lis.eq(i).width();
346 lis.eq(i).hide();
346 lis.eq(i).hide();
347 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
347 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
348 $(el).addClass('disabled');
348 $(el).addClass('disabled');
349 }
349 }
350 }
350 }
351
351
352 function moveTabLeft(el) {
352 function moveTabLeft(el) {
353 var lis = $(el).parents('div.tabs').first().find('ul').children();
353 var lis = $(el).parents('div.tabs').first().find('ul').children();
354 var i = 0;
354 var i = 0;
355 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
355 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
356 if (i > 0) {
356 if (i > 0) {
357 lis.eq(i-1).show();
357 lis.eq(i-1).show();
358 $(el).siblings('.tab-right').removeClass('disabled');
358 $(el).siblings('.tab-right').removeClass('disabled');
359 }
359 }
360 if (i <= 1) {
360 if (i <= 1) {
361 $(el).addClass('disabled');
361 $(el).addClass('disabled');
362 }
362 }
363 }
363 }
364
364
365 function displayTabsButtons() {
365 function displayTabsButtons() {
366 var lis;
366 var lis;
367 var tabsWidth;
367 var tabsWidth;
368 var el;
368 var el;
369 var numHidden;
369 var numHidden;
370 $('div.tabs').each(function() {
370 $('div.tabs').each(function() {
371 el = $(this);
371 el = $(this);
372 lis = el.find('ul').children();
372 lis = el.find('ul').children();
373 tabsWidth = 0;
373 tabsWidth = 0;
374 numHidden = 0;
374 numHidden = 0;
375 lis.each(function(){
375 lis.each(function(){
376 if ($(this).is(':visible')) {
376 if ($(this).is(':visible')) {
377 tabsWidth += $(this).outerWidth(true);
377 tabsWidth += $(this).outerWidth(true);
378 } else {
378 } else {
379 numHidden++;
379 numHidden++;
380 }
380 }
381 });
381 });
382 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
382 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
383 if ((tabsWidth < el.width() - bw) && (lis.first().is(':visible'))) {
383 if ((tabsWidth < el.width() - bw) && (lis.first().is(':visible'))) {
384 el.find('div.tabs-buttons').hide();
384 el.find('div.tabs-buttons').hide();
385 } else {
385 } else {
386 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
386 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
387 }
387 }
388 });
388 });
389 }
389 }
390
390
391 function setPredecessorFieldsVisibility() {
391 function setPredecessorFieldsVisibility() {
392 var relationType = $('#relation_relation_type');
392 var relationType = $('#relation_relation_type');
393 if (relationType.val() == "precedes" || relationType.val() == "follows") {
393 if (relationType.val() == "precedes" || relationType.val() == "follows") {
394 $('#predecessor_fields').show();
394 $('#predecessor_fields').show();
395 } else {
395 } else {
396 $('#predecessor_fields').hide();
396 $('#predecessor_fields').hide();
397 }
397 }
398 }
398 }
399
399
400 function showModal(id, width, title) {
400 function showModal(id, width, title) {
401 var el = $('#'+id).first();
401 var el = $('#'+id).first();
402 if (el.length === 0 || el.is(':visible')) {return;}
402 if (el.length === 0 || el.is(':visible')) {return;}
403 if (!title) title = el.find('h3.title').text();
403 if (!title) title = el.find('h3.title').text();
404 // moves existing modals behind the transparent background
404 // moves existing modals behind the transparent background
405 $(".modal").zIndex(99);
405 $(".modal").zIndex(99);
406 el.dialog({
406 el.dialog({
407 width: width,
407 width: width,
408 modal: true,
408 modal: true,
409 resizable: false,
409 resizable: false,
410 dialogClass: 'modal',
410 dialogClass: 'modal',
411 title: title
411 title: title
412 }).on('dialogclose', function(){
412 }).on('dialogclose', function(){
413 $(".modal").zIndex(101);
413 $(".modal").zIndex(101);
414 });
414 });
415 el.find("input[type=text], input[type=submit]").first().focus();
415 el.find("input[type=text], input[type=submit]").first().focus();
416 }
416 }
417
417
418 function hideModal(el) {
418 function hideModal(el) {
419 var modal;
419 var modal;
420 if (el) {
420 if (el) {
421 modal = $(el).parents('.ui-dialog-content');
421 modal = $(el).parents('.ui-dialog-content');
422 } else {
422 } else {
423 modal = $('#ajax-modal');
423 modal = $('#ajax-modal');
424 }
424 }
425 modal.dialog("close");
425 modal.dialog("close");
426 }
426 }
427
427
428 function submitPreview(url, form, target) {
428 function submitPreview(url, form, target) {
429 $.ajax({
429 $.ajax({
430 url: url,
430 url: url,
431 type: 'post',
431 type: 'post',
432 data: $('#'+form).serialize(),
432 data: $('#'+form).serialize(),
433 success: function(data){
433 success: function(data){
434 $('#'+target).html(data);
434 $('#'+target).html(data);
435 }
435 }
436 });
436 });
437 }
437 }
438
438
439 function collapseScmEntry(id) {
439 function collapseScmEntry(id) {
440 $('.'+id).each(function() {
440 $('.'+id).each(function() {
441 if ($(this).hasClass('open')) {
441 if ($(this).hasClass('open')) {
442 collapseScmEntry($(this).attr('id'));
442 collapseScmEntry($(this).attr('id'));
443 }
443 }
444 $(this).hide();
444 $(this).hide();
445 });
445 });
446 $('#'+id).removeClass('open');
446 $('#'+id).removeClass('open');
447 }
447 }
448
448
449 function expandScmEntry(id) {
449 function expandScmEntry(id) {
450 $('.'+id).each(function() {
450 $('.'+id).each(function() {
451 $(this).show();
451 $(this).show();
452 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
452 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
453 expandScmEntry($(this).attr('id'));
453 expandScmEntry($(this).attr('id'));
454 }
454 }
455 });
455 });
456 $('#'+id).addClass('open');
456 $('#'+id).addClass('open');
457 }
457 }
458
458
459 function scmEntryClick(id, url) {
459 function scmEntryClick(id, url) {
460 var el = $('#'+id);
460 var el = $('#'+id);
461 if (el.hasClass('open')) {
461 if (el.hasClass('open')) {
462 collapseScmEntry(id);
462 collapseScmEntry(id);
463 el.addClass('collapsed');
463 el.addClass('collapsed');
464 return false;
464 return false;
465 } else if (el.hasClass('loaded')) {
465 } else if (el.hasClass('loaded')) {
466 expandScmEntry(id);
466 expandScmEntry(id);
467 el.removeClass('collapsed');
467 el.removeClass('collapsed');
468 return false;
468 return false;
469 }
469 }
470 if (el.hasClass('loading')) {
470 if (el.hasClass('loading')) {
471 return false;
471 return false;
472 }
472 }
473 el.addClass('loading');
473 el.addClass('loading');
474 $.ajax({
474 $.ajax({
475 url: url,
475 url: url,
476 success: function(data) {
476 success: function(data) {
477 el.after(data);
477 el.after(data);
478 el.addClass('open').addClass('loaded').removeClass('loading');
478 el.addClass('open').addClass('loaded').removeClass('loading');
479 }
479 }
480 });
480 });
481 return true;
481 return true;
482 }
482 }
483
483
484 function randomKey(size) {
484 function randomKey(size) {
485 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
485 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
486 var key = '';
486 var key = '';
487 for (var i = 0; i < size; i++) {
487 for (var i = 0; i < size; i++) {
488 key += chars.charAt(Math.floor(Math.random() * chars.length));
488 key += chars.charAt(Math.floor(Math.random() * chars.length));
489 }
489 }
490 return key;
490 return key;
491 }
491 }
492
492
493 function updateIssueFrom(url, el) {
493 function updateIssueFrom(url, el) {
494 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
494 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
495 $(this).data('valuebeforeupdate', $(this).val());
495 $(this).data('valuebeforeupdate', $(this).val());
496 });
496 });
497 if (el) {
497 if (el) {
498 $("#form_update_triggered_by").val($(el).attr('id'));
498 $("#form_update_triggered_by").val($(el).attr('id'));
499 }
499 }
500 return $.ajax({
500 return $.ajax({
501 url: url,
501 url: url,
502 type: 'post',
502 type: 'post',
503 data: $('#issue-form').serialize()
503 data: $('#issue-form').serialize()
504 });
504 });
505 }
505 }
506
506
507 function replaceIssueFormWith(html){
507 function replaceIssueFormWith(html){
508 var replacement = $(html);
508 var replacement = $(html);
509 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
509 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
510 var object_id = $(this).attr('id');
510 var object_id = $(this).attr('id');
511 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
511 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
512 replacement.find('#'+object_id).val($(this).val());
512 replacement.find('#'+object_id).val($(this).val());
513 }
513 }
514 });
514 });
515 $('#all_attributes').empty();
515 $('#all_attributes').empty();
516 $('#all_attributes').prepend(replacement);
516 $('#all_attributes').prepend(replacement);
517 }
517 }
518
518
519 function updateBulkEditFrom(url) {
519 function updateBulkEditFrom(url) {
520 $.ajax({
520 $.ajax({
521 url: url,
521 url: url,
522 type: 'post',
522 type: 'post',
523 data: $('#bulk_edit_form').serialize()
523 data: $('#bulk_edit_form').serialize()
524 });
524 });
525 }
525 }
526
526
527 function observeAutocompleteField(fieldId, url, options) {
527 function observeAutocompleteField(fieldId, url, options) {
528 $(document).ready(function() {
528 $(document).ready(function() {
529 $('#'+fieldId).autocomplete($.extend({
529 $('#'+fieldId).autocomplete($.extend({
530 source: url,
530 source: url,
531 minLength: 2,
531 minLength: 2,
532 position: {collision: "flipfit"},
532 position: {collision: "flipfit"},
533 search: function(){$('#'+fieldId).addClass('ajax-loading');},
533 search: function(){$('#'+fieldId).addClass('ajax-loading');},
534 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
534 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
535 }, options));
535 }, options));
536 $('#'+fieldId).addClass('autocomplete');
536 $('#'+fieldId).addClass('autocomplete');
537 });
537 });
538 }
538 }
539
539
540 function observeSearchfield(fieldId, targetId, url) {
540 function observeSearchfield(fieldId, targetId, url) {
541 $('#'+fieldId).each(function() {
541 $('#'+fieldId).each(function() {
542 var $this = $(this);
542 var $this = $(this);
543 $this.addClass('autocomplete');
543 $this.addClass('autocomplete');
544 $this.attr('data-value-was', $this.val());
544 $this.attr('data-value-was', $this.val());
545 var check = function() {
545 var check = function() {
546 var val = $this.val();
546 var val = $this.val();
547 if ($this.attr('data-value-was') != val){
547 if ($this.attr('data-value-was') != val){
548 $this.attr('data-value-was', val);
548 $this.attr('data-value-was', val);
549 $.ajax({
549 $.ajax({
550 url: url,
550 url: url,
551 type: 'get',
551 type: 'get',
552 data: {q: $this.val()},
552 data: {q: $this.val()},
553 success: function(data){ if(targetId) $('#'+targetId).html(data); },
553 success: function(data){ if(targetId) $('#'+targetId).html(data); },
554 beforeSend: function(){ $this.addClass('ajax-loading'); },
554 beforeSend: function(){ $this.addClass('ajax-loading'); },
555 complete: function(){ $this.removeClass('ajax-loading'); }
555 complete: function(){ $this.removeClass('ajax-loading'); }
556 });
556 });
557 }
557 }
558 };
558 };
559 var reset = function() {
559 var reset = function() {
560 if (timer) {
560 if (timer) {
561 clearInterval(timer);
561 clearInterval(timer);
562 timer = setInterval(check, 300);
562 timer = setInterval(check, 300);
563 }
563 }
564 };
564 };
565 var timer = setInterval(check, 300);
565 var timer = setInterval(check, 300);
566 $this.bind('keyup click mousemove', reset);
566 $this.bind('keyup click mousemove', reset);
567 });
567 });
568 }
568 }
569
569
570 function beforeShowDatePicker(input, inst) {
570 function beforeShowDatePicker(input, inst) {
571 var default_date = null;
571 var default_date = null;
572 switch ($(input).attr("id")) {
572 switch ($(input).attr("id")) {
573 case "issue_start_date" :
573 case "issue_start_date" :
574 if ($("#issue_due_date").size() > 0) {
574 if ($("#issue_due_date").size() > 0) {
575 default_date = $("#issue_due_date").val();
575 default_date = $("#issue_due_date").val();
576 }
576 }
577 break;
577 break;
578 case "issue_due_date" :
578 case "issue_due_date" :
579 if ($("#issue_start_date").size() > 0) {
579 if ($("#issue_start_date").size() > 0) {
580 var start_date = $("#issue_start_date").val();
580 var start_date = $("#issue_start_date").val();
581 if (start_date != "") {
581 if (start_date != "") {
582 start_date = new Date(Date.parse(start_date));
582 start_date = new Date(Date.parse(start_date));
583 if (start_date > new Date()) {
583 if (start_date > new Date()) {
584 default_date = $("#issue_start_date").val();
584 default_date = $("#issue_start_date").val();
585 }
585 }
586 }
586 }
587 }
587 }
588 break;
588 break;
589 }
589 }
590 $(input).datepicker("option", "defaultDate", default_date);
590 $(input).datepickerFallback("option", "defaultDate", default_date);
591 }
591 }
592
592
593 (function($){
593 (function($){
594 $.fn.positionedItems = function(sortableOptions, options){
594 $.fn.positionedItems = function(sortableOptions, options){
595 var settings = $.extend({
595 var settings = $.extend({
596 firstPosition: 1
596 firstPosition: 1
597 }, options );
597 }, options );
598
598
599 return this.sortable($.extend({
599 return this.sortable($.extend({
600 handle: ".sort-handle",
600 handle: ".sort-handle",
601 helper: function(event, ui){
601 helper: function(event, ui){
602 ui.children('td').each(function(){
602 ui.children('td').each(function(){
603 $(this).width($(this).width());
603 $(this).width($(this).width());
604 });
604 });
605 return ui;
605 return ui;
606 },
606 },
607 update: function(event, ui) {
607 update: function(event, ui) {
608 var sortable = $(this);
608 var sortable = $(this);
609 var handle = ui.item.find(".sort-handle").addClass("ajax-loading");
609 var handle = ui.item.find(".sort-handle").addClass("ajax-loading");
610 var url = handle.data("reorder-url");
610 var url = handle.data("reorder-url");
611 var param = handle.data("reorder-param");
611 var param = handle.data("reorder-param");
612 var data = {};
612 var data = {};
613 data[param] = {position: ui.item.index() + settings['firstPosition']};
613 data[param] = {position: ui.item.index() + settings['firstPosition']};
614 $.ajax({
614 $.ajax({
615 url: url,
615 url: url,
616 type: 'put',
616 type: 'put',
617 dataType: 'script',
617 dataType: 'script',
618 data: data,
618 data: data,
619 success: function(data){
619 success: function(data){
620 sortable.children(":even").removeClass("even").addClass("odd");
620 sortable.children(":even").removeClass("even").addClass("odd");
621 sortable.children(":odd").removeClass("odd").addClass("even");
621 sortable.children(":odd").removeClass("odd").addClass("even");
622 },
622 },
623 error: function(jqXHR, textStatus, errorThrown){
623 error: function(jqXHR, textStatus, errorThrown){
624 alert(jqXHR.status);
624 alert(jqXHR.status);
625 sortable.sortable("cancel");
625 sortable.sortable("cancel");
626 },
626 },
627 complete: function(jqXHR, textStatus, errorThrown){
627 complete: function(jqXHR, textStatus, errorThrown){
628 handle.removeClass("ajax-loading");
628 handle.removeClass("ajax-loading");
629 }
629 }
630 });
630 });
631 },
631 },
632 }, sortableOptions));
632 }, sortableOptions));
633 }
633 }
634 }( jQuery ));
634 }( jQuery ));
635
635
636 function initMyPageSortable(list, url) {
636 function initMyPageSortable(list, url) {
637 $('#list-'+list).sortable({
637 $('#list-'+list).sortable({
638 connectWith: '.block-receiver',
638 connectWith: '.block-receiver',
639 tolerance: 'pointer',
639 tolerance: 'pointer',
640 update: function(){
640 update: function(){
641 $.ajax({
641 $.ajax({
642 url: url,
642 url: url,
643 type: 'post',
643 type: 'post',
644 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
644 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
645 });
645 });
646 }
646 }
647 });
647 });
648 $("#list-top, #list-left, #list-right").disableSelection();
648 $("#list-top, #list-left, #list-right").disableSelection();
649 }
649 }
650
650
651 var warnLeavingUnsavedMessage;
651 var warnLeavingUnsavedMessage;
652 function warnLeavingUnsaved(message) {
652 function warnLeavingUnsaved(message) {
653 warnLeavingUnsavedMessage = message;
653 warnLeavingUnsavedMessage = message;
654 $(document).on('submit', 'form', function(){
654 $(document).on('submit', 'form', function(){
655 $('textarea').removeData('changed');
655 $('textarea').removeData('changed');
656 });
656 });
657 $(document).on('change', 'textarea', function(){
657 $(document).on('change', 'textarea', function(){
658 $(this).data('changed', 'changed');
658 $(this).data('changed', 'changed');
659 });
659 });
660 window.onbeforeunload = function(){
660 window.onbeforeunload = function(){
661 var warn = false;
661 var warn = false;
662 $('textarea').blur().each(function(){
662 $('textarea').blur().each(function(){
663 if ($(this).data('changed')) {
663 if ($(this).data('changed')) {
664 warn = true;
664 warn = true;
665 }
665 }
666 });
666 });
667 if (warn) {return warnLeavingUnsavedMessage;}
667 if (warn) {return warnLeavingUnsavedMessage;}
668 };
668 };
669 }
669 }
670
670
671 function setupAjaxIndicator() {
671 function setupAjaxIndicator() {
672 $(document).bind('ajaxSend', function(event, xhr, settings) {
672 $(document).bind('ajaxSend', function(event, xhr, settings) {
673 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
673 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
674 $('#ajax-indicator').show();
674 $('#ajax-indicator').show();
675 }
675 }
676 });
676 });
677 $(document).bind('ajaxStop', function() {
677 $(document).bind('ajaxStop', function() {
678 $('#ajax-indicator').hide();
678 $('#ajax-indicator').hide();
679 });
679 });
680 }
680 }
681
681
682 function setupTabs() {
682 function setupTabs() {
683 if($('.tabs').length > 0) {
683 if($('.tabs').length > 0) {
684 displayTabsButtons();
684 displayTabsButtons();
685 $(window).resize(displayTabsButtons);
685 $(window).resize(displayTabsButtons);
686 }
686 }
687 }
687 }
688
688
689 function hideOnLoad() {
689 function hideOnLoad() {
690 $('.hol').hide();
690 $('.hol').hide();
691 }
691 }
692
692
693 function addFormObserversForDoubleSubmit() {
693 function addFormObserversForDoubleSubmit() {
694 $('form[method=post]').each(function() {
694 $('form[method=post]').each(function() {
695 if (!$(this).hasClass('multiple-submit')) {
695 if (!$(this).hasClass('multiple-submit')) {
696 $(this).submit(function(form_submission) {
696 $(this).submit(function(form_submission) {
697 if ($(form_submission.target).attr('data-submitted')) {
697 if ($(form_submission.target).attr('data-submitted')) {
698 form_submission.preventDefault();
698 form_submission.preventDefault();
699 } else {
699 } else {
700 $(form_submission.target).attr('data-submitted', true);
700 $(form_submission.target).attr('data-submitted', true);
701 }
701 }
702 });
702 });
703 }
703 }
704 });
704 });
705 }
705 }
706
706
707 function defaultFocus(){
707 function defaultFocus(){
708 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
708 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
709 $('#content input[type=text], #content textarea').first().focus();
709 $('#content input[type=text], #content textarea').first().focus();
710 }
710 }
711 }
711 }
712
712
713 function blockEventPropagation(event) {
713 function blockEventPropagation(event) {
714 event.stopPropagation();
714 event.stopPropagation();
715 event.preventDefault();
715 event.preventDefault();
716 }
716 }
717
717
718 function toggleDisabledOnChange() {
718 function toggleDisabledOnChange() {
719 var checked = $(this).is(':checked');
719 var checked = $(this).is(':checked');
720 $($(this).data('disables')).attr('disabled', checked);
720 $($(this).data('disables')).attr('disabled', checked);
721 $($(this).data('enables')).attr('disabled', !checked);
721 $($(this).data('enables')).attr('disabled', !checked);
722 }
722 }
723 function toggleDisabledInit() {
723 function toggleDisabledInit() {
724 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
724 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
725 }
725 }
726
727 (function ( $ ) {
728
729 // detect if native date input is supported
730 var nativeDateInputSupported = true;
731
732 var input = document.createElement('input');
733 input.setAttribute('type','date');
734 if (input.type === 'text') {
735 nativeDateInputSupported = false;
736 }
737
738 var notADateValue = 'not-a-date';
739 input.setAttribute('value', notADateValue);
740 if (input.value === notADateValue) {
741 nativeDateInputSupported = false;
742 }
743
744 $.fn.datepickerFallback = function( options ) {
745 if (nativeDateInputSupported) {
746 return this;
747 } else {
748 return this.datepicker( options );
749 }
750 };
751 }( jQuery ));
752
726 $(document).ready(function(){
753 $(document).ready(function(){
727 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
754 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
728 toggleDisabledInit();
755 toggleDisabledInit();
729 });
756 });
730
757
731 function keepAnchorOnSignIn(form){
758 function keepAnchorOnSignIn(form){
732 var hash = decodeURIComponent(self.document.location.hash);
759 var hash = decodeURIComponent(self.document.location.hash);
733 if (hash) {
760 if (hash) {
734 if (hash.indexOf("#") === -1) {
761 if (hash.indexOf("#") === -1) {
735 hash = "#" + hash;
762 hash = "#" + hash;
736 }
763 }
737 form.action = form.action + hash;
764 form.action = form.action + hash;
738 }
765 }
739 return true;
766 return true;
740 }
767 }
741
768
742 $(document).ready(setupAjaxIndicator);
769 $(document).ready(setupAjaxIndicator);
743 $(document).ready(hideOnLoad);
770 $(document).ready(hideOnLoad);
744 $(document).ready(addFormObserversForDoubleSubmit);
771 $(document).ready(addFormObserversForDoubleSubmit);
745 $(document).ready(defaultFocus);
772 $(document).ready(defaultFocus);
746 $(document).ready(setupTabs);
773 $(document).ready(setupTabs);
General Comments 0
You need to be logged in to leave comments. Login now