##// END OF EJS Templates
Added JSON support to the issues API. #1214...
Eric Davis -
r3652:345301284a9b
parent child
Show More
@@ -1,352 +1,359
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class ApplicationController < ActionController::Base
21 class ApplicationController < ActionController::Base
22 include Redmine::I18n
22 include Redmine::I18n
23
23
24 layout 'base'
24 layout 'base'
25 exempt_from_layout 'builder'
25 exempt_from_layout 'builder'
26
26
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
29 # TODO: remove it when Rails is fixed
29 # TODO: remove it when Rails is fixed
30 before_filter :delete_broken_cookies
30 before_filter :delete_broken_cookies
31 def delete_broken_cookies
31 def delete_broken_cookies
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
33 cookies.delete '_redmine_session'
33 cookies.delete '_redmine_session'
34 redirect_to home_path
34 redirect_to home_path
35 return false
35 return false
36 end
36 end
37 end
37 end
38
38
39 before_filter :user_setup, :check_if_login_required, :set_localization
39 before_filter :user_setup, :check_if_login_required, :set_localization
40 filter_parameter_logging :password
40 filter_parameter_logging :password
41 protect_from_forgery
41 protect_from_forgery
42
42
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
44
44
45 include Redmine::Search::Controller
45 include Redmine::Search::Controller
46 include Redmine::MenuManager::MenuController
46 include Redmine::MenuManager::MenuController
47 helper Redmine::MenuManager::MenuHelper
47 helper Redmine::MenuManager::MenuHelper
48
48
49 Redmine::Scm::Base.all.each do |scm|
49 Redmine::Scm::Base.all.each do |scm|
50 require_dependency "repository/#{scm.underscore}"
50 require_dependency "repository/#{scm.underscore}"
51 end
51 end
52
52
53 def user_setup
53 def user_setup
54 # Check the settings cache for each request
54 # Check the settings cache for each request
55 Setting.check_cache
55 Setting.check_cache
56 # Find the current user
56 # Find the current user
57 User.current = find_current_user
57 User.current = find_current_user
58 end
58 end
59
59
60 # Returns the current user or nil if no user is logged in
60 # Returns the current user or nil if no user is logged in
61 # and starts a session if needed
61 # and starts a session if needed
62 def find_current_user
62 def find_current_user
63 if session[:user_id]
63 if session[:user_id]
64 # existing session
64 # existing session
65 (User.active.find(session[:user_id]) rescue nil)
65 (User.active.find(session[:user_id]) rescue nil)
66 elsif cookies[:autologin] && Setting.autologin?
66 elsif cookies[:autologin] && Setting.autologin?
67 # auto-login feature starts a new session
67 # auto-login feature starts a new session
68 user = User.try_to_autologin(cookies[:autologin])
68 user = User.try_to_autologin(cookies[:autologin])
69 session[:user_id] = user.id if user
69 session[:user_id] = user.id if user
70 user
70 user
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
72 # RSS key authentication does not start a session
72 # RSS key authentication does not start a session
73 User.find_by_rss_key(params[:key])
73 User.find_by_rss_key(params[:key])
74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
76 # Use API key
76 # Use API key
77 User.find_by_api_key(params[:key])
77 User.find_by_api_key(params[:key])
78 else
78 else
79 # HTTP Basic, either username/password or API key/random
79 # HTTP Basic, either username/password or API key/random
80 authenticate_with_http_basic do |username, password|
80 authenticate_with_http_basic do |username, password|
81 User.try_to_login(username, password) || User.find_by_api_key(username)
81 User.try_to_login(username, password) || User.find_by_api_key(username)
82 end
82 end
83 end
83 end
84 end
84 end
85 end
85 end
86
86
87 # Sets the logged in user
87 # Sets the logged in user
88 def logged_user=(user)
88 def logged_user=(user)
89 reset_session
89 reset_session
90 if user && user.is_a?(User)
90 if user && user.is_a?(User)
91 User.current = user
91 User.current = user
92 session[:user_id] = user.id
92 session[:user_id] = user.id
93 else
93 else
94 User.current = User.anonymous
94 User.current = User.anonymous
95 end
95 end
96 end
96 end
97
97
98 # check if login is globally required to access the application
98 # check if login is globally required to access the application
99 def check_if_login_required
99 def check_if_login_required
100 # no check needed if user is already logged in
100 # no check needed if user is already logged in
101 return true if User.current.logged?
101 return true if User.current.logged?
102 require_login if Setting.login_required?
102 require_login if Setting.login_required?
103 end
103 end
104
104
105 def set_localization
105 def set_localization
106 lang = nil
106 lang = nil
107 if User.current.logged?
107 if User.current.logged?
108 lang = find_language(User.current.language)
108 lang = find_language(User.current.language)
109 end
109 end
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
112 if !accept_lang.blank?
112 if !accept_lang.blank?
113 accept_lang = accept_lang.downcase
113 accept_lang = accept_lang.downcase
114 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
114 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
115 end
115 end
116 end
116 end
117 lang ||= Setting.default_language
117 lang ||= Setting.default_language
118 set_language_if_valid(lang)
118 set_language_if_valid(lang)
119 end
119 end
120
120
121 def require_login
121 def require_login
122 if !User.current.logged?
122 if !User.current.logged?
123 # Extract only the basic url parameters on non-GET requests
123 # Extract only the basic url parameters on non-GET requests
124 if request.get?
124 if request.get?
125 url = url_for(params)
125 url = url_for(params)
126 else
126 else
127 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
127 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
128 end
128 end
129 respond_to do |format|
129 respond_to do |format|
130 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
130 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
131 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
131 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
132 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
132 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
134 end
134 end
135 return false
135 return false
136 end
136 end
137 true
137 true
138 end
138 end
139
139
140 def require_admin
140 def require_admin
141 return unless require_login
141 return unless require_login
142 if !User.current.admin?
142 if !User.current.admin?
143 render_403
143 render_403
144 return false
144 return false
145 end
145 end
146 true
146 true
147 end
147 end
148
148
149 def deny_access
149 def deny_access
150 User.current.logged? ? render_403 : require_login
150 User.current.logged? ? render_403 : require_login
151 end
151 end
152
152
153 # Authorize the user for the requested action
153 # Authorize the user for the requested action
154 def authorize(ctrl = params[:controller], action = params[:action], global = false)
154 def authorize(ctrl = params[:controller], action = params[:action], global = false)
155 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
155 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
156 allowed ? true : deny_access
156 allowed ? true : deny_access
157 end
157 end
158
158
159 # Authorize the user for the requested action outside a project
159 # Authorize the user for the requested action outside a project
160 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
160 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
161 authorize(ctrl, action, global)
161 authorize(ctrl, action, global)
162 end
162 end
163
163
164 # Find project of id params[:id]
164 # Find project of id params[:id]
165 def find_project
165 def find_project
166 @project = Project.find(params[:id])
166 @project = Project.find(params[:id])
167 rescue ActiveRecord::RecordNotFound
167 rescue ActiveRecord::RecordNotFound
168 render_404
168 render_404
169 end
169 end
170
170
171 # Find a project based on params[:project_id]
171 # Find a project based on params[:project_id]
172 # TODO: some subclasses override this, see about merging their logic
172 # TODO: some subclasses override this, see about merging their logic
173 def find_optional_project
173 def find_optional_project
174 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
174 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
175 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
175 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
176 allowed ? true : deny_access
176 allowed ? true : deny_access
177 rescue ActiveRecord::RecordNotFound
177 rescue ActiveRecord::RecordNotFound
178 render_404
178 render_404
179 end
179 end
180
180
181 # Finds and sets @project based on @object.project
181 # Finds and sets @project based on @object.project
182 def find_project_from_association
182 def find_project_from_association
183 render_404 unless @object.present?
183 render_404 unless @object.present?
184
184
185 @project = @object.project
185 @project = @object.project
186 rescue ActiveRecord::RecordNotFound
186 rescue ActiveRecord::RecordNotFound
187 render_404
187 render_404
188 end
188 end
189
189
190 def find_model_object
190 def find_model_object
191 model = self.class.read_inheritable_attribute('model_object')
191 model = self.class.read_inheritable_attribute('model_object')
192 if model
192 if model
193 @object = model.find(params[:id])
193 @object = model.find(params[:id])
194 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
194 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
195 end
195 end
196 rescue ActiveRecord::RecordNotFound
196 rescue ActiveRecord::RecordNotFound
197 render_404
197 render_404
198 end
198 end
199
199
200 def self.model_object(model)
200 def self.model_object(model)
201 write_inheritable_attribute('model_object', model)
201 write_inheritable_attribute('model_object', model)
202 end
202 end
203
203
204 # make sure that the user is a member of the project (or admin) if project is private
204 # make sure that the user is a member of the project (or admin) if project is private
205 # used as a before_filter for actions that do not require any particular permission on the project
205 # used as a before_filter for actions that do not require any particular permission on the project
206 def check_project_privacy
206 def check_project_privacy
207 if @project && @project.active?
207 if @project && @project.active?
208 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
208 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
209 true
209 true
210 else
210 else
211 User.current.logged? ? render_403 : require_login
211 User.current.logged? ? render_403 : require_login
212 end
212 end
213 else
213 else
214 @project = nil
214 @project = nil
215 render_404
215 render_404
216 false
216 false
217 end
217 end
218 end
218 end
219
219
220 def redirect_back_or_default(default)
220 def redirect_back_or_default(default)
221 back_url = CGI.unescape(params[:back_url].to_s)
221 back_url = CGI.unescape(params[:back_url].to_s)
222 if !back_url.blank?
222 if !back_url.blank?
223 begin
223 begin
224 uri = URI.parse(back_url)
224 uri = URI.parse(back_url)
225 # do not redirect user to another host or to the login or register page
225 # do not redirect user to another host or to the login or register page
226 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
226 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
227 redirect_to(back_url)
227 redirect_to(back_url)
228 return
228 return
229 end
229 end
230 rescue URI::InvalidURIError
230 rescue URI::InvalidURIError
231 # redirect to default
231 # redirect to default
232 end
232 end
233 end
233 end
234 redirect_to default
234 redirect_to default
235 end
235 end
236
236
237 def render_403
237 def render_403
238 @project = nil
238 @project = nil
239 respond_to do |format|
239 respond_to do |format|
240 format.html { render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 }
240 format.html { render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 }
241 format.atom { head 403 }
241 format.atom { head 403 }
242 format.xml { head 403 }
242 format.xml { head 403 }
243 format.json { head 403 }
243 format.json { head 403 }
244 end
244 end
245 return false
245 return false
246 end
246 end
247
247
248 def render_404
248 def render_404
249 respond_to do |format|
249 respond_to do |format|
250 format.html { render :template => "common/404", :layout => !request.xhr?, :status => 404 }
250 format.html { render :template => "common/404", :layout => !request.xhr?, :status => 404 }
251 format.atom { head 404 }
251 format.atom { head 404 }
252 format.xml { head 404 }
252 format.xml { head 404 }
253 format.json { head 404 }
253 format.json { head 404 }
254 end
254 end
255 return false
255 return false
256 end
256 end
257
257
258 def render_error(msg)
258 def render_error(msg)
259 respond_to do |format|
259 respond_to do |format|
260 format.html {
260 format.html {
261 flash.now[:error] = msg
261 flash.now[:error] = msg
262 render :text => '', :layout => !request.xhr?, :status => 500
262 render :text => '', :layout => !request.xhr?, :status => 500
263 }
263 }
264 format.atom { head 500 }
264 format.atom { head 500 }
265 format.xml { head 500 }
265 format.xml { head 500 }
266 format.json { head 500 }
266 format.json { head 500 }
267 end
267 end
268 end
268 end
269
269
270 def invalid_authenticity_token
270 def invalid_authenticity_token
271 if api_request?
271 if api_request?
272 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
272 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
273 end
273 end
274 render_error "Invalid form authenticity token."
274 render_error "Invalid form authenticity token."
275 end
275 end
276
276
277 def render_feed(items, options={})
277 def render_feed(items, options={})
278 @items = items || []
278 @items = items || []
279 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
279 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
280 @items = @items.slice(0, Setting.feeds_limit.to_i)
280 @items = @items.slice(0, Setting.feeds_limit.to_i)
281 @title = options[:title] || Setting.app_title
281 @title = options[:title] || Setting.app_title
282 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
282 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
283 end
283 end
284
284
285 def self.accept_key_auth(*actions)
285 def self.accept_key_auth(*actions)
286 actions = actions.flatten.map(&:to_s)
286 actions = actions.flatten.map(&:to_s)
287 write_inheritable_attribute('accept_key_auth_actions', actions)
287 write_inheritable_attribute('accept_key_auth_actions', actions)
288 end
288 end
289
289
290 def accept_key_auth_actions
290 def accept_key_auth_actions
291 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
291 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
292 end
292 end
293
293
294 # Returns the number of objects that should be displayed
294 # Returns the number of objects that should be displayed
295 # on the paginated list
295 # on the paginated list
296 def per_page_option
296 def per_page_option
297 per_page = nil
297 per_page = nil
298 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
298 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
299 per_page = params[:per_page].to_s.to_i
299 per_page = params[:per_page].to_s.to_i
300 session[:per_page] = per_page
300 session[:per_page] = per_page
301 elsif session[:per_page]
301 elsif session[:per_page]
302 per_page = session[:per_page]
302 per_page = session[:per_page]
303 else
303 else
304 per_page = Setting.per_page_options_array.first || 25
304 per_page = Setting.per_page_options_array.first || 25
305 end
305 end
306 per_page
306 per_page
307 end
307 end
308
308
309 # qvalues http header parser
309 # qvalues http header parser
310 # code taken from webrick
310 # code taken from webrick
311 def parse_qvalues(value)
311 def parse_qvalues(value)
312 tmp = []
312 tmp = []
313 if value
313 if value
314 parts = value.split(/,\s*/)
314 parts = value.split(/,\s*/)
315 parts.each {|part|
315 parts.each {|part|
316 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
316 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
317 val = m[1]
317 val = m[1]
318 q = (m[2] or 1).to_f
318 q = (m[2] or 1).to_f
319 tmp.push([val, q])
319 tmp.push([val, q])
320 end
320 end
321 }
321 }
322 tmp = tmp.sort_by{|val, q| -q}
322 tmp = tmp.sort_by{|val, q| -q}
323 tmp.collect!{|val, q| val}
323 tmp.collect!{|val, q| val}
324 end
324 end
325 return tmp
325 return tmp
326 rescue
326 rescue
327 nil
327 nil
328 end
328 end
329
329
330 # Returns a string that can be used as filename value in Content-Disposition header
330 # Returns a string that can be used as filename value in Content-Disposition header
331 def filename_for_content_disposition(name)
331 def filename_for_content_disposition(name)
332 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
332 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
333 end
333 end
334
334
335 def api_request?
335 def api_request?
336 %w(xml json).include? params[:format]
336 %w(xml json).include? params[:format]
337 end
337 end
338
338
339 # Renders a warning flash if obj has unsaved attachments
339 # Renders a warning flash if obj has unsaved attachments
340 def render_attachment_warning_if_needed(obj)
340 def render_attachment_warning_if_needed(obj)
341 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
341 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
342 end
342 end
343
343
344 # Rescues an invalid query statement. Just in case...
344 # Rescues an invalid query statement. Just in case...
345 def query_statement_invalid(exception)
345 def query_statement_invalid(exception)
346 logger.error "Query::StatementInvalid: #{exception.message}" if logger
346 logger.error "Query::StatementInvalid: #{exception.message}" if logger
347 session.delete(:query)
347 session.delete(:query)
348 sort_clear if respond_to?(:sort_clear)
348 sort_clear if respond_to?(:sort_clear)
349 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
349 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
350 end
350 end
351
351
352 # Converts the errors on an ActiveRecord object into a common JSON format
353 def object_errors_to_json(object)
354 object.errors.collect do |attribute, error|
355 { attribute => error }
356 end.to_json
357 end
358
352 end
359 end
@@ -1,481 +1,488
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => [:new, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :create, :update_form, :preview, :auto_complete]
24 before_filter :find_project, :only => [:new, :create, :update_form, :preview, :auto_complete]
25 before_filter :authorize, :except => [:index, :changes, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes]
26 before_filter :find_optional_project, :only => [:index, :changes]
27 before_filter :check_for_default_issue_status, :only => [:new, :create]
27 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :build_new_issue_from_params, :only => [:new, :create]
28 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 accept_key_auth :index, :show, :changes
29 accept_key_auth :index, :show, :changes
30
30
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32
32
33 helper :journals
33 helper :journals
34 helper :projects
34 helper :projects
35 include ProjectsHelper
35 include ProjectsHelper
36 helper :custom_fields
36 helper :custom_fields
37 include CustomFieldsHelper
37 include CustomFieldsHelper
38 helper :issue_relations
38 helper :issue_relations
39 include IssueRelationsHelper
39 include IssueRelationsHelper
40 helper :watchers
40 helper :watchers
41 include WatchersHelper
41 include WatchersHelper
42 helper :attachments
42 helper :attachments
43 include AttachmentsHelper
43 include AttachmentsHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :sort
46 helper :sort
47 include SortHelper
47 include SortHelper
48 include IssuesHelper
48 include IssuesHelper
49 helper :timelog
49 helper :timelog
50 include Redmine::Export::PDF
50 include Redmine::Export::PDF
51
51
52 verify :method => [:post, :delete],
52 verify :method => [:post, :delete],
53 :only => :destroy,
53 :only => :destroy,
54 :render => { :nothing => true, :status => :method_not_allowed }
54 :render => { :nothing => true, :status => :method_not_allowed }
55
55
56 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
56 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
57 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
57 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
58
58
59 def index
59 def index
60 retrieve_query
60 retrieve_query
61 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
61 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
62 sort_update(@query.sortable_columns)
62 sort_update(@query.sortable_columns)
63
63
64 if @query.valid?
64 if @query.valid?
65 limit = case params[:format]
65 limit = case params[:format]
66 when 'csv', 'pdf'
66 when 'csv', 'pdf'
67 Setting.issues_export_limit.to_i
67 Setting.issues_export_limit.to_i
68 when 'atom'
68 when 'atom'
69 Setting.feeds_limit.to_i
69 Setting.feeds_limit.to_i
70 else
70 else
71 per_page_option
71 per_page_option
72 end
72 end
73
73
74 @issue_count = @query.issue_count
74 @issue_count = @query.issue_count
75 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
75 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
76 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
77 :order => sort_clause,
77 :order => sort_clause,
78 :offset => @issue_pages.current.offset,
78 :offset => @issue_pages.current.offset,
79 :limit => limit)
79 :limit => limit)
80 @issue_count_by_group = @query.issue_count_by_group
80 @issue_count_by_group = @query.issue_count_by_group
81
81
82 respond_to do |format|
82 respond_to do |format|
83 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
83 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
84 format.xml { render :layout => false }
84 format.xml { render :layout => false }
85 format.json { render :text => @issues.to_json, :layout => false }
85 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
86 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
86 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
87 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
87 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
88 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
88 end
89 end
89 else
90 else
90 # Send html if the query is not valid
91 # Send html if the query is not valid
91 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
92 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
92 end
93 end
93 rescue ActiveRecord::RecordNotFound
94 rescue ActiveRecord::RecordNotFound
94 render_404
95 render_404
95 end
96 end
96
97
97 def changes
98 def changes
98 retrieve_query
99 retrieve_query
99 sort_init 'id', 'desc'
100 sort_init 'id', 'desc'
100 sort_update(@query.sortable_columns)
101 sort_update(@query.sortable_columns)
101
102
102 if @query.valid?
103 if @query.valid?
103 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
104 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
104 :limit => 25)
105 :limit => 25)
105 end
106 end
106 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
107 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
107 render :layout => false, :content_type => 'application/atom+xml'
108 render :layout => false, :content_type => 'application/atom+xml'
108 rescue ActiveRecord::RecordNotFound
109 rescue ActiveRecord::RecordNotFound
109 render_404
110 render_404
110 end
111 end
111
112
112 def show
113 def show
113 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
114 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
114 @journals.each_with_index {|j,i| j.indice = i+1}
115 @journals.each_with_index {|j,i| j.indice = i+1}
115 @journals.reverse! if User.current.wants_comments_in_reverse_order?
116 @journals.reverse! if User.current.wants_comments_in_reverse_order?
116 @changesets = @issue.changesets.visible.all
117 @changesets = @issue.changesets.visible.all
117 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
118 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
118 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
119 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
120 @priorities = IssuePriority.all
121 @priorities = IssuePriority.all
121 @time_entry = TimeEntry.new
122 @time_entry = TimeEntry.new
122 respond_to do |format|
123 respond_to do |format|
123 format.html { render :template => 'issues/show.rhtml' }
124 format.html { render :template => 'issues/show.rhtml' }
124 format.xml { render :layout => false }
125 format.xml { render :layout => false }
126 format.json { render :text => @issue.to_json, :layout => false }
125 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
127 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
126 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
128 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
127 end
129 end
128 end
130 end
129
131
130 # Add a new issue
132 # Add a new issue
131 # The new issue will be created from an existing one if copy_from parameter is given
133 # The new issue will be created from an existing one if copy_from parameter is given
132 def new
134 def new
133 render :action => 'new', :layout => !request.xhr?
135 render :action => 'new', :layout => !request.xhr?
134 end
136 end
135
137
136 def create
138 def create
137 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
138 if @issue.save
140 if @issue.save
139 attachments = Attachment.attach_files(@issue, params[:attachments])
141 attachments = Attachment.attach_files(@issue, params[:attachments])
140 render_attachment_warning_if_needed(@issue)
142 render_attachment_warning_if_needed(@issue)
141 flash[:notice] = l(:notice_successful_create)
143 flash[:notice] = l(:notice_successful_create)
142 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
143 respond_to do |format|
145 respond_to do |format|
144 format.html {
146 format.html {
145 redirect_to(params[:continue] ? { :action => 'new', :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
147 redirect_to(params[:continue] ? { :action => 'new', :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
146 { :action => 'show', :id => @issue })
148 { :action => 'show', :id => @issue })
147 }
149 }
148 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
150 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
151 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
149 end
152 end
150 return
153 return
151 else
154 else
152 respond_to do |format|
155 respond_to do |format|
153 format.html { render :action => 'new' }
156 format.html { render :action => 'new' }
154 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
157 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
158 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
155 end
159 end
156 end
160 end
157 end
161 end
158
162
159 # Attributes that can be updated on workflow transition (without :edit permission)
163 # Attributes that can be updated on workflow transition (without :edit permission)
160 # TODO: make it configurable (at least per role)
164 # TODO: make it configurable (at least per role)
161 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
165 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
162
166
163 def edit
167 def edit
164 update_issue_from_params
168 update_issue_from_params
165
169
166 @journal = @issue.current_journal
170 @journal = @issue.current_journal
167
171
168 respond_to do |format|
172 respond_to do |format|
169 format.html { }
173 format.html { }
170 format.xml { }
174 format.xml { }
171 end
175 end
172 end
176 end
173
177
174 def update
178 def update
175 update_issue_from_params
179 update_issue_from_params
176
180
177 if @issue.save_issue_with_child_records(params, @time_entry)
181 if @issue.save_issue_with_child_records(params, @time_entry)
178 render_attachment_warning_if_needed(@issue)
182 render_attachment_warning_if_needed(@issue)
179 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
183 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
180
184
181 respond_to do |format|
185 respond_to do |format|
182 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
186 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
183 format.xml { head :ok }
187 format.xml { head :ok }
188 format.json { head :ok }
184 end
189 end
185 else
190 else
186 render_attachment_warning_if_needed(@issue)
191 render_attachment_warning_if_needed(@issue)
187 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
188 @journal = @issue.current_journal
193 @journal = @issue.current_journal
189
194
190 respond_to do |format|
195 respond_to do |format|
191 format.html { render :action => 'edit' }
196 format.html { render :action => 'edit' }
192 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
197 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
198 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
193 end
199 end
194 end
200 end
195 end
201 end
196
202
197 def reply
203 def reply
198 journal = Journal.find(params[:journal_id]) if params[:journal_id]
204 journal = Journal.find(params[:journal_id]) if params[:journal_id]
199 if journal
205 if journal
200 user = journal.user
206 user = journal.user
201 text = journal.notes
207 text = journal.notes
202 else
208 else
203 user = @issue.author
209 user = @issue.author
204 text = @issue.description
210 text = @issue.description
205 end
211 end
206 # Replaces pre blocks with [...]
212 # Replaces pre blocks with [...]
207 text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
213 text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
208 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
214 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
209 content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
215 content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
210
216
211 render(:update) { |page|
217 render(:update) { |page|
212 page.<< "$('notes').value = \"#{escape_javascript content}\";"
218 page.<< "$('notes').value = \"#{escape_javascript content}\";"
213 page.show 'update'
219 page.show 'update'
214 page << "Form.Element.focus('notes');"
220 page << "Form.Element.focus('notes');"
215 page << "Element.scrollTo('update');"
221 page << "Element.scrollTo('update');"
216 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
222 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
217 }
223 }
218 end
224 end
219
225
220 # Bulk edit a set of issues
226 # Bulk edit a set of issues
221 def bulk_edit
227 def bulk_edit
222 @issues.sort!
228 @issues.sort!
223 if request.post?
229 if request.post?
224 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
230 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
225 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
231 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
226 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
232 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
227
233
228 unsaved_issue_ids = []
234 unsaved_issue_ids = []
229 @issues.each do |issue|
235 @issues.each do |issue|
230 issue.reload
236 issue.reload
231 journal = issue.init_journal(User.current, params[:notes])
237 journal = issue.init_journal(User.current, params[:notes])
232 issue.safe_attributes = attributes
238 issue.safe_attributes = attributes
233 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
239 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
234 unless issue.save
240 unless issue.save
235 # Keep unsaved issue ids to display them in flash error
241 # Keep unsaved issue ids to display them in flash error
236 unsaved_issue_ids << issue.id
242 unsaved_issue_ids << issue.id
237 end
243 end
238 end
244 end
239 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
245 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
240 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
246 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
241 return
247 return
242 end
248 end
243 @available_statuses = Workflow.available_statuses(@project)
249 @available_statuses = Workflow.available_statuses(@project)
244 @custom_fields = @project.all_issue_custom_fields
250 @custom_fields = @project.all_issue_custom_fields
245 end
251 end
246
252
247 def move
253 def move
248 @issues.sort!
254 @issues.sort!
249 @copy = params[:copy_options] && params[:copy_options][:copy]
255 @copy = params[:copy_options] && params[:copy_options][:copy]
250 @allowed_projects = Issue.allowed_target_projects_on_move
256 @allowed_projects = Issue.allowed_target_projects_on_move
251 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
257 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
252 @target_project ||= @project
258 @target_project ||= @project
253 @trackers = @target_project.trackers
259 @trackers = @target_project.trackers
254 @available_statuses = Workflow.available_statuses(@project)
260 @available_statuses = Workflow.available_statuses(@project)
255 if request.post?
261 if request.post?
256 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
262 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
257 unsaved_issue_ids = []
263 unsaved_issue_ids = []
258 moved_issues = []
264 moved_issues = []
259 @issues.each do |issue|
265 @issues.each do |issue|
260 issue.reload
266 issue.reload
261 changed_attributes = {}
267 changed_attributes = {}
262 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
268 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
263 unless params[valid_attribute].blank?
269 unless params[valid_attribute].blank?
264 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
270 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
265 end
271 end
266 end
272 end
267 issue.init_journal(User.current)
273 issue.init_journal(User.current)
268 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
274 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
269 if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
275 if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
270 moved_issues << r
276 moved_issues << r
271 else
277 else
272 unsaved_issue_ids << issue.id
278 unsaved_issue_ids << issue.id
273 end
279 end
274 end
280 end
275 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
281 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
276
282
277 if params[:follow]
283 if params[:follow]
278 if @issues.size == 1 && moved_issues.size == 1
284 if @issues.size == 1 && moved_issues.size == 1
279 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
285 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
280 else
286 else
281 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
287 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
282 end
288 end
283 else
289 else
284 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
290 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
285 end
291 end
286 return
292 return
287 end
293 end
288 render :layout => false if request.xhr?
294 render :layout => false if request.xhr?
289 end
295 end
290
296
291 def destroy
297 def destroy
292 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
298 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
293 if @hours > 0
299 if @hours > 0
294 case params[:todo]
300 case params[:todo]
295 when 'destroy'
301 when 'destroy'
296 # nothing to do
302 # nothing to do
297 when 'nullify'
303 when 'nullify'
298 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
304 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
299 when 'reassign'
305 when 'reassign'
300 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
306 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
301 if reassign_to.nil?
307 if reassign_to.nil?
302 flash.now[:error] = l(:error_issue_not_found_in_project)
308 flash.now[:error] = l(:error_issue_not_found_in_project)
303 return
309 return
304 else
310 else
305 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
311 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
306 end
312 end
307 else
313 else
308 unless params[:format] == 'xml'
314 unless params[:format] == 'xml' || params[:format] == 'json'
309 # display the destroy form if it's a user request
315 # display the destroy form if it's a user request
310 return
316 return
311 end
317 end
312 end
318 end
313 end
319 end
314 @issues.each(&:destroy)
320 @issues.each(&:destroy)
315 respond_to do |format|
321 respond_to do |format|
316 format.html { redirect_to :action => 'index', :project_id => @project }
322 format.html { redirect_to :action => 'index', :project_id => @project }
317 format.xml { head :ok }
323 format.xml { head :ok }
324 format.json { head :ok }
318 end
325 end
319 end
326 end
320
327
321 def context_menu
328 def context_menu
322 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
329 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
323 if (@issues.size == 1)
330 if (@issues.size == 1)
324 @issue = @issues.first
331 @issue = @issues.first
325 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
332 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
326 end
333 end
327 projects = @issues.collect(&:project).compact.uniq
334 projects = @issues.collect(&:project).compact.uniq
328 @project = projects.first if projects.size == 1
335 @project = projects.first if projects.size == 1
329
336
330 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
337 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
331 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
338 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
332 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
339 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
333 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
340 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
334 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
341 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
335 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
342 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
336 }
343 }
337 if @project
344 if @project
338 @assignables = @project.assignable_users
345 @assignables = @project.assignable_users
339 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
346 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
340 @trackers = @project.trackers
347 @trackers = @project.trackers
341 end
348 end
342
349
343 @priorities = IssuePriority.all.reverse
350 @priorities = IssuePriority.all.reverse
344 @statuses = IssueStatus.find(:all, :order => 'position')
351 @statuses = IssueStatus.find(:all, :order => 'position')
345 @back = params[:back_url] || request.env['HTTP_REFERER']
352 @back = params[:back_url] || request.env['HTTP_REFERER']
346
353
347 render :layout => false
354 render :layout => false
348 end
355 end
349
356
350 def update_form
357 def update_form
351 if params[:id].blank?
358 if params[:id].blank?
352 @issue = Issue.new
359 @issue = Issue.new
353 @issue.project = @project
360 @issue.project = @project
354 else
361 else
355 @issue = @project.issues.visible.find(params[:id])
362 @issue = @project.issues.visible.find(params[:id])
356 end
363 end
357 @issue.attributes = params[:issue]
364 @issue.attributes = params[:issue]
358 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
365 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
359 @priorities = IssuePriority.all
366 @priorities = IssuePriority.all
360
367
361 render :partial => 'attributes'
368 render :partial => 'attributes'
362 end
369 end
363
370
364 def preview
371 def preview
365 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
372 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
366 if @issue
373 if @issue
367 @attachements = @issue.attachments
374 @attachements = @issue.attachments
368 @description = params[:issue] && params[:issue][:description]
375 @description = params[:issue] && params[:issue][:description]
369 if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
376 if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
370 @description = nil
377 @description = nil
371 end
378 end
372 @notes = params[:notes]
379 @notes = params[:notes]
373 else
380 else
374 @description = (params[:issue] ? params[:issue][:description] : nil)
381 @description = (params[:issue] ? params[:issue][:description] : nil)
375 end
382 end
376 render :layout => false
383 render :layout => false
377 end
384 end
378
385
379 def auto_complete
386 def auto_complete
380 @issues = []
387 @issues = []
381 q = params[:q].to_s
388 q = params[:q].to_s
382 if q.match(/^\d+$/)
389 if q.match(/^\d+$/)
383 @issues << @project.issues.visible.find_by_id(q.to_i)
390 @issues << @project.issues.visible.find_by_id(q.to_i)
384 end
391 end
385 unless q.blank?
392 unless q.blank?
386 @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
393 @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
387 end
394 end
388 render :layout => false
395 render :layout => false
389 end
396 end
390
397
391 private
398 private
392 def find_issue
399 def find_issue
393 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
400 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
394 @project = @issue.project
401 @project = @issue.project
395 rescue ActiveRecord::RecordNotFound
402 rescue ActiveRecord::RecordNotFound
396 render_404
403 render_404
397 end
404 end
398
405
399 # Filter for bulk operations
406 # Filter for bulk operations
400 def find_issues
407 def find_issues
401 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
408 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
402 raise ActiveRecord::RecordNotFound if @issues.empty?
409 raise ActiveRecord::RecordNotFound if @issues.empty?
403 projects = @issues.collect(&:project).compact.uniq
410 projects = @issues.collect(&:project).compact.uniq
404 if projects.size == 1
411 if projects.size == 1
405 @project = projects.first
412 @project = projects.first
406 else
413 else
407 # TODO: let users bulk edit/move/destroy issues from different projects
414 # TODO: let users bulk edit/move/destroy issues from different projects
408 render_error 'Can not bulk edit/move/destroy issues from different projects'
415 render_error 'Can not bulk edit/move/destroy issues from different projects'
409 return false
416 return false
410 end
417 end
411 rescue ActiveRecord::RecordNotFound
418 rescue ActiveRecord::RecordNotFound
412 render_404
419 render_404
413 end
420 end
414
421
415 def find_project
422 def find_project
416 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
423 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
417 @project = Project.find(project_id)
424 @project = Project.find(project_id)
418 rescue ActiveRecord::RecordNotFound
425 rescue ActiveRecord::RecordNotFound
419 render_404
426 render_404
420 end
427 end
421
428
422 # Used by #edit and #update to set some common instance variables
429 # Used by #edit and #update to set some common instance variables
423 # from the params
430 # from the params
424 # TODO: Refactor, not everything in here is needed by #edit
431 # TODO: Refactor, not everything in here is needed by #edit
425 def update_issue_from_params
432 def update_issue_from_params
426 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
433 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
427 @priorities = IssuePriority.all
434 @priorities = IssuePriority.all
428 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
435 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
429 @time_entry = TimeEntry.new
436 @time_entry = TimeEntry.new
430
437
431 @notes = params[:notes]
438 @notes = params[:notes]
432 @issue.init_journal(User.current, @notes)
439 @issue.init_journal(User.current, @notes)
433 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
440 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
434 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
441 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
435 attrs = params[:issue].dup
442 attrs = params[:issue].dup
436 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
443 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
437 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
444 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
438 @issue.safe_attributes = attrs
445 @issue.safe_attributes = attrs
439 end
446 end
440
447
441 end
448 end
442
449
443 # TODO: Refactor, lots of extra code in here
450 # TODO: Refactor, lots of extra code in here
444 def build_new_issue_from_params
451 def build_new_issue_from_params
445 @issue = Issue.new
452 @issue = Issue.new
446 @issue.copy_from(params[:copy_from]) if params[:copy_from]
453 @issue.copy_from(params[:copy_from]) if params[:copy_from]
447 @issue.project = @project
454 @issue.project = @project
448 # Tracker must be set before custom field values
455 # Tracker must be set before custom field values
449 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
456 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
450 if @issue.tracker.nil?
457 if @issue.tracker.nil?
451 render_error l(:error_no_tracker_in_project)
458 render_error l(:error_no_tracker_in_project)
452 return false
459 return false
453 end
460 end
454 if params[:issue].is_a?(Hash)
461 if params[:issue].is_a?(Hash)
455 @issue.safe_attributes = params[:issue]
462 @issue.safe_attributes = params[:issue]
456 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
463 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
457 end
464 end
458 @issue.author = User.current
465 @issue.author = User.current
459 @issue.start_date ||= Date.today
466 @issue.start_date ||= Date.today
460 @priorities = IssuePriority.all
467 @priorities = IssuePriority.all
461 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
468 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
462 end
469 end
463
470
464 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
471 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
465 if unsaved_issue_ids.empty?
472 if unsaved_issue_ids.empty?
466 flash[:notice] = l(:notice_successful_update) unless issues.empty?
473 flash[:notice] = l(:notice_successful_update) unless issues.empty?
467 else
474 else
468 flash[:error] = l(:notice_failed_to_save_issues,
475 flash[:error] = l(:notice_failed_to_save_issues,
469 :count => unsaved_issue_ids.size,
476 :count => unsaved_issue_ids.size,
470 :total => issues.size,
477 :total => issues.size,
471 :ids => '#' + unsaved_issue_ids.join(', #'))
478 :ids => '#' + unsaved_issue_ids.join(', #'))
472 end
479 end
473 end
480 end
474
481
475 def check_for_default_issue_status
482 def check_for_default_issue_status
476 if IssueStatus.default.nil?
483 if IssueStatus.default.nil?
477 render_error l(:error_no_default_issue_status)
484 render_error l(:error_no_default_issue_status)
478 return false
485 return false
479 end
486 end
480 end
487 end
481 end
488 end
@@ -1,187 +1,339
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 IssuesApiTest < ActionController::IntegrationTest
20 class IssuesApiTest < 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 context "/index.xml" do
49 context "/index.xml" do
50 setup do
50 setup do
51 get '/issues.xml'
51 get '/issues.xml'
52 end
52 end
53
53
54 should_respond_with :success
54 should_respond_with :success
55 should_respond_with_content_type 'application/xml'
55 should_respond_with_content_type 'application/xml'
56 end
56 end
57
57
58 context "/index.json" do
59 setup do
60 get '/issues.json'
61 end
62
63 should_respond_with :success
64 should_respond_with_content_type 'application/json'
65
66 should 'return a valid JSON string' do
67 assert ActiveSupport::JSON.decode(response.body)
68 end
69 end
70
58 context "/index.xml with filter" do
71 context "/index.xml with filter" do
59 setup do
72 setup do
60 get '/issues.xml?status_id=5'
73 get '/issues.xml?status_id=5'
61 end
74 end
62
75
63 should_respond_with :success
76 should_respond_with :success
64 should_respond_with_content_type 'application/xml'
77 should_respond_with_content_type 'application/xml'
65 should "show only issues with the status_id" do
78 should "show only issues with the status_id" do
66 assert_tag :tag => 'issues',
79 assert_tag :tag => 'issues',
67 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
80 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
68 :only => { :tag => 'issue' } }
81 :only => { :tag => 'issue' } }
69 end
82 end
70 end
83 end
71
84
85 context "/index.json with filter" do
86 setup do
87 get '/issues.json?status_id=5'
88 end
89
90 should_respond_with :success
91 should_respond_with_content_type 'application/json'
92
93 should 'return a valid JSON string' do
94 assert ActiveSupport::JSON.decode(response.body)
95 end
96
97 should "show only issues with the status_id" do
98 json = ActiveSupport::JSON.decode(response.body)
99 status_ids_used = json.collect {|j| j['status_id'] }
100 assert_equal 3, status_ids_used.length
101 assert status_ids_used.all? {|id| id == 5 }
102 end
103
104 end
105
72 context "/issues/1.xml" do
106 context "/issues/1.xml" do
73 setup do
107 setup do
74 get '/issues/1.xml'
108 get '/issues/1.xml'
75 end
109 end
76
110
77 should_respond_with :success
111 should_respond_with :success
78 should_respond_with_content_type 'application/xml'
112 should_respond_with_content_type 'application/xml'
79 end
113 end
80
114
115 context "/issues/1.json" do
116 setup do
117 get '/issues/1.json'
118 end
119
120 should_respond_with :success
121 should_respond_with_content_type 'application/json'
122
123 should 'return a valid JSON string' do
124 assert ActiveSupport::JSON.decode(response.body)
125 end
126 end
127
81 context "POST /issues.xml" do
128 context "POST /issues.xml" do
82 setup do
129 setup do
83 @issue_count = Issue.count
130 @issue_count = Issue.count
84 @attributes = {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}
131 @attributes = {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}
85 post '/issues.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
132 post '/issues.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
86 end
133 end
87
134
88 should_respond_with :created
135 should_respond_with :created
89 should_respond_with_content_type 'application/xml'
136 should_respond_with_content_type 'application/xml'
90
137
91 should "create an issue with the attributes" do
138 should "create an issue with the attributes" do
92 assert_equal Issue.count, @issue_count + 1
139 assert_equal Issue.count, @issue_count + 1
93
140
94 issue = Issue.first(:order => 'id DESC')
141 issue = Issue.first(:order => 'id DESC')
95 @attributes.each do |attribute, value|
142 @attributes.each do |attribute, value|
96 assert_equal value, issue.send(attribute)
143 assert_equal value, issue.send(attribute)
97 end
144 end
98 end
145 end
99 end
146 end
100
147
101 context "POST /issues.xml with failure" do
148 context "POST /issues.xml with failure" do
102 setup do
149 setup do
103 @attributes = {:project_id => 1}
150 @attributes = {:project_id => 1}
104 post '/issues.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
151 post '/issues.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
105 end
152 end
106
153
107 should_respond_with :unprocessable_entity
154 should_respond_with :unprocessable_entity
108 should_respond_with_content_type 'application/xml'
155 should_respond_with_content_type 'application/xml'
109
156
110 should "have an errors tag" do
157 should "have an errors tag" do
111 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
158 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
112 end
159 end
113 end
160 end
114
161
162 context "POST /issues.json" do
163 setup do
164 @issue_count = Issue.count
165 @attributes = {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}
166 post '/issues.json', {:issue => @attributes}, :authorization => credentials('jsmith')
167 end
168
169 should_respond_with :created
170 should_respond_with_content_type 'application/json'
171
172 should "create an issue with the attributes" do
173 assert_equal Issue.count, @issue_count + 1
174
175 issue = Issue.first(:order => 'id DESC')
176 @attributes.each do |attribute, value|
177 assert_equal value, issue.send(attribute)
178 end
179 end
180 end
181
182 context "POST /issues.json with failure" do
183 setup do
184 @attributes = {:project_id => 1}
185 post '/issues.json', {:issue => @attributes}, :authorization => credentials('jsmith')
186 end
187
188 should_respond_with :unprocessable_entity
189 should_respond_with_content_type 'application/json'
190
191 should "have an errors element" do
192 json = ActiveSupport::JSON.decode(response.body)
193 assert_equal "can't be blank", json.first['subject']
194 end
195 end
196
115 context "PUT /issues/1.xml" do
197 context "PUT /issues/1.xml" do
116 setup do
198 setup do
117 @issue_count = Issue.count
199 @issue_count = Issue.count
118 @journal_count = Journal.count
200 @journal_count = Journal.count
119 @attributes = {:subject => 'API update'}
201 @attributes = {:subject => 'API update'}
120
202
121 put '/issues/1.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
203 put '/issues/1.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
122 end
204 end
123
205
124 should_respond_with :ok
206 should_respond_with :ok
125 should_respond_with_content_type 'application/xml'
207 should_respond_with_content_type 'application/xml'
126
208
127 should "not create a new issue" do
209 should "not create a new issue" do
128 assert_equal Issue.count, @issue_count
210 assert_equal Issue.count, @issue_count
129 end
211 end
130
212
131 should "create a new journal" do
213 should "create a new journal" do
132 assert_equal Journal.count, @journal_count + 1
214 assert_equal Journal.count, @journal_count + 1
133 end
215 end
134
216
135 should "update the issue" do
217 should "update the issue" do
136 issue = Issue.find(1)
218 issue = Issue.find(1)
137 @attributes.each do |attribute, value|
219 @attributes.each do |attribute, value|
138 assert_equal value, issue.send(attribute)
220 assert_equal value, issue.send(attribute)
139 end
221 end
140 end
222 end
141
223
142 end
224 end
143
225
144 context "PUT /issues/1.xml with failed update" do
226 context "PUT /issues/1.xml with failed update" do
145 setup do
227 setup do
146 @attributes = {:subject => ''}
228 @attributes = {:subject => ''}
147 @issue_count = Issue.count
229 @issue_count = Issue.count
148 @journal_count = Journal.count
230 @journal_count = Journal.count
149
231
150 put '/issues/1.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
232 put '/issues/1.xml', {:issue => @attributes}, :authorization => credentials('jsmith')
151 end
233 end
152
234
153 should_respond_with :unprocessable_entity
235 should_respond_with :unprocessable_entity
154 should_respond_with_content_type 'application/xml'
236 should_respond_with_content_type 'application/xml'
155
237
156 should "not create a new issue" do
238 should "not create a new issue" do
157 assert_equal Issue.count, @issue_count
239 assert_equal Issue.count, @issue_count
158 end
240 end
159
241
160 should "not create a new journal" do
242 should "not create a new journal" do
161 assert_equal Journal.count, @journal_count
243 assert_equal Journal.count, @journal_count
162 end
244 end
163
245
164 should "have an errors tag" do
246 should "have an errors tag" do
165 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
247 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
166 end
248 end
167 end
249 end
168
250
251 context "PUT /issues/1.json" do
252 setup do
253 @issue_count = Issue.count
254 @journal_count = Journal.count
255 @attributes = {:subject => 'API update'}
256
257 put '/issues/1.json', {:issue => @attributes}, :authorization => credentials('jsmith')
258 end
259
260 should_respond_with :ok
261 should_respond_with_content_type 'application/json'
262
263 should "not create a new issue" do
264 assert_equal Issue.count, @issue_count
265 end
266
267 should "create a new journal" do
268 assert_equal Journal.count, @journal_count + 1
269 end
270
271 should "update the issue" do
272 issue = Issue.find(1)
273 @attributes.each do |attribute, value|
274 assert_equal value, issue.send(attribute)
275 end
276 end
277
278 end
279
280 context "PUT /issues/1.json with failed update" do
281 setup do
282 @attributes = {:subject => ''}
283 @issue_count = Issue.count
284 @journal_count = Journal.count
285
286 put '/issues/1.json', {:issue => @attributes}, :authorization => credentials('jsmith')
287 end
288
289 should_respond_with :unprocessable_entity
290 should_respond_with_content_type 'application/json'
291
292 should "not create a new issue" do
293 assert_equal Issue.count, @issue_count
294 end
295
296 should "not create a new journal" do
297 assert_equal Journal.count, @journal_count
298 end
299
300 should "have an errors attribute" do
301 json = ActiveSupport::JSON.decode(response.body)
302 assert_equal "can't be blank", json.first['subject']
303 end
304 end
305
169 context "DELETE /issues/1.xml" do
306 context "DELETE /issues/1.xml" do
170 setup do
307 setup do
171 @issue_count = Issue.count
308 @issue_count = Issue.count
172 delete '/issues/1.xml', {}, :authorization => credentials('jsmith')
309 delete '/issues/1.xml', {}, :authorization => credentials('jsmith')
173 end
310 end
174
311
175 should_respond_with :ok
312 should_respond_with :ok
176 should_respond_with_content_type 'application/xml'
313 should_respond_with_content_type 'application/xml'
177
314
178 should "delete the issue" do
315 should "delete the issue" do
179 assert_equal Issue.count, @issue_count -1
316 assert_equal Issue.count, @issue_count -1
180 assert_nil Issue.find_by_id(1)
317 assert_nil Issue.find_by_id(1)
181 end
318 end
182 end
319 end
183
320
321 context "DELETE /issues/1.json" do
322 setup do
323 @issue_count = Issue.count
324 delete '/issues/1.json', {}, :authorization => credentials('jsmith')
325 end
326
327 should_respond_with :ok
328 should_respond_with_content_type 'application/json'
329
330 should "delete the issue" do
331 assert_equal Issue.count, @issue_count -1
332 assert_nil Issue.find_by_id(1)
333 end
334 end
335
184 def credentials(user, password=nil)
336 def credentials(user, password=nil)
185 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
337 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
186 end
338 end
187 end
339 end
General Comments 0
You need to be logged in to leave comments. Login now