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