##// END OF EJS Templates
Makes some attributes optional in API response to get faster/lightweight responses....
Jean-Philippe Lang -
r4372:5f57bceabbdc
parent child
Show More
@@ -1,885 +1,895
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 'forwardable'
18 require 'forwardable'
19 require 'cgi'
19 require 'cgi'
20
20
21 module ApplicationHelper
21 module ApplicationHelper
22 include Redmine::WikiFormatting::Macros::Definitions
22 include Redmine::WikiFormatting::Macros::Definitions
23 include Redmine::I18n
23 include Redmine::I18n
24 include GravatarHelper::PublicMethods
24 include GravatarHelper::PublicMethods
25
25
26 extend Forwardable
26 extend Forwardable
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28
28
29 # Return true if user is authorized for controller/action, otherwise false
29 # Return true if user is authorized for controller/action, otherwise false
30 def authorize_for(controller, action)
30 def authorize_for(controller, action)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 end
32 end
33
33
34 # Display a link if user is authorized
34 # Display a link if user is authorized
35 #
35 #
36 # @param [String] name Anchor text (passed to link_to)
36 # @param [String] name Anchor text (passed to link_to)
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 # @param [optional, Hash] html_options Options passed to link_to
38 # @param [optional, Hash] html_options Options passed to link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
42 end
43
43
44 # Display a link to remote if user is authorized
44 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
46 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active?
54 if user.active?
55 link_to name, :controller => 'users', :action => 'show', :id => user
55 link_to name, :controller => 'users', :action => 'show', :id => user
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 #
71 #
72 def link_to_issue(issue, options={})
72 def link_to_issue(issue, options={})
73 title = nil
73 title = nil
74 subject = nil
74 subject = nil
75 if options[:subject] == false
75 if options[:subject] == false
76 title = truncate(issue.subject, :length => 60)
76 title = truncate(issue.subject, :length => 60)
77 else
77 else
78 subject = issue.subject
78 subject = issue.subject
79 if options[:truncate]
79 if options[:truncate]
80 subject = truncate(subject, :length => options[:truncate])
80 subject = truncate(subject, :length => options[:truncate])
81 end
81 end
82 end
82 end
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 :class => issue.css_classes,
84 :class => issue.css_classes,
85 :title => title
85 :title => title
86 s << ": #{h subject}" if subject
86 s << ": #{h subject}" if subject
87 s = "#{h issue.project} - " + s if options[:project]
87 s = "#{h issue.project} - " + s if options[:project]
88 s
88 s
89 end
89 end
90
90
91 # Generates a link to an attachment.
91 # Generates a link to an attachment.
92 # Options:
92 # Options:
93 # * :text - Link text (default to attachment filename)
93 # * :text - Link text (default to attachment filename)
94 # * :download - Force download (default: false)
94 # * :download - Force download (default: false)
95 def link_to_attachment(attachment, options={})
95 def link_to_attachment(attachment, options={})
96 text = options.delete(:text) || attachment.filename
96 text = options.delete(:text) || attachment.filename
97 action = options.delete(:download) ? 'download' : 'show'
97 action = options.delete(:download) ? 'download' : 'show'
98
98
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 end
100 end
101
101
102 # Generates a link to a SCM revision
102 # Generates a link to a SCM revision
103 # Options:
103 # Options:
104 # * :text - Link text (default to the formatted revision)
104 # * :text - Link text (default to the formatted revision)
105 def link_to_revision(revision, project, options={})
105 def link_to_revision(revision, project, options={})
106 text = options.delete(:text) || format_revision(revision)
106 text = options.delete(:text) || format_revision(revision)
107
107
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 end
109 end
110
110
111 # Generates a link to a project if active
111 # Generates a link to a project if active
112 # Examples:
112 # Examples:
113 #
113 #
114 # link_to_project(project) # => link to the specified project overview
114 # link_to_project(project) # => link to the specified project overview
115 # link_to_project(project, :action=>'settings') # => link to project settings
115 # link_to_project(project, :action=>'settings') # => link to project settings
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 #
118 #
119 def link_to_project(project, options={}, html_options = nil)
119 def link_to_project(project, options={}, html_options = nil)
120 if project.active?
120 if project.active?
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 link_to(h(project), url, html_options)
122 link_to(h(project), url, html_options)
123 else
123 else
124 h(project)
124 h(project)
125 end
125 end
126 end
126 end
127
127
128 def toggle_link(name, id, options={})
128 def toggle_link(name, id, options={})
129 onclick = "Element.toggle('#{id}'); "
129 onclick = "Element.toggle('#{id}'); "
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 onclick << "return false;"
131 onclick << "return false;"
132 link_to(name, "#", :onclick => onclick)
132 link_to(name, "#", :onclick => onclick)
133 end
133 end
134
134
135 def image_to_function(name, function, html_options = {})
135 def image_to_function(name, function, html_options = {})
136 html_options.symbolize_keys!
136 html_options.symbolize_keys!
137 tag(:input, html_options.merge({
137 tag(:input, html_options.merge({
138 :type => "image", :src => image_path(name),
138 :type => "image", :src => image_path(name),
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 }))
140 }))
141 end
141 end
142
142
143 def prompt_to_remote(name, text, param, url, html_options = {})
143 def prompt_to_remote(name, text, param, url, html_options = {})
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 link_to name, {}, html_options
145 link_to name, {}, html_options
146 end
146 end
147
147
148 def format_activity_title(text)
148 def format_activity_title(text)
149 h(truncate_single_line(text, :length => 100))
149 h(truncate_single_line(text, :length => 100))
150 end
150 end
151
151
152 def format_activity_day(date)
152 def format_activity_day(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 end
154 end
155
155
156 def format_activity_description(text)
156 def format_activity_description(text)
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 end
158 end
159
159
160 def format_version_name(version)
160 def format_version_name(version)
161 if version.project == @project
161 if version.project == @project
162 h(version)
162 h(version)
163 else
163 else
164 h("#{version.project} - #{version}")
164 h("#{version.project} - #{version}")
165 end
165 end
166 end
166 end
167
167
168 def due_date_distance_in_words(date)
168 def due_date_distance_in_words(date)
169 if date
169 if date
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 end
171 end
172 end
172 end
173
173
174 def render_page_hierarchy(pages, node=nil)
174 def render_page_hierarchy(pages, node=nil)
175 content = ''
175 content = ''
176 if pages[node]
176 if pages[node]
177 content << "<ul class=\"pages-hierarchy\">\n"
177 content << "<ul class=\"pages-hierarchy\">\n"
178 pages[node].each do |page|
178 pages[node].each do |page|
179 content << "<li>"
179 content << "<li>"
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 content << "</li>\n"
183 content << "</li>\n"
184 end
184 end
185 content << "</ul>\n"
185 content << "</ul>\n"
186 end
186 end
187 content
187 content
188 end
188 end
189
189
190 # Renders flash messages
190 # Renders flash messages
191 def render_flash_messages
191 def render_flash_messages
192 s = ''
192 s = ''
193 flash.each do |k,v|
193 flash.each do |k,v|
194 s << content_tag('div', v, :class => "flash #{k}")
194 s << content_tag('div', v, :class => "flash #{k}")
195 end
195 end
196 s
196 s
197 end
197 end
198
198
199 # Renders tabs and their content
199 # Renders tabs and their content
200 def render_tabs(tabs)
200 def render_tabs(tabs)
201 if tabs.any?
201 if tabs.any?
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 else
203 else
204 content_tag 'p', l(:label_no_data), :class => "nodata"
204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 end
205 end
206 end
206 end
207
207
208 # Renders the project quick-jump box
208 # Renders the project quick-jump box
209 def render_project_jump_box
209 def render_project_jump_box
210 # Retrieve them now to avoid a COUNT query
210 # Retrieve them now to avoid a COUNT query
211 projects = User.current.projects.all
211 projects = User.current.projects.all
212 if projects.any?
212 if projects.any?
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 '<option value="" disabled="disabled">---</option>'
215 '<option value="" disabled="disabled">---</option>'
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 end
218 end
219 s << '</select>'
219 s << '</select>'
220 s
220 s
221 end
221 end
222 end
222 end
223
223
224 def project_tree_options_for_select(projects, options = {})
224 def project_tree_options_for_select(projects, options = {})
225 s = ''
225 s = ''
226 project_tree(projects) do |project, level|
226 project_tree(projects) do |project, level|
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 tag_options = {:value => project.id}
228 tag_options = {:value => project.id}
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 tag_options[:selected] = 'selected'
230 tag_options[:selected] = 'selected'
231 else
231 else
232 tag_options[:selected] = nil
232 tag_options[:selected] = nil
233 end
233 end
234 tag_options.merge!(yield(project)) if block_given?
234 tag_options.merge!(yield(project)) if block_given?
235 s << content_tag('option', name_prefix + h(project), tag_options)
235 s << content_tag('option', name_prefix + h(project), tag_options)
236 end
236 end
237 s
237 s
238 end
238 end
239
239
240 # Yields the given block for each project with its level in the tree
240 # Yields the given block for each project with its level in the tree
241 #
241 #
242 # Wrapper for Project#project_tree
242 # Wrapper for Project#project_tree
243 def project_tree(projects, &block)
243 def project_tree(projects, &block)
244 Project.project_tree(projects, &block)
244 Project.project_tree(projects, &block)
245 end
245 end
246
246
247 def project_nested_ul(projects, &block)
247 def project_nested_ul(projects, &block)
248 s = ''
248 s = ''
249 if projects.any?
249 if projects.any?
250 ancestors = []
250 ancestors = []
251 projects.sort_by(&:lft).each do |project|
251 projects.sort_by(&:lft).each do |project|
252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 s << "<ul>\n"
253 s << "<ul>\n"
254 else
254 else
255 ancestors.pop
255 ancestors.pop
256 s << "</li>"
256 s << "</li>"
257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 ancestors.pop
258 ancestors.pop
259 s << "</ul></li>\n"
259 s << "</ul></li>\n"
260 end
260 end
261 end
261 end
262 s << "<li>"
262 s << "<li>"
263 s << yield(project).to_s
263 s << yield(project).to_s
264 ancestors << project
264 ancestors << project
265 end
265 end
266 s << ("</li></ul>\n" * ancestors.size)
266 s << ("</li></ul>\n" * ancestors.size)
267 end
267 end
268 s
268 s
269 end
269 end
270
270
271 def principals_check_box_tags(name, principals)
271 def principals_check_box_tags(name, principals)
272 s = ''
272 s = ''
273 principals.sort.each do |principal|
273 principals.sort.each do |principal|
274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 end
275 end
276 s
276 s
277 end
277 end
278
278
279 # Truncates and returns the string as a single line
279 # Truncates and returns the string as a single line
280 def truncate_single_line(string, *args)
280 def truncate_single_line(string, *args)
281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 end
282 end
283
283
284 # Truncates at line break after 250 characters or options[:length]
284 # Truncates at line break after 250 characters or options[:length]
285 def truncate_lines(string, options={})
285 def truncate_lines(string, options={})
286 length = options[:length] || 250
286 length = options[:length] || 250
287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 "#{$1}..."
288 "#{$1}..."
289 else
289 else
290 string
290 string
291 end
291 end
292 end
292 end
293
293
294 def html_hours(text)
294 def html_hours(text)
295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 end
296 end
297
297
298 def authoring(created, author, options={})
298 def authoring(created, author, options={})
299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 end
300 end
301
301
302 def time_tag(time)
302 def time_tag(time)
303 text = distance_of_time_in_words(Time.now, time)
303 text = distance_of_time_in_words(Time.now, time)
304 if @project
304 if @project
305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 else
306 else
307 content_tag('acronym', text, :title => format_time(time))
307 content_tag('acronym', text, :title => format_time(time))
308 end
308 end
309 end
309 end
310
310
311 def syntax_highlight(name, content)
311 def syntax_highlight(name, content)
312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 end
313 end
314
314
315 def to_path_param(path)
315 def to_path_param(path)
316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 end
317 end
318
318
319 def pagination_links_full(paginator, count=nil, options={})
319 def pagination_links_full(paginator, count=nil, options={})
320 page_param = options.delete(:page_param) || :page
320 page_param = options.delete(:page_param) || :page
321 per_page_links = options.delete(:per_page_links)
321 per_page_links = options.delete(:per_page_links)
322 url_param = params.dup
322 url_param = params.dup
323 # don't reuse query params if filters are present
323 # don't reuse query params if filters are present
324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325
325
326 html = ''
326 html = ''
327 if paginator.current.previous
327 if paginator.current.previous
328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 end
329 end
330
330
331 html << (pagination_links_each(paginator, options) do |n|
331 html << (pagination_links_each(paginator, options) do |n|
332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 end || '')
333 end || '')
334
334
335 if paginator.current.next
335 if paginator.current.next
336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 end
337 end
338
338
339 unless count.nil?
339 unless count.nil?
340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 html << " | #{links}"
342 html << " | #{links}"
343 end
343 end
344 end
344 end
345
345
346 html
346 html
347 end
347 end
348
348
349 def per_page_links(selected=nil)
349 def per_page_links(selected=nil)
350 url_param = params.dup
350 url_param = params.dup
351 url_param.clear if url_param.has_key?(:set_filter)
351 url_param.clear if url_param.has_key?(:set_filter)
352
352
353 links = Setting.per_page_options_array.collect do |n|
353 links = Setting.per_page_options_array.collect do |n|
354 n == selected ? n : link_to_remote(n, {:update => "content",
354 n == selected ? n : link_to_remote(n, {:update => "content",
355 :url => params.dup.merge(:per_page => n),
355 :url => params.dup.merge(:per_page => n),
356 :method => :get},
356 :method => :get},
357 {:href => url_for(url_param.merge(:per_page => n))})
357 {:href => url_for(url_param.merge(:per_page => n))})
358 end
358 end
359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 end
360 end
361
361
362 def reorder_links(name, url)
362 def reorder_links(name, url)
363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
367 end
367 end
368
368
369 def breadcrumb(*args)
369 def breadcrumb(*args)
370 elements = args.flatten
370 elements = args.flatten
371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 end
372 end
373
373
374 def other_formats_links(&block)
374 def other_formats_links(&block)
375 concat('<p class="other-formats">' + l(:label_export_to))
375 concat('<p class="other-formats">' + l(:label_export_to))
376 yield Redmine::Views::OtherFormatsBuilder.new(self)
376 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 concat('</p>')
377 concat('</p>')
378 end
378 end
379
379
380 def page_header_title
380 def page_header_title
381 if @project.nil? || @project.new_record?
381 if @project.nil? || @project.new_record?
382 h(Setting.app_title)
382 h(Setting.app_title)
383 else
383 else
384 b = []
384 b = []
385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 if ancestors.any?
386 if ancestors.any?
387 root = ancestors.shift
387 root = ancestors.shift
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 if ancestors.size > 2
389 if ancestors.size > 2
390 b << '&#8230;'
390 b << '&#8230;'
391 ancestors = ancestors[-2, 2]
391 ancestors = ancestors[-2, 2]
392 end
392 end
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 end
394 end
395 b << h(@project)
395 b << h(@project)
396 b.join(' &#187; ')
396 b.join(' &#187; ')
397 end
397 end
398 end
398 end
399
399
400 def html_title(*args)
400 def html_title(*args)
401 if args.empty?
401 if args.empty?
402 title = []
402 title = []
403 title << @project.name if @project
403 title << @project.name if @project
404 title += @html_title if @html_title
404 title += @html_title if @html_title
405 title << Setting.app_title
405 title << Setting.app_title
406 title.select {|t| !t.blank? }.join(' - ')
406 title.select {|t| !t.blank? }.join(' - ')
407 else
407 else
408 @html_title ||= []
408 @html_title ||= []
409 @html_title += args
409 @html_title += args
410 end
410 end
411 end
411 end
412
412
413 # Returns the theme, controller name, and action as css classes for the
413 # Returns the theme, controller name, and action as css classes for the
414 # HTML body.
414 # HTML body.
415 def body_css_classes
415 def body_css_classes
416 css = []
416 css = []
417 if theme = Redmine::Themes.theme(Setting.ui_theme)
417 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 css << 'theme-' + theme.name
418 css << 'theme-' + theme.name
419 end
419 end
420
420
421 css << 'controller-' + params[:controller]
421 css << 'controller-' + params[:controller]
422 css << 'action-' + params[:action]
422 css << 'action-' + params[:action]
423 css.join(' ')
423 css.join(' ')
424 end
424 end
425
425
426 def accesskey(s)
426 def accesskey(s)
427 Redmine::AccessKeys.key_for s
427 Redmine::AccessKeys.key_for s
428 end
428 end
429
429
430 # Formats text according to system settings.
430 # Formats text according to system settings.
431 # 2 ways to call this method:
431 # 2 ways to call this method:
432 # * with a String: textilizable(text, options)
432 # * with a String: textilizable(text, options)
433 # * with an object and one of its attribute: textilizable(issue, :description, options)
433 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 def textilizable(*args)
434 def textilizable(*args)
435 options = args.last.is_a?(Hash) ? args.pop : {}
435 options = args.last.is_a?(Hash) ? args.pop : {}
436 case args.size
436 case args.size
437 when 1
437 when 1
438 obj = options[:object]
438 obj = options[:object]
439 text = args.shift
439 text = args.shift
440 when 2
440 when 2
441 obj = args.shift
441 obj = args.shift
442 attr = args.shift
442 attr = args.shift
443 text = obj.send(attr).to_s
443 text = obj.send(attr).to_s
444 else
444 else
445 raise ArgumentError, 'invalid arguments to textilizable'
445 raise ArgumentError, 'invalid arguments to textilizable'
446 end
446 end
447 return '' if text.blank?
447 return '' if text.blank?
448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 only_path = options.delete(:only_path) == false ? false : true
449 only_path = options.delete(:only_path) == false ? false : true
450
450
451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452
452
453 parse_non_pre_blocks(text) do |text|
453 parse_non_pre_blocks(text) do |text|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
455 send method_name, text, project, obj, attr, only_path, options
455 send method_name, text, project, obj, attr, only_path, options
456 end
456 end
457 end
457 end
458 end
458 end
459
459
460 def parse_non_pre_blocks(text)
460 def parse_non_pre_blocks(text)
461 s = StringScanner.new(text)
461 s = StringScanner.new(text)
462 tags = []
462 tags = []
463 parsed = ''
463 parsed = ''
464 while !s.eos?
464 while !s.eos?
465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
467 if tags.empty?
467 if tags.empty?
468 yield text
468 yield text
469 end
469 end
470 parsed << text
470 parsed << text
471 if tag
471 if tag
472 if closing
472 if closing
473 if tags.last == tag.downcase
473 if tags.last == tag.downcase
474 tags.pop
474 tags.pop
475 end
475 end
476 else
476 else
477 tags << tag.downcase
477 tags << tag.downcase
478 end
478 end
479 parsed << full_tag
479 parsed << full_tag
480 end
480 end
481 end
481 end
482 # Close any non closing tags
482 # Close any non closing tags
483 while tag = tags.pop
483 while tag = tags.pop
484 parsed << "</#{tag}>"
484 parsed << "</#{tag}>"
485 end
485 end
486 parsed
486 parsed
487 end
487 end
488
488
489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
490 # when using an image link, try to use an attachment, if possible
490 # when using an image link, try to use an attachment, if possible
491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
492 attachments = nil
492 attachments = nil
493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
496 # search for the picture in attachments
496 # search for the picture in attachments
497 if found = attachments.detect { |att| att.filename.downcase == filename }
497 if found = attachments.detect { |att| att.filename.downcase == filename }
498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
499 desc = found.description.to_s.gsub('"', '')
499 desc = found.description.to_s.gsub('"', '')
500 if !desc.blank? && alttext.blank?
500 if !desc.blank? && alttext.blank?
501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
502 end
502 end
503 "src=\"#{image_url}\"#{alt}"
503 "src=\"#{image_url}\"#{alt}"
504 else
504 else
505 m
505 m
506 end
506 end
507 end
507 end
508 end
508 end
509 end
509 end
510
510
511 # Wiki links
511 # Wiki links
512 #
512 #
513 # Examples:
513 # Examples:
514 # [[mypage]]
514 # [[mypage]]
515 # [[mypage|mytext]]
515 # [[mypage|mytext]]
516 # wiki links can refer other project wikis, using project name or identifier:
516 # wiki links can refer other project wikis, using project name or identifier:
517 # [[project:]] -> wiki starting page
517 # [[project:]] -> wiki starting page
518 # [[project:|mytext]]
518 # [[project:|mytext]]
519 # [[project:mypage]]
519 # [[project:mypage]]
520 # [[project:mypage|mytext]]
520 # [[project:mypage|mytext]]
521 def parse_wiki_links(text, project, obj, attr, only_path, options)
521 def parse_wiki_links(text, project, obj, attr, only_path, options)
522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
523 link_project = project
523 link_project = project
524 esc, all, page, title = $1, $2, $3, $5
524 esc, all, page, title = $1, $2, $3, $5
525 if esc.nil?
525 if esc.nil?
526 if page =~ /^([^\:]+)\:(.*)$/
526 if page =~ /^([^\:]+)\:(.*)$/
527 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
527 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
528 page = $2
528 page = $2
529 title ||= $1 if page.blank?
529 title ||= $1 if page.blank?
530 end
530 end
531
531
532 if link_project && link_project.wiki
532 if link_project && link_project.wiki
533 # extract anchor
533 # extract anchor
534 anchor = nil
534 anchor = nil
535 if page =~ /^(.+?)\#(.+)$/
535 if page =~ /^(.+?)\#(.+)$/
536 page, anchor = $1, $2
536 page, anchor = $1, $2
537 end
537 end
538 # check if page exists
538 # check if page exists
539 wiki_page = link_project.wiki.find_page(page)
539 wiki_page = link_project.wiki.find_page(page)
540 url = case options[:wiki_links]
540 url = case options[:wiki_links]
541 when :local; "#{title}.html"
541 when :local; "#{title}.html"
542 when :anchor; "##{title}" # used for single-file wiki export
542 when :anchor; "##{title}" # used for single-file wiki export
543 else
543 else
544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
546 end
546 end
547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
548 else
548 else
549 # project or wiki doesn't exist
549 # project or wiki doesn't exist
550 all
550 all
551 end
551 end
552 else
552 else
553 all
553 all
554 end
554 end
555 end
555 end
556 end
556 end
557
557
558 # Redmine links
558 # Redmine links
559 #
559 #
560 # Examples:
560 # Examples:
561 # Issues:
561 # Issues:
562 # #52 -> Link to issue #52
562 # #52 -> Link to issue #52
563 # Changesets:
563 # Changesets:
564 # r52 -> Link to revision 52
564 # r52 -> Link to revision 52
565 # commit:a85130f -> Link to scmid starting with a85130f
565 # commit:a85130f -> Link to scmid starting with a85130f
566 # Documents:
566 # Documents:
567 # document#17 -> Link to document with id 17
567 # document#17 -> Link to document with id 17
568 # document:Greetings -> Link to the document with title "Greetings"
568 # document:Greetings -> Link to the document with title "Greetings"
569 # document:"Some document" -> Link to the document with title "Some document"
569 # document:"Some document" -> Link to the document with title "Some document"
570 # Versions:
570 # Versions:
571 # version#3 -> Link to version with id 3
571 # version#3 -> Link to version with id 3
572 # version:1.0.0 -> Link to version named "1.0.0"
572 # version:1.0.0 -> Link to version named "1.0.0"
573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
574 # Attachments:
574 # Attachments:
575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
576 # Source files:
576 # Source files:
577 # source:some/file -> Link to the file located at /some/file in the project's repository
577 # source:some/file -> Link to the file located at /some/file in the project's repository
578 # source:some/file@52 -> Link to the file's revision 52
578 # source:some/file@52 -> Link to the file's revision 52
579 # source:some/file#L120 -> Link to line 120 of the file
579 # source:some/file#L120 -> Link to line 120 of the file
580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
581 # export:some/file -> Force the download of the file
581 # export:some/file -> Force the download of the file
582 # Forum messages:
582 # Forum messages:
583 # message#1218 -> Link to message with id 1218
583 # message#1218 -> Link to message with id 1218
584 def parse_redmine_links(text, project, obj, attr, only_path, options)
584 def parse_redmine_links(text, project, obj, attr, only_path, options)
585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
587 link = nil
587 link = nil
588 if esc.nil?
588 if esc.nil?
589 if prefix.nil? && sep == 'r'
589 if prefix.nil? && sep == 'r'
590 if project && (changeset = project.changesets.find_by_revision(identifier))
590 if project && (changeset = project.changesets.find_by_revision(identifier))
591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
592 :class => 'changeset',
592 :class => 'changeset',
593 :title => truncate_single_line(changeset.comments, :length => 100))
593 :title => truncate_single_line(changeset.comments, :length => 100))
594 end
594 end
595 elsif sep == '#'
595 elsif sep == '#'
596 oid = identifier.to_i
596 oid = identifier.to_i
597 case prefix
597 case prefix
598 when nil
598 when nil
599 if issue = Issue.visible.find_by_id(oid, :include => :status)
599 if issue = Issue.visible.find_by_id(oid, :include => :status)
600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
601 :class => issue.css_classes,
601 :class => issue.css_classes,
602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
603 end
603 end
604 when 'document'
604 when 'document'
605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
607 :class => 'document'
607 :class => 'document'
608 end
608 end
609 when 'version'
609 when 'version'
610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
612 :class => 'version'
612 :class => 'version'
613 end
613 end
614 when 'message'
614 when 'message'
615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
617 :controller => 'messages',
617 :controller => 'messages',
618 :action => 'show',
618 :action => 'show',
619 :board_id => message.board,
619 :board_id => message.board,
620 :id => message.root,
620 :id => message.root,
621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
622 :class => 'message'
622 :class => 'message'
623 end
623 end
624 when 'project'
624 when 'project'
625 if p = Project.visible.find_by_id(oid)
625 if p = Project.visible.find_by_id(oid)
626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
627 end
627 end
628 end
628 end
629 elsif sep == ':'
629 elsif sep == ':'
630 # removes the double quotes if any
630 # removes the double quotes if any
631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
632 case prefix
632 case prefix
633 when 'document'
633 when 'document'
634 if project && document = project.documents.find_by_title(name)
634 if project && document = project.documents.find_by_title(name)
635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 :class => 'document'
636 :class => 'document'
637 end
637 end
638 when 'version'
638 when 'version'
639 if project && version = project.versions.find_by_name(name)
639 if project && version = project.versions.find_by_name(name)
640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 :class => 'version'
641 :class => 'version'
642 end
642 end
643 when 'commit'
643 when 'commit'
644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
646 :class => 'changeset',
646 :class => 'changeset',
647 :title => truncate_single_line(changeset.comments, :length => 100)
647 :title => truncate_single_line(changeset.comments, :length => 100)
648 end
648 end
649 when 'source', 'export'
649 when 'source', 'export'
650 if project && project.repository
650 if project && project.repository
651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
652 path, rev, anchor = $1, $3, $5
652 path, rev, anchor = $1, $3, $5
653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
654 :path => to_path_param(path),
654 :path => to_path_param(path),
655 :rev => rev,
655 :rev => rev,
656 :anchor => anchor,
656 :anchor => anchor,
657 :format => (prefix == 'export' ? 'raw' : nil)},
657 :format => (prefix == 'export' ? 'raw' : nil)},
658 :class => (prefix == 'export' ? 'source download' : 'source')
658 :class => (prefix == 'export' ? 'source download' : 'source')
659 end
659 end
660 when 'attachment'
660 when 'attachment'
661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
662 if attachments && attachment = attachments.detect {|a| a.filename == name }
662 if attachments && attachment = attachments.detect {|a| a.filename == name }
663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
664 :class => 'attachment'
664 :class => 'attachment'
665 end
665 end
666 when 'project'
666 when 'project'
667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
669 end
669 end
670 end
670 end
671 end
671 end
672 end
672 end
673 leading + (link || "#{prefix}#{sep}#{identifier}")
673 leading + (link || "#{prefix}#{sep}#{identifier}")
674 end
674 end
675 end
675 end
676
676
677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
678 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
678 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
679
679
680 # Headings and TOC
680 # Headings and TOC
681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
682 def parse_headings(text, project, obj, attr, only_path, options)
682 def parse_headings(text, project, obj, attr, only_path, options)
683 headings = []
683 headings = []
684 text.gsub!(HEADING_RE) do
684 text.gsub!(HEADING_RE) do
685 level, attrs, content = $1.to_i, $2, $3
685 level, attrs, content = $1.to_i, $2, $3
686 item = strip_tags(content).strip
686 item = strip_tags(content).strip
687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
688 headings << [level, anchor, item]
688 headings << [level, anchor, item]
689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
690 end unless options[:headings] == false
690 end unless options[:headings] == false
691
691
692 text.gsub!(TOC_RE) do
692 text.gsub!(TOC_RE) do
693 if headings.empty?
693 if headings.empty?
694 ''
694 ''
695 else
695 else
696 div_class = 'toc'
696 div_class = 'toc'
697 div_class << ' right' if $1 == '>'
697 div_class << ' right' if $1 == '>'
698 div_class << ' left' if $1 == '<'
698 div_class << ' left' if $1 == '<'
699 out = "<ul class=\"#{div_class}\"><li>"
699 out = "<ul class=\"#{div_class}\"><li>"
700 root = headings.map(&:first).min
700 root = headings.map(&:first).min
701 current = root
701 current = root
702 started = false
702 started = false
703 headings.each do |level, anchor, item|
703 headings.each do |level, anchor, item|
704 if level > current
704 if level > current
705 out << '<ul><li>' * (level - current)
705 out << '<ul><li>' * (level - current)
706 elsif level < current
706 elsif level < current
707 out << "</li></ul>\n" * (current - level) + "</li><li>"
707 out << "</li></ul>\n" * (current - level) + "</li><li>"
708 elsif started
708 elsif started
709 out << '</li><li>'
709 out << '</li><li>'
710 end
710 end
711 out << "<a href=\"##{anchor}\">#{item}</a>"
711 out << "<a href=\"##{anchor}\">#{item}</a>"
712 current = level
712 current = level
713 started = true
713 started = true
714 end
714 end
715 out << '</li></ul>' * (current - root)
715 out << '</li></ul>' * (current - root)
716 out << '</li></ul>'
716 out << '</li></ul>'
717 end
717 end
718 end
718 end
719 end
719 end
720
720
721 # Same as Rails' simple_format helper without using paragraphs
721 # Same as Rails' simple_format helper without using paragraphs
722 def simple_format_without_paragraph(text)
722 def simple_format_without_paragraph(text)
723 text.to_s.
723 text.to_s.
724 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
724 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
725 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
725 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
726 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
726 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
727 end
727 end
728
728
729 def lang_options_for_select(blank=true)
729 def lang_options_for_select(blank=true)
730 (blank ? [["(auto)", ""]] : []) +
730 (blank ? [["(auto)", ""]] : []) +
731 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
731 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
732 end
732 end
733
733
734 def label_tag_for(name, option_tags = nil, options = {})
734 def label_tag_for(name, option_tags = nil, options = {})
735 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
735 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
736 content_tag("label", label_text)
736 content_tag("label", label_text)
737 end
737 end
738
738
739 def labelled_tabular_form_for(name, object, options, &proc)
739 def labelled_tabular_form_for(name, object, options, &proc)
740 options[:html] ||= {}
740 options[:html] ||= {}
741 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
741 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
742 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
742 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
743 end
743 end
744
744
745 def back_url_hidden_field_tag
745 def back_url_hidden_field_tag
746 back_url = params[:back_url] || request.env['HTTP_REFERER']
746 back_url = params[:back_url] || request.env['HTTP_REFERER']
747 back_url = CGI.unescape(back_url.to_s)
747 back_url = CGI.unescape(back_url.to_s)
748 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
748 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
749 end
749 end
750
750
751 def check_all_links(form_name)
751 def check_all_links(form_name)
752 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
752 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
753 " | " +
753 " | " +
754 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
754 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
755 end
755 end
756
756
757 def progress_bar(pcts, options={})
757 def progress_bar(pcts, options={})
758 pcts = [pcts, pcts] unless pcts.is_a?(Array)
758 pcts = [pcts, pcts] unless pcts.is_a?(Array)
759 pcts = pcts.collect(&:round)
759 pcts = pcts.collect(&:round)
760 pcts[1] = pcts[1] - pcts[0]
760 pcts[1] = pcts[1] - pcts[0]
761 pcts << (100 - pcts[1] - pcts[0])
761 pcts << (100 - pcts[1] - pcts[0])
762 width = options[:width] || '100px;'
762 width = options[:width] || '100px;'
763 legend = options[:legend] || ''
763 legend = options[:legend] || ''
764 content_tag('table',
764 content_tag('table',
765 content_tag('tr',
765 content_tag('tr',
766 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
766 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
767 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
767 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
768 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
768 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
769 ), :class => 'progress', :style => "width: #{width};") +
769 ), :class => 'progress', :style => "width: #{width};") +
770 content_tag('p', legend, :class => 'pourcent')
770 content_tag('p', legend, :class => 'pourcent')
771 end
771 end
772
772
773 def checked_image(checked=true)
773 def checked_image(checked=true)
774 if checked
774 if checked
775 image_tag 'toggle_check.png'
775 image_tag 'toggle_check.png'
776 end
776 end
777 end
777 end
778
778
779 def context_menu(url)
779 def context_menu(url)
780 unless @context_menu_included
780 unless @context_menu_included
781 content_for :header_tags do
781 content_for :header_tags do
782 javascript_include_tag('context_menu') +
782 javascript_include_tag('context_menu') +
783 stylesheet_link_tag('context_menu')
783 stylesheet_link_tag('context_menu')
784 end
784 end
785 if l(:direction) == 'rtl'
785 if l(:direction) == 'rtl'
786 content_for :header_tags do
786 content_for :header_tags do
787 stylesheet_link_tag('context_menu_rtl')
787 stylesheet_link_tag('context_menu_rtl')
788 end
788 end
789 end
789 end
790 @context_menu_included = true
790 @context_menu_included = true
791 end
791 end
792 javascript_tag "new ContextMenu('#{ url_for(url) }')"
792 javascript_tag "new ContextMenu('#{ url_for(url) }')"
793 end
793 end
794
794
795 def context_menu_link(name, url, options={})
795 def context_menu_link(name, url, options={})
796 options[:class] ||= ''
796 options[:class] ||= ''
797 if options.delete(:selected)
797 if options.delete(:selected)
798 options[:class] << ' icon-checked disabled'
798 options[:class] << ' icon-checked disabled'
799 options[:disabled] = true
799 options[:disabled] = true
800 end
800 end
801 if options.delete(:disabled)
801 if options.delete(:disabled)
802 options.delete(:method)
802 options.delete(:method)
803 options.delete(:confirm)
803 options.delete(:confirm)
804 options.delete(:onclick)
804 options.delete(:onclick)
805 options[:class] << ' disabled'
805 options[:class] << ' disabled'
806 url = '#'
806 url = '#'
807 end
807 end
808 link_to name, url, options
808 link_to name, url, options
809 end
809 end
810
810
811 def calendar_for(field_id)
811 def calendar_for(field_id)
812 include_calendar_headers_tags
812 include_calendar_headers_tags
813 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
813 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
814 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
814 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
815 end
815 end
816
816
817 def include_calendar_headers_tags
817 def include_calendar_headers_tags
818 unless @calendar_headers_tags_included
818 unless @calendar_headers_tags_included
819 @calendar_headers_tags_included = true
819 @calendar_headers_tags_included = true
820 content_for :header_tags do
820 content_for :header_tags do
821 start_of_week = case Setting.start_of_week.to_i
821 start_of_week = case Setting.start_of_week.to_i
822 when 1
822 when 1
823 'Calendar._FD = 1;' # Monday
823 'Calendar._FD = 1;' # Monday
824 when 7
824 when 7
825 'Calendar._FD = 0;' # Sunday
825 'Calendar._FD = 0;' # Sunday
826 else
826 else
827 '' # use language
827 '' # use language
828 end
828 end
829
829
830 javascript_include_tag('calendar/calendar') +
830 javascript_include_tag('calendar/calendar') +
831 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
831 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
832 javascript_tag(start_of_week) +
832 javascript_tag(start_of_week) +
833 javascript_include_tag('calendar/calendar-setup') +
833 javascript_include_tag('calendar/calendar-setup') +
834 stylesheet_link_tag('calendar')
834 stylesheet_link_tag('calendar')
835 end
835 end
836 end
836 end
837 end
837 end
838
838
839 def content_for(name, content = nil, &block)
839 def content_for(name, content = nil, &block)
840 @has_content ||= {}
840 @has_content ||= {}
841 @has_content[name] = true
841 @has_content[name] = true
842 super(name, content, &block)
842 super(name, content, &block)
843 end
843 end
844
844
845 def has_content?(name)
845 def has_content?(name)
846 (@has_content && @has_content[name]) || false
846 (@has_content && @has_content[name]) || false
847 end
847 end
848
848
849 # Returns the avatar image tag for the given +user+ if avatars are enabled
849 # Returns the avatar image tag for the given +user+ if avatars are enabled
850 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
850 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
851 def avatar(user, options = { })
851 def avatar(user, options = { })
852 if Setting.gravatar_enabled?
852 if Setting.gravatar_enabled?
853 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
853 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
854 email = nil
854 email = nil
855 if user.respond_to?(:mail)
855 if user.respond_to?(:mail)
856 email = user.mail
856 email = user.mail
857 elsif user.to_s =~ %r{<(.+?)>}
857 elsif user.to_s =~ %r{<(.+?)>}
858 email = $1
858 email = $1
859 end
859 end
860 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
860 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
861 else
861 else
862 ''
862 ''
863 end
863 end
864 end
864 end
865
865
866 def favicon
866 def favicon
867 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
867 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
868 end
868 end
869
870 # Returns true if arg is expected in the API response
871 def include_in_api_response?(arg)
872 unless @included_in_api_response
873 param = params[:include]
874 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
875 @included_in_api_response.collect!(&:strip)
876 end
877 @included_in_api_response.include?(arg.to_s)
878 end
869
879
870 private
880 private
871
881
872 def wiki_helper
882 def wiki_helper
873 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
883 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
874 extend helper
884 extend helper
875 return self
885 return self
876 end
886 end
877
887
878 def link_to_remote_content_update(text, url_params)
888 def link_to_remote_content_update(text, url_params)
879 link_to_remote(text,
889 link_to_remote(text,
880 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
890 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
881 {:href => url_for(:params => url_params)}
891 {:href => url_for(:params => url_params)}
882 )
892 )
883 end
893 end
884
894
885 end
895 end
@@ -1,62 +1,62
1 api.issue do
1 api.issue do
2 api.id @issue.id
2 api.id @issue.id
3 api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil?
3 api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil?
4 api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil?
4 api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil?
5 api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil?
5 api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil?
6 api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil?
6 api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil?
7 api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
7 api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil?
8 api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
8 api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil?
9 api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
9 api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil?
10 api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
10 api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil?
11 api.parent(:id => @issue.parent_id) unless @issue.parent.nil?
11 api.parent(:id => @issue.parent_id) unless @issue.parent.nil?
12
12
13 api.subject @issue.subject
13 api.subject @issue.subject
14 api.description @issue.description
14 api.description @issue.description
15 api.start_date @issue.start_date
15 api.start_date @issue.start_date
16 api.due_date @issue.due_date
16 api.due_date @issue.due_date
17 api.done_ratio @issue.done_ratio
17 api.done_ratio @issue.done_ratio
18 api.estimated_hours @issue.estimated_hours
18 api.estimated_hours @issue.estimated_hours
19 if User.current.allowed_to?(:view_time_entries, @project)
19 if User.current.allowed_to?(:view_time_entries, @project)
20 api.spent_hours @issue.spent_hours
20 api.spent_hours @issue.spent_hours
21 end
21 end
22
22
23 render_api_custom_values @issue.custom_field_values, api
23 render_api_custom_values @issue.custom_field_values, api
24
24
25 api.created_on @issue.created_on
25 api.created_on @issue.created_on
26 api.updated_on @issue.updated_on
26 api.updated_on @issue.updated_on
27
27
28 render_api_issue_children(@issue, api)
28 render_api_issue_children(@issue, api) if include_in_api_response?('children')
29
29
30 api.array :relations do
30 api.array :relations do
31 @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation|
31 @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation|
32 api.relation(:id => relation.id, :issue_id => relation.other_issue(@issue).id, :relation_type => relation.relation_type_for(@issue), :delay => relation.delay)
32 api.relation(:id => relation.id, :issue_id => relation.other_issue(@issue).id, :relation_type => relation.relation_type_for(@issue), :delay => relation.delay)
33 end
33 end
34 end
34 end if include_in_api_response?('relations')
35
35
36 api.array :changesets do
36 api.array :changesets do
37 @issue.changesets.each do |changeset|
37 @issue.changesets.each do |changeset|
38 api.changeset :revision => changeset.revision do
38 api.changeset :revision => changeset.revision do
39 api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
39 api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil?
40 api.comments changeset.comments
40 api.comments changeset.comments
41 api.committed_on changeset.committed_on
41 api.committed_on changeset.committed_on
42 end
42 end
43 end
43 end
44 end if User.current.allowed_to?(:view_changesets, @project) && @issue.changesets.any?
44 end if include_in_api_response?('changesets') && User.current.allowed_to?(:view_changesets, @project)
45
45
46 api.array :journals do
46 api.array :journals do
47 @issue.journals.each do |journal|
47 @issue.journals.each do |journal|
48 api.journal :id => journal.id do
48 api.journal :id => journal.id do
49 api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
49 api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil?
50 api.notes journal.notes
50 api.notes journal.notes
51 api.array :details do
51 api.array :details do
52 journal.details.each do |detail|
52 journal.details.each do |detail|
53 api.detail :property => detail.property, :name => detail.prop_key do
53 api.detail :property => detail.property, :name => detail.prop_key do
54 api.old_value detail.old_value
54 api.old_value detail.old_value
55 api.new_value detail.value
55 api.new_value detail.value
56 end
56 end
57 end
57 end
58 end
58 end
59 end
59 end
60 end
60 end
61 end unless @issue.journals.empty?
61 end if include_in_api_response?('journals')
62 end
62 end
@@ -1,18 +1,18
1 api.project do
1 api.project do
2 api.id @project.id
2 api.id @project.id
3 api.name @project.name
3 api.name @project.name
4 api.identifier @project.identifier
4 api.identifier @project.identifier
5 api.description @project.description
5 api.description @project.description
6 api.homepage @project.homepage
6 api.homepage @project.homepage
7
7
8 render_api_custom_values @project.visible_custom_field_values, api
8 render_api_custom_values @project.visible_custom_field_values, api
9
9
10 api.created_on @project.created_on
10 api.created_on @project.created_on
11 api.updated_on @project.updated_on
11 api.updated_on @project.updated_on
12
12
13 api.array :trackers do
13 api.array :trackers do
14 @project.trackers.each do |tracker|
14 @project.trackers.each do |tracker|
15 api.tracker(:id => tracker.id, :name => tracker.name)
15 api.tracker(:id => tracker.id, :name => tracker.name)
16 end
16 end
17 end
17 end if include_in_api_response?('trackers')
18 end
18 end
@@ -1,24 +1,24
1 api.user do
1 api.user do
2 api.id @user.id
2 api.id @user.id
3 api.login @user.login if User.current.admin?
3 api.login @user.login if User.current.admin?
4 api.firstname @user.firstname
4 api.firstname @user.firstname
5 api.lastname @user.lastname
5 api.lastname @user.lastname
6 api.mail @user.mail if User.current.admin? || !@user.pref.hide_mail
6 api.mail @user.mail if User.current.admin? || !@user.pref.hide_mail
7 api.created_on @user.created_on
7 api.created_on @user.created_on
8 api.last_login_on @user.last_login_on
8 api.last_login_on @user.last_login_on
9
9
10 render_api_custom_values @user.visible_custom_field_values, api
10 render_api_custom_values @user.visible_custom_field_values, api
11
11
12 api.array :memberships do
12 api.array :memberships do
13 @memberships.each do |membership|
13 @memberships.each do |membership|
14 api.membership do
14 api.membership do
15 api.project :id => membership.project.id, :name => membership.project.name
15 api.project :id => membership.project.id, :name => membership.project.name
16 api.array :roles do
16 api.array :roles do
17 membership.roles.each do |role|
17 membership.roles.each do |role|
18 api.role :id => role.id, :name => role.name
18 api.role :id => role.id, :name => role.name
19 end
19 end
20 end
20 end
21 end if membership.project
21 end if membership.project
22 end
22 end
23 end if @memberships.present?
23 end if include_in_api_response?('memberships') && @memberships
24 end
24 end
@@ -1,471 +1,471
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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.dirname(__FILE__)}/../../test_helper"
18 require "#{File.dirname(__FILE__)}/../../test_helper"
19
19
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :versions,
28 :versions,
29 :trackers,
29 :trackers,
30 :projects_trackers,
30 :projects_trackers,
31 :issue_categories,
31 :issue_categories,
32 :enabled_modules,
32 :enabled_modules,
33 :enumerations,
33 :enumerations,
34 :attachments,
34 :attachments,
35 :workflows,
35 :workflows,
36 :custom_fields,
36 :custom_fields,
37 :custom_values,
37 :custom_values,
38 :custom_fields_projects,
38 :custom_fields_projects,
39 :custom_fields_trackers,
39 :custom_fields_trackers,
40 :time_entries,
40 :time_entries,
41 :journals,
41 :journals,
42 :journal_details,
42 :journal_details,
43 :queries
43 :queries
44
44
45 def setup
45 def setup
46 Setting.rest_api_enabled = '1'
46 Setting.rest_api_enabled = '1'
47 end
47 end
48
48
49 # Use a private project to make sure auth is really working and not just
49 # Use a private project to make sure auth is really working and not just
50 # only showing public issues.
50 # only showing public issues.
51 context "/index.xml" do
51 context "/index.xml" do
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53 end
53 end
54
54
55 context "/index.json" do
55 context "/index.json" do
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
57 end
57 end
58
58
59 context "/index.xml with filter" do
59 context "/index.xml with filter" do
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
61
61
62 should "show only issues with the status_id" do
62 should "show only issues with the status_id" do
63 get '/issues.xml?status_id=5'
63 get '/issues.xml?status_id=5'
64 assert_tag :tag => 'issues',
64 assert_tag :tag => 'issues',
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
66 :only => { :tag => 'issue' } }
66 :only => { :tag => 'issue' } }
67 end
67 end
68 end
68 end
69
69
70 context "/index.json with filter" do
70 context "/index.json with filter" do
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
72
72
73 should "show only issues with the status_id" do
73 should "show only issues with the status_id" do
74 get '/issues.json?status_id=5'
74 get '/issues.json?status_id=5'
75
75
76 json = ActiveSupport::JSON.decode(response.body)
76 json = ActiveSupport::JSON.decode(response.body)
77 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
77 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
78 assert_equal 3, status_ids_used.length
78 assert_equal 3, status_ids_used.length
79 assert status_ids_used.all? {|id| id == 5 }
79 assert status_ids_used.all? {|id| id == 5 }
80 end
80 end
81
81
82 end
82 end
83
83
84 # Issue 6 is on a private project
84 # Issue 6 is on a private project
85 context "/issues/6.xml" do
85 context "/issues/6.xml" do
86 should_allow_api_authentication(:get, "/issues/6.xml")
86 should_allow_api_authentication(:get, "/issues/6.xml")
87 end
87 end
88
88
89 context "/issues/6.json" do
89 context "/issues/6.json" do
90 should_allow_api_authentication(:get, "/issues/6.json")
90 should_allow_api_authentication(:get, "/issues/6.json")
91 end
91 end
92
92
93 context "GET /issues/:id" do
93 context "GET /issues/:id" do
94 context "with journals" do
94 context "with journals" do
95 context ".xml" do
95 context ".xml" do
96 should "display journals" do
96 should "display journals" do
97 get '/issues/1.xml'
97 get '/issues/1.xml?include=journals'
98
98
99 assert_tag :tag => 'issue',
99 assert_tag :tag => 'issue',
100 :child => {
100 :child => {
101 :tag => 'journals',
101 :tag => 'journals',
102 :attributes => { :type => 'array' },
102 :attributes => { :type => 'array' },
103 :child => {
103 :child => {
104 :tag => 'journal',
104 :tag => 'journal',
105 :attributes => { :id => '1'},
105 :attributes => { :id => '1'},
106 :child => {
106 :child => {
107 :tag => 'details',
107 :tag => 'details',
108 :attributes => { :type => 'array' },
108 :attributes => { :type => 'array' },
109 :child => {
109 :child => {
110 :tag => 'detail',
110 :tag => 'detail',
111 :attributes => { :name => 'status_id' },
111 :attributes => { :name => 'status_id' },
112 :child => {
112 :child => {
113 :tag => 'old_value',
113 :tag => 'old_value',
114 :content => '1',
114 :content => '1',
115 :sibling => {
115 :sibling => {
116 :tag => 'new_value',
116 :tag => 'new_value',
117 :content => '2'
117 :content => '2'
118 }
118 }
119 }
119 }
120 }
120 }
121 }
121 }
122 }
122 }
123 }
123 }
124 end
124 end
125 end
125 end
126 end
126 end
127
127
128 context "with custom fields" do
128 context "with custom fields" do
129 context ".xml" do
129 context ".xml" do
130 should "display custom fields" do
130 should "display custom fields" do
131 get '/issues/3.xml'
131 get '/issues/3.xml'
132
132
133 assert_tag :tag => 'issue',
133 assert_tag :tag => 'issue',
134 :child => {
134 :child => {
135 :tag => 'custom_fields',
135 :tag => 'custom_fields',
136 :attributes => { :type => 'array' },
136 :attributes => { :type => 'array' },
137 :child => {
137 :child => {
138 :tag => 'custom_field',
138 :tag => 'custom_field',
139 :attributes => { :id => '1'},
139 :attributes => { :id => '1'},
140 :child => {
140 :child => {
141 :tag => 'value',
141 :tag => 'value',
142 :content => 'MySQL'
142 :content => 'MySQL'
143 }
143 }
144 }
144 }
145 }
145 }
146
146
147 assert_nothing_raised do
147 assert_nothing_raised do
148 Hash.from_xml(response.body).to_xml
148 Hash.from_xml(response.body).to_xml
149 end
149 end
150 end
150 end
151 end
151 end
152 end
152 end
153
153
154 context "with subtasks" do
154 context "with subtasks" do
155 setup do
155 setup do
156 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
156 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
157 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
157 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
158 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
158 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
159 end
159 end
160
160
161 context ".xml" do
161 context ".xml" do
162 should "display children" do
162 should "display children" do
163 get '/issues/1.xml'
163 get '/issues/1.xml?include=children'
164
164
165 assert_tag :tag => 'issue',
165 assert_tag :tag => 'issue',
166 :child => {
166 :child => {
167 :tag => 'children',
167 :tag => 'children',
168 :children => {:count => 2},
168 :children => {:count => 2},
169 :child => {
169 :child => {
170 :tag => 'issue',
170 :tag => 'issue',
171 :attributes => {:id => @c1.id.to_s},
171 :attributes => {:id => @c1.id.to_s},
172 :child => {
172 :child => {
173 :tag => 'subject',
173 :tag => 'subject',
174 :content => 'child c1',
174 :content => 'child c1',
175 :sibling => {
175 :sibling => {
176 :tag => 'children',
176 :tag => 'children',
177 :children => {:count => 1},
177 :children => {:count => 1},
178 :child => {
178 :child => {
179 :tag => 'issue',
179 :tag => 'issue',
180 :attributes => {:id => @c3.id.to_s}
180 :attributes => {:id => @c3.id.to_s}
181 }
181 }
182 }
182 }
183 }
183 }
184 }
184 }
185 }
185 }
186 end
186 end
187
187
188 context ".json" do
188 context ".json" do
189 should "display children" do
189 should "display children" do
190 get '/issues/1.json'
190 get '/issues/1.json?include=children'
191
191
192 json = ActiveSupport::JSON.decode(response.body)
192 json = ActiveSupport::JSON.decode(response.body)
193 assert_equal([
193 assert_equal([
194 {
194 {
195 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
195 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
196 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
196 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
197 },
197 },
198 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
198 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
199 ],
199 ],
200 json['issue']['children'])
200 json['issue']['children'])
201 end
201 end
202 end
202 end
203 end
203 end
204 end
204 end
205 end
205 end
206
206
207 context "POST /issues.xml" do
207 context "POST /issues.xml" do
208 should_allow_api_authentication(:post,
208 should_allow_api_authentication(:post,
209 '/issues.xml',
209 '/issues.xml',
210 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
210 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
211 {:success_code => :created})
211 {:success_code => :created})
212
212
213 should "create an issue with the attributes" do
213 should "create an issue with the attributes" do
214 assert_difference('Issue.count') do
214 assert_difference('Issue.count') do
215 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
215 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
216 end
216 end
217
217
218 issue = Issue.first(:order => 'id DESC')
218 issue = Issue.first(:order => 'id DESC')
219 assert_equal 1, issue.project_id
219 assert_equal 1, issue.project_id
220 assert_equal 2, issue.tracker_id
220 assert_equal 2, issue.tracker_id
221 assert_equal 3, issue.status_id
221 assert_equal 3, issue.status_id
222 assert_equal 'API test', issue.subject
222 assert_equal 'API test', issue.subject
223
223
224 assert_response :created
224 assert_response :created
225 assert_equal 'application/xml', @response.content_type
225 assert_equal 'application/xml', @response.content_type
226 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
226 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
227 end
227 end
228 end
228 end
229
229
230 context "POST /issues.xml with failure" do
230 context "POST /issues.xml with failure" do
231 should_allow_api_authentication(:post,
231 should_allow_api_authentication(:post,
232 '/issues.xml',
232 '/issues.xml',
233 {:issue => {:project_id => 1}},
233 {:issue => {:project_id => 1}},
234 {:success_code => :unprocessable_entity})
234 {:success_code => :unprocessable_entity})
235
235
236 should "have an errors tag" do
236 should "have an errors tag" do
237 assert_no_difference('Issue.count') do
237 assert_no_difference('Issue.count') do
238 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
238 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
239 end
239 end
240
240
241 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
241 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
242 end
242 end
243 end
243 end
244
244
245 context "POST /issues.json" do
245 context "POST /issues.json" do
246 should_allow_api_authentication(:post,
246 should_allow_api_authentication(:post,
247 '/issues.json',
247 '/issues.json',
248 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
248 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
249 {:success_code => :created})
249 {:success_code => :created})
250
250
251 should "create an issue with the attributes" do
251 should "create an issue with the attributes" do
252 assert_difference('Issue.count') do
252 assert_difference('Issue.count') do
253 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
253 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
254 end
254 end
255
255
256 issue = Issue.first(:order => 'id DESC')
256 issue = Issue.first(:order => 'id DESC')
257 assert_equal 1, issue.project_id
257 assert_equal 1, issue.project_id
258 assert_equal 2, issue.tracker_id
258 assert_equal 2, issue.tracker_id
259 assert_equal 3, issue.status_id
259 assert_equal 3, issue.status_id
260 assert_equal 'API test', issue.subject
260 assert_equal 'API test', issue.subject
261 end
261 end
262
262
263 end
263 end
264
264
265 context "POST /issues.json with failure" do
265 context "POST /issues.json with failure" do
266 should_allow_api_authentication(:post,
266 should_allow_api_authentication(:post,
267 '/issues.json',
267 '/issues.json',
268 {:issue => {:project_id => 1}},
268 {:issue => {:project_id => 1}},
269 {:success_code => :unprocessable_entity})
269 {:success_code => :unprocessable_entity})
270
270
271 should "have an errors element" do
271 should "have an errors element" do
272 assert_no_difference('Issue.count') do
272 assert_no_difference('Issue.count') do
273 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
273 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
274 end
274 end
275
275
276 json = ActiveSupport::JSON.decode(response.body)
276 json = ActiveSupport::JSON.decode(response.body)
277 assert json['errors'].include?(['subject', "can't be blank"])
277 assert json['errors'].include?(['subject', "can't be blank"])
278 end
278 end
279 end
279 end
280
280
281 # Issue 6 is on a private project
281 # Issue 6 is on a private project
282 context "PUT /issues/6.xml" do
282 context "PUT /issues/6.xml" do
283 setup do
283 setup do
284 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
284 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
285 @headers = { :authorization => credentials('jsmith') }
285 @headers = { :authorization => credentials('jsmith') }
286 end
286 end
287
287
288 should_allow_api_authentication(:put,
288 should_allow_api_authentication(:put,
289 '/issues/6.xml',
289 '/issues/6.xml',
290 {:issue => {:subject => 'API update', :notes => 'A new note'}},
290 {:issue => {:subject => 'API update', :notes => 'A new note'}},
291 {:success_code => :ok})
291 {:success_code => :ok})
292
292
293 should "not create a new issue" do
293 should "not create a new issue" do
294 assert_no_difference('Issue.count') do
294 assert_no_difference('Issue.count') do
295 put '/issues/6.xml', @parameters, @headers
295 put '/issues/6.xml', @parameters, @headers
296 end
296 end
297 end
297 end
298
298
299 should "create a new journal" do
299 should "create a new journal" do
300 assert_difference('Journal.count') do
300 assert_difference('Journal.count') do
301 put '/issues/6.xml', @parameters, @headers
301 put '/issues/6.xml', @parameters, @headers
302 end
302 end
303 end
303 end
304
304
305 should "add the note to the journal" do
305 should "add the note to the journal" do
306 put '/issues/6.xml', @parameters, @headers
306 put '/issues/6.xml', @parameters, @headers
307
307
308 journal = Journal.last
308 journal = Journal.last
309 assert_equal "A new note", journal.notes
309 assert_equal "A new note", journal.notes
310 end
310 end
311
311
312 should "update the issue" do
312 should "update the issue" do
313 put '/issues/6.xml', @parameters, @headers
313 put '/issues/6.xml', @parameters, @headers
314
314
315 issue = Issue.find(6)
315 issue = Issue.find(6)
316 assert_equal "API update", issue.subject
316 assert_equal "API update", issue.subject
317 end
317 end
318
318
319 end
319 end
320
320
321 context "PUT /issues/3.xml with custom fields" do
321 context "PUT /issues/3.xml with custom fields" do
322 setup do
322 setup do
323 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
323 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
324 @headers = { :authorization => credentials('jsmith') }
324 @headers = { :authorization => credentials('jsmith') }
325 end
325 end
326
326
327 should "update custom fields" do
327 should "update custom fields" do
328 assert_no_difference('Issue.count') do
328 assert_no_difference('Issue.count') do
329 put '/issues/3.xml', @parameters, @headers
329 put '/issues/3.xml', @parameters, @headers
330 end
330 end
331
331
332 issue = Issue.find(3)
332 issue = Issue.find(3)
333 assert_equal '150', issue.custom_value_for(2).value
333 assert_equal '150', issue.custom_value_for(2).value
334 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
334 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
335 end
335 end
336 end
336 end
337
337
338 context "PUT /issues/6.xml with failed update" do
338 context "PUT /issues/6.xml with failed update" do
339 setup do
339 setup do
340 @parameters = {:issue => {:subject => ''}}
340 @parameters = {:issue => {:subject => ''}}
341 @headers = { :authorization => credentials('jsmith') }
341 @headers = { :authorization => credentials('jsmith') }
342 end
342 end
343
343
344 should_allow_api_authentication(:put,
344 should_allow_api_authentication(:put,
345 '/issues/6.xml',
345 '/issues/6.xml',
346 {:issue => {:subject => ''}}, # Missing subject should fail
346 {:issue => {:subject => ''}}, # Missing subject should fail
347 {:success_code => :unprocessable_entity})
347 {:success_code => :unprocessable_entity})
348
348
349 should "not create a new issue" do
349 should "not create a new issue" do
350 assert_no_difference('Issue.count') do
350 assert_no_difference('Issue.count') do
351 put '/issues/6.xml', @parameters, @headers
351 put '/issues/6.xml', @parameters, @headers
352 end
352 end
353 end
353 end
354
354
355 should "not create a new journal" do
355 should "not create a new journal" do
356 assert_no_difference('Journal.count') do
356 assert_no_difference('Journal.count') do
357 put '/issues/6.xml', @parameters, @headers
357 put '/issues/6.xml', @parameters, @headers
358 end
358 end
359 end
359 end
360
360
361 should "have an errors tag" do
361 should "have an errors tag" do
362 put '/issues/6.xml', @parameters, @headers
362 put '/issues/6.xml', @parameters, @headers
363
363
364 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
364 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
365 end
365 end
366 end
366 end
367
367
368 context "PUT /issues/6.json" do
368 context "PUT /issues/6.json" do
369 setup do
369 setup do
370 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
370 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
371 @headers = { :authorization => credentials('jsmith') }
371 @headers = { :authorization => credentials('jsmith') }
372 end
372 end
373
373
374 should_allow_api_authentication(:put,
374 should_allow_api_authentication(:put,
375 '/issues/6.json',
375 '/issues/6.json',
376 {:issue => {:subject => 'API update', :notes => 'A new note'}},
376 {:issue => {:subject => 'API update', :notes => 'A new note'}},
377 {:success_code => :ok})
377 {:success_code => :ok})
378
378
379 should "not create a new issue" do
379 should "not create a new issue" do
380 assert_no_difference('Issue.count') do
380 assert_no_difference('Issue.count') do
381 put '/issues/6.json', @parameters, @headers
381 put '/issues/6.json', @parameters, @headers
382 end
382 end
383 end
383 end
384
384
385 should "create a new journal" do
385 should "create a new journal" do
386 assert_difference('Journal.count') do
386 assert_difference('Journal.count') do
387 put '/issues/6.json', @parameters, @headers
387 put '/issues/6.json', @parameters, @headers
388 end
388 end
389 end
389 end
390
390
391 should "add the note to the journal" do
391 should "add the note to the journal" do
392 put '/issues/6.json', @parameters, @headers
392 put '/issues/6.json', @parameters, @headers
393
393
394 journal = Journal.last
394 journal = Journal.last
395 assert_equal "A new note", journal.notes
395 assert_equal "A new note", journal.notes
396 end
396 end
397
397
398 should "update the issue" do
398 should "update the issue" do
399 put '/issues/6.json', @parameters, @headers
399 put '/issues/6.json', @parameters, @headers
400
400
401 issue = Issue.find(6)
401 issue = Issue.find(6)
402 assert_equal "API update", issue.subject
402 assert_equal "API update", issue.subject
403 end
403 end
404
404
405 end
405 end
406
406
407 context "PUT /issues/6.json with failed update" do
407 context "PUT /issues/6.json with failed update" do
408 setup do
408 setup do
409 @parameters = {:issue => {:subject => ''}}
409 @parameters = {:issue => {:subject => ''}}
410 @headers = { :authorization => credentials('jsmith') }
410 @headers = { :authorization => credentials('jsmith') }
411 end
411 end
412
412
413 should_allow_api_authentication(:put,
413 should_allow_api_authentication(:put,
414 '/issues/6.json',
414 '/issues/6.json',
415 {:issue => {:subject => ''}}, # Missing subject should fail
415 {:issue => {:subject => ''}}, # Missing subject should fail
416 {:success_code => :unprocessable_entity})
416 {:success_code => :unprocessable_entity})
417
417
418 should "not create a new issue" do
418 should "not create a new issue" do
419 assert_no_difference('Issue.count') do
419 assert_no_difference('Issue.count') do
420 put '/issues/6.json', @parameters, @headers
420 put '/issues/6.json', @parameters, @headers
421 end
421 end
422 end
422 end
423
423
424 should "not create a new journal" do
424 should "not create a new journal" do
425 assert_no_difference('Journal.count') do
425 assert_no_difference('Journal.count') do
426 put '/issues/6.json', @parameters, @headers
426 put '/issues/6.json', @parameters, @headers
427 end
427 end
428 end
428 end
429
429
430 should "have an errors attribute" do
430 should "have an errors attribute" do
431 put '/issues/6.json', @parameters, @headers
431 put '/issues/6.json', @parameters, @headers
432
432
433 json = ActiveSupport::JSON.decode(response.body)
433 json = ActiveSupport::JSON.decode(response.body)
434 assert json['errors'].include?(['subject', "can't be blank"])
434 assert json['errors'].include?(['subject', "can't be blank"])
435 end
435 end
436 end
436 end
437
437
438 context "DELETE /issues/1.xml" do
438 context "DELETE /issues/1.xml" do
439 should_allow_api_authentication(:delete,
439 should_allow_api_authentication(:delete,
440 '/issues/6.xml',
440 '/issues/6.xml',
441 {},
441 {},
442 {:success_code => :ok})
442 {:success_code => :ok})
443
443
444 should "delete the issue" do
444 should "delete the issue" do
445 assert_difference('Issue.count',-1) do
445 assert_difference('Issue.count',-1) do
446 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
446 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
447 end
447 end
448
448
449 assert_nil Issue.find_by_id(6)
449 assert_nil Issue.find_by_id(6)
450 end
450 end
451 end
451 end
452
452
453 context "DELETE /issues/1.json" do
453 context "DELETE /issues/1.json" do
454 should_allow_api_authentication(:delete,
454 should_allow_api_authentication(:delete,
455 '/issues/6.json',
455 '/issues/6.json',
456 {},
456 {},
457 {:success_code => :ok})
457 {:success_code => :ok})
458
458
459 should "delete the issue" do
459 should "delete the issue" do
460 assert_difference('Issue.count',-1) do
460 assert_difference('Issue.count',-1) do
461 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
461 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
462 end
462 end
463
463
464 assert_nil Issue.find_by_id(6)
464 assert_nil Issue.find_by_id(6)
465 end
465 end
466 end
466 end
467
467
468 def credentials(user, password=nil)
468 def credentials(user, password=nil)
469 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
469 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
470 end
470 end
471 end
471 end
General Comments 0
You need to be logged in to leave comments. Login now