##// END OF EJS Templates
Fixed that 200 API responses have a body containing one space (#11388)....
Jean-Philippe Lang -
r9792:18f693f9f7c1
parent child
Show More
@@ -1,538 +1,544
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class Unauthorized < Exception; end
21 class Unauthorized < Exception; end
22
22
23 class ApplicationController < ActionController::Base
23 class ApplicationController < ActionController::Base
24 include Redmine::I18n
24 include Redmine::I18n
25
25
26 class_attribute :accept_api_auth_actions
26 class_attribute :accept_api_auth_actions
27 class_attribute :accept_rss_auth_actions
27 class_attribute :accept_rss_auth_actions
28 class_attribute :model_object
28 class_attribute :model_object
29
29
30 layout 'base'
30 layout 'base'
31
31
32 protect_from_forgery
32 protect_from_forgery
33 def handle_unverified_request
33 def handle_unverified_request
34 super
34 super
35 cookies.delete(:autologin)
35 cookies.delete(:autologin)
36 end
36 end
37
37
38 before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
38 before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
39
39
40 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
40 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
41 rescue_from ::Unauthorized, :with => :deny_access
41 rescue_from ::Unauthorized, :with => :deny_access
42
42
43 include Redmine::Search::Controller
43 include Redmine::Search::Controller
44 include Redmine::MenuManager::MenuController
44 include Redmine::MenuManager::MenuController
45 helper Redmine::MenuManager::MenuHelper
45 helper Redmine::MenuManager::MenuHelper
46
46
47 def session_expiration
47 def session_expiration
48 if session[:user_id]
48 if session[:user_id]
49 if session_expired? && !try_to_autologin
49 if session_expired? && !try_to_autologin
50 reset_session
50 reset_session
51 flash[:error] = l(:error_session_expired)
51 flash[:error] = l(:error_session_expired)
52 redirect_to signin_url
52 redirect_to signin_url
53 else
53 else
54 session[:atime] = Time.now.utc.to_i
54 session[:atime] = Time.now.utc.to_i
55 end
55 end
56 end
56 end
57 end
57 end
58
58
59 def session_expired?
59 def session_expired?
60 if Setting.session_lifetime?
60 if Setting.session_lifetime?
61 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
61 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
62 return true
62 return true
63 end
63 end
64 end
64 end
65 if Setting.session_timeout?
65 if Setting.session_timeout?
66 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
66 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
67 return true
67 return true
68 end
68 end
69 end
69 end
70 false
70 false
71 end
71 end
72
72
73 def start_user_session(user)
73 def start_user_session(user)
74 session[:user_id] = user.id
74 session[:user_id] = user.id
75 session[:ctime] = Time.now.utc.to_i
75 session[:ctime] = Time.now.utc.to_i
76 session[:atime] = Time.now.utc.to_i
76 session[:atime] = Time.now.utc.to_i
77 end
77 end
78
78
79 def user_setup
79 def user_setup
80 # Check the settings cache for each request
80 # Check the settings cache for each request
81 Setting.check_cache
81 Setting.check_cache
82 # Find the current user
82 # Find the current user
83 User.current = find_current_user
83 User.current = find_current_user
84 end
84 end
85
85
86 # Returns the current user or nil if no user is logged in
86 # Returns the current user or nil if no user is logged in
87 # and starts a session if needed
87 # and starts a session if needed
88 def find_current_user
88 def find_current_user
89 if session[:user_id]
89 if session[:user_id]
90 # existing session
90 # existing session
91 (User.active.find(session[:user_id]) rescue nil)
91 (User.active.find(session[:user_id]) rescue nil)
92 elsif user = try_to_autologin
92 elsif user = try_to_autologin
93 user
93 user
94 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
94 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
95 # RSS key authentication does not start a session
95 # RSS key authentication does not start a session
96 User.find_by_rss_key(params[:key])
96 User.find_by_rss_key(params[:key])
97 elsif Setting.rest_api_enabled? && accept_api_auth?
97 elsif Setting.rest_api_enabled? && accept_api_auth?
98 if (key = api_key_from_request)
98 if (key = api_key_from_request)
99 # Use API key
99 # Use API key
100 User.find_by_api_key(key)
100 User.find_by_api_key(key)
101 else
101 else
102 # HTTP Basic, either username/password or API key/random
102 # HTTP Basic, either username/password or API key/random
103 authenticate_with_http_basic do |username, password|
103 authenticate_with_http_basic do |username, password|
104 User.try_to_login(username, password) || User.find_by_api_key(username)
104 User.try_to_login(username, password) || User.find_by_api_key(username)
105 end
105 end
106 end
106 end
107 end
107 end
108 end
108 end
109
109
110 def try_to_autologin
110 def try_to_autologin
111 if cookies[:autologin] && Setting.autologin?
111 if cookies[:autologin] && Setting.autologin?
112 # auto-login feature starts a new session
112 # auto-login feature starts a new session
113 user = User.try_to_autologin(cookies[:autologin])
113 user = User.try_to_autologin(cookies[:autologin])
114 if user
114 if user
115 reset_session
115 reset_session
116 start_user_session(user)
116 start_user_session(user)
117 end
117 end
118 user
118 user
119 end
119 end
120 end
120 end
121
121
122 # Sets the logged in user
122 # Sets the logged in user
123 def logged_user=(user)
123 def logged_user=(user)
124 reset_session
124 reset_session
125 if user && user.is_a?(User)
125 if user && user.is_a?(User)
126 User.current = user
126 User.current = user
127 start_user_session(user)
127 start_user_session(user)
128 else
128 else
129 User.current = User.anonymous
129 User.current = User.anonymous
130 end
130 end
131 end
131 end
132
132
133 # Logs out current user
133 # Logs out current user
134 def logout_user
134 def logout_user
135 if User.current.logged?
135 if User.current.logged?
136 cookies.delete :autologin
136 cookies.delete :autologin
137 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
137 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
138 self.logged_user = nil
138 self.logged_user = nil
139 end
139 end
140 end
140 end
141
141
142 # check if login is globally required to access the application
142 # check if login is globally required to access the application
143 def check_if_login_required
143 def check_if_login_required
144 # no check needed if user is already logged in
144 # no check needed if user is already logged in
145 return true if User.current.logged?
145 return true if User.current.logged?
146 require_login if Setting.login_required?
146 require_login if Setting.login_required?
147 end
147 end
148
148
149 def set_localization
149 def set_localization
150 lang = nil
150 lang = nil
151 if User.current.logged?
151 if User.current.logged?
152 lang = find_language(User.current.language)
152 lang = find_language(User.current.language)
153 end
153 end
154 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
154 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
155 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
155 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
156 if !accept_lang.blank?
156 if !accept_lang.blank?
157 accept_lang = accept_lang.downcase
157 accept_lang = accept_lang.downcase
158 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
158 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
159 end
159 end
160 end
160 end
161 lang ||= Setting.default_language
161 lang ||= Setting.default_language
162 set_language_if_valid(lang)
162 set_language_if_valid(lang)
163 end
163 end
164
164
165 def require_login
165 def require_login
166 if !User.current.logged?
166 if !User.current.logged?
167 # Extract only the basic url parameters on non-GET requests
167 # Extract only the basic url parameters on non-GET requests
168 if request.get?
168 if request.get?
169 url = url_for(params)
169 url = url_for(params)
170 else
170 else
171 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
171 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
172 end
172 end
173 respond_to do |format|
173 respond_to do |format|
174 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
174 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
175 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
175 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
176 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
176 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
177 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
177 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
178 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
178 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
179 end
179 end
180 return false
180 return false
181 end
181 end
182 true
182 true
183 end
183 end
184
184
185 def require_admin
185 def require_admin
186 return unless require_login
186 return unless require_login
187 if !User.current.admin?
187 if !User.current.admin?
188 render_403
188 render_403
189 return false
189 return false
190 end
190 end
191 true
191 true
192 end
192 end
193
193
194 def deny_access
194 def deny_access
195 User.current.logged? ? render_403 : require_login
195 User.current.logged? ? render_403 : require_login
196 end
196 end
197
197
198 # Authorize the user for the requested action
198 # Authorize the user for the requested action
199 def authorize(ctrl = params[:controller], action = params[:action], global = false)
199 def authorize(ctrl = params[:controller], action = params[:action], global = false)
200 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
200 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
201 if allowed
201 if allowed
202 true
202 true
203 else
203 else
204 if @project && @project.archived?
204 if @project && @project.archived?
205 render_403 :message => :notice_not_authorized_archived_project
205 render_403 :message => :notice_not_authorized_archived_project
206 else
206 else
207 deny_access
207 deny_access
208 end
208 end
209 end
209 end
210 end
210 end
211
211
212 # Authorize the user for the requested action outside a project
212 # Authorize the user for the requested action outside a project
213 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
213 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
214 authorize(ctrl, action, global)
214 authorize(ctrl, action, global)
215 end
215 end
216
216
217 # Find project of id params[:id]
217 # Find project of id params[:id]
218 def find_project
218 def find_project
219 @project = Project.find(params[:id])
219 @project = Project.find(params[:id])
220 rescue ActiveRecord::RecordNotFound
220 rescue ActiveRecord::RecordNotFound
221 render_404
221 render_404
222 end
222 end
223
223
224 # Find project of id params[:project_id]
224 # Find project of id params[:project_id]
225 def find_project_by_project_id
225 def find_project_by_project_id
226 @project = Project.find(params[:project_id])
226 @project = Project.find(params[:project_id])
227 rescue ActiveRecord::RecordNotFound
227 rescue ActiveRecord::RecordNotFound
228 render_404
228 render_404
229 end
229 end
230
230
231 # Find a project based on params[:project_id]
231 # Find a project based on params[:project_id]
232 # TODO: some subclasses override this, see about merging their logic
232 # TODO: some subclasses override this, see about merging their logic
233 def find_optional_project
233 def find_optional_project
234 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
234 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
235 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
235 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
236 allowed ? true : deny_access
236 allowed ? true : deny_access
237 rescue ActiveRecord::RecordNotFound
237 rescue ActiveRecord::RecordNotFound
238 render_404
238 render_404
239 end
239 end
240
240
241 # Finds and sets @project based on @object.project
241 # Finds and sets @project based on @object.project
242 def find_project_from_association
242 def find_project_from_association
243 render_404 unless @object.present?
243 render_404 unless @object.present?
244
244
245 @project = @object.project
245 @project = @object.project
246 end
246 end
247
247
248 def find_model_object
248 def find_model_object
249 model = self.class.model_object
249 model = self.class.model_object
250 if model
250 if model
251 @object = model.find(params[:id])
251 @object = model.find(params[:id])
252 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
252 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
253 end
253 end
254 rescue ActiveRecord::RecordNotFound
254 rescue ActiveRecord::RecordNotFound
255 render_404
255 render_404
256 end
256 end
257
257
258 def self.model_object(model)
258 def self.model_object(model)
259 self.model_object = model
259 self.model_object = model
260 end
260 end
261
261
262 # Filter for bulk issue operations
262 # Filter for bulk issue operations
263 def find_issues
263 def find_issues
264 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
264 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
265 raise ActiveRecord::RecordNotFound if @issues.empty?
265 raise ActiveRecord::RecordNotFound if @issues.empty?
266 if @issues.detect {|issue| !issue.visible?}
266 if @issues.detect {|issue| !issue.visible?}
267 deny_access
267 deny_access
268 return
268 return
269 end
269 end
270 @projects = @issues.collect(&:project).compact.uniq
270 @projects = @issues.collect(&:project).compact.uniq
271 @project = @projects.first if @projects.size == 1
271 @project = @projects.first if @projects.size == 1
272 rescue ActiveRecord::RecordNotFound
272 rescue ActiveRecord::RecordNotFound
273 render_404
273 render_404
274 end
274 end
275
275
276 # make sure that the user is a member of the project (or admin) if project is private
276 # make sure that the user is a member of the project (or admin) if project is private
277 # used as a before_filter for actions that do not require any particular permission on the project
277 # used as a before_filter for actions that do not require any particular permission on the project
278 def check_project_privacy
278 def check_project_privacy
279 if @project && !@project.archived?
279 if @project && !@project.archived?
280 if @project.visible?
280 if @project.visible?
281 true
281 true
282 else
282 else
283 deny_access
283 deny_access
284 end
284 end
285 else
285 else
286 @project = nil
286 @project = nil
287 render_404
287 render_404
288 false
288 false
289 end
289 end
290 end
290 end
291
291
292 def back_url
292 def back_url
293 params[:back_url] || request.env['HTTP_REFERER']
293 params[:back_url] || request.env['HTTP_REFERER']
294 end
294 end
295
295
296 def redirect_back_or_default(default)
296 def redirect_back_or_default(default)
297 back_url = CGI.unescape(params[:back_url].to_s)
297 back_url = CGI.unescape(params[:back_url].to_s)
298 if !back_url.blank?
298 if !back_url.blank?
299 begin
299 begin
300 uri = URI.parse(back_url)
300 uri = URI.parse(back_url)
301 # do not redirect user to another host or to the login or register page
301 # do not redirect user to another host or to the login or register page
302 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
302 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
303 redirect_to(back_url)
303 redirect_to(back_url)
304 return
304 return
305 end
305 end
306 rescue URI::InvalidURIError
306 rescue URI::InvalidURIError
307 # redirect to default
307 # redirect to default
308 end
308 end
309 end
309 end
310 redirect_to default
310 redirect_to default
311 false
311 false
312 end
312 end
313
313
314 # Redirects to the request referer if present, redirects to args or call block otherwise.
314 # Redirects to the request referer if present, redirects to args or call block otherwise.
315 def redirect_to_referer_or(*args, &block)
315 def redirect_to_referer_or(*args, &block)
316 redirect_to :back
316 redirect_to :back
317 rescue ::ActionController::RedirectBackError
317 rescue ::ActionController::RedirectBackError
318 if args.any?
318 if args.any?
319 redirect_to *args
319 redirect_to *args
320 elsif block_given?
320 elsif block_given?
321 block.call
321 block.call
322 else
322 else
323 raise "#redirect_to_referer_or takes arguments or a block"
323 raise "#redirect_to_referer_or takes arguments or a block"
324 end
324 end
325 end
325 end
326
326
327 def render_403(options={})
327 def render_403(options={})
328 @project = nil
328 @project = nil
329 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
329 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
330 return false
330 return false
331 end
331 end
332
332
333 def render_404(options={})
333 def render_404(options={})
334 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
334 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
335 return false
335 return false
336 end
336 end
337
337
338 # Renders an error response
338 # Renders an error response
339 def render_error(arg)
339 def render_error(arg)
340 arg = {:message => arg} unless arg.is_a?(Hash)
340 arg = {:message => arg} unless arg.is_a?(Hash)
341
341
342 @message = arg[:message]
342 @message = arg[:message]
343 @message = l(@message) if @message.is_a?(Symbol)
343 @message = l(@message) if @message.is_a?(Symbol)
344 @status = arg[:status] || 500
344 @status = arg[:status] || 500
345
345
346 respond_to do |format|
346 respond_to do |format|
347 format.html {
347 format.html {
348 render :template => 'common/error', :layout => use_layout, :status => @status
348 render :template => 'common/error', :layout => use_layout, :status => @status
349 }
349 }
350 format.atom { head @status }
350 format.atom { head @status }
351 format.xml { head @status }
351 format.xml { head @status }
352 format.js { head @status }
352 format.js { head @status }
353 format.json { head @status }
353 format.json { head @status }
354 end
354 end
355 end
355 end
356
356
357 # Filter for actions that provide an API response
357 # Filter for actions that provide an API response
358 # but have no HTML representation for non admin users
358 # but have no HTML representation for non admin users
359 def require_admin_or_api_request
359 def require_admin_or_api_request
360 return true if api_request?
360 return true if api_request?
361 if User.current.admin?
361 if User.current.admin?
362 true
362 true
363 elsif User.current.logged?
363 elsif User.current.logged?
364 render_error(:status => 406)
364 render_error(:status => 406)
365 else
365 else
366 deny_access
366 deny_access
367 end
367 end
368 end
368 end
369
369
370 # Picks which layout to use based on the request
370 # Picks which layout to use based on the request
371 #
371 #
372 # @return [boolean, string] name of the layout to use or false for no layout
372 # @return [boolean, string] name of the layout to use or false for no layout
373 def use_layout
373 def use_layout
374 request.xhr? ? false : 'base'
374 request.xhr? ? false : 'base'
375 end
375 end
376
376
377 def invalid_authenticity_token
377 def invalid_authenticity_token
378 if api_request?
378 if api_request?
379 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
379 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
380 end
380 end
381 render_error "Invalid form authenticity token."
381 render_error "Invalid form authenticity token."
382 end
382 end
383
383
384 def render_feed(items, options={})
384 def render_feed(items, options={})
385 @items = items || []
385 @items = items || []
386 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
386 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
387 @items = @items.slice(0, Setting.feeds_limit.to_i)
387 @items = @items.slice(0, Setting.feeds_limit.to_i)
388 @title = options[:title] || Setting.app_title
388 @title = options[:title] || Setting.app_title
389 render :template => "common/feed.atom", :layout => false,
389 render :template => "common/feed.atom", :layout => false,
390 :content_type => 'application/atom+xml'
390 :content_type => 'application/atom+xml'
391 end
391 end
392
392
393 def self.accept_rss_auth(*actions)
393 def self.accept_rss_auth(*actions)
394 if actions.any?
394 if actions.any?
395 self.accept_rss_auth_actions = actions
395 self.accept_rss_auth_actions = actions
396 else
396 else
397 self.accept_rss_auth_actions || []
397 self.accept_rss_auth_actions || []
398 end
398 end
399 end
399 end
400
400
401 def accept_rss_auth?(action=action_name)
401 def accept_rss_auth?(action=action_name)
402 self.class.accept_rss_auth.include?(action.to_sym)
402 self.class.accept_rss_auth.include?(action.to_sym)
403 end
403 end
404
404
405 def self.accept_api_auth(*actions)
405 def self.accept_api_auth(*actions)
406 if actions.any?
406 if actions.any?
407 self.accept_api_auth_actions = actions
407 self.accept_api_auth_actions = actions
408 else
408 else
409 self.accept_api_auth_actions || []
409 self.accept_api_auth_actions || []
410 end
410 end
411 end
411 end
412
412
413 def accept_api_auth?(action=action_name)
413 def accept_api_auth?(action=action_name)
414 self.class.accept_api_auth.include?(action.to_sym)
414 self.class.accept_api_auth.include?(action.to_sym)
415 end
415 end
416
416
417 # Returns the number of objects that should be displayed
417 # Returns the number of objects that should be displayed
418 # on the paginated list
418 # on the paginated list
419 def per_page_option
419 def per_page_option
420 per_page = nil
420 per_page = nil
421 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
421 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
422 per_page = params[:per_page].to_s.to_i
422 per_page = params[:per_page].to_s.to_i
423 session[:per_page] = per_page
423 session[:per_page] = per_page
424 elsif session[:per_page]
424 elsif session[:per_page]
425 per_page = session[:per_page]
425 per_page = session[:per_page]
426 else
426 else
427 per_page = Setting.per_page_options_array.first || 25
427 per_page = Setting.per_page_options_array.first || 25
428 end
428 end
429 per_page
429 per_page
430 end
430 end
431
431
432 # Returns offset and limit used to retrieve objects
432 # Returns offset and limit used to retrieve objects
433 # for an API response based on offset, limit and page parameters
433 # for an API response based on offset, limit and page parameters
434 def api_offset_and_limit(options=params)
434 def api_offset_and_limit(options=params)
435 if options[:offset].present?
435 if options[:offset].present?
436 offset = options[:offset].to_i
436 offset = options[:offset].to_i
437 if offset < 0
437 if offset < 0
438 offset = 0
438 offset = 0
439 end
439 end
440 end
440 end
441 limit = options[:limit].to_i
441 limit = options[:limit].to_i
442 if limit < 1
442 if limit < 1
443 limit = 25
443 limit = 25
444 elsif limit > 100
444 elsif limit > 100
445 limit = 100
445 limit = 100
446 end
446 end
447 if offset.nil? && options[:page].present?
447 if offset.nil? && options[:page].present?
448 offset = (options[:page].to_i - 1) * limit
448 offset = (options[:page].to_i - 1) * limit
449 offset = 0 if offset < 0
449 offset = 0 if offset < 0
450 end
450 end
451 offset ||= 0
451 offset ||= 0
452
452
453 [offset, limit]
453 [offset, limit]
454 end
454 end
455
455
456 # qvalues http header parser
456 # qvalues http header parser
457 # code taken from webrick
457 # code taken from webrick
458 def parse_qvalues(value)
458 def parse_qvalues(value)
459 tmp = []
459 tmp = []
460 if value
460 if value
461 parts = value.split(/,\s*/)
461 parts = value.split(/,\s*/)
462 parts.each {|part|
462 parts.each {|part|
463 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
463 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
464 val = m[1]
464 val = m[1]
465 q = (m[2] or 1).to_f
465 q = (m[2] or 1).to_f
466 tmp.push([val, q])
466 tmp.push([val, q])
467 end
467 end
468 }
468 }
469 tmp = tmp.sort_by{|val, q| -q}
469 tmp = tmp.sort_by{|val, q| -q}
470 tmp.collect!{|val, q| val}
470 tmp.collect!{|val, q| val}
471 end
471 end
472 return tmp
472 return tmp
473 rescue
473 rescue
474 nil
474 nil
475 end
475 end
476
476
477 # Returns a string that can be used as filename value in Content-Disposition header
477 # Returns a string that can be used as filename value in Content-Disposition header
478 def filename_for_content_disposition(name)
478 def filename_for_content_disposition(name)
479 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
479 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
480 end
480 end
481
481
482 def api_request?
482 def api_request?
483 %w(xml json).include? params[:format]
483 %w(xml json).include? params[:format]
484 end
484 end
485
485
486 # Returns the API key present in the request
486 # Returns the API key present in the request
487 def api_key_from_request
487 def api_key_from_request
488 if params[:key].present?
488 if params[:key].present?
489 params[:key].to_s
489 params[:key].to_s
490 elsif request.headers["X-Redmine-API-Key"].present?
490 elsif request.headers["X-Redmine-API-Key"].present?
491 request.headers["X-Redmine-API-Key"].to_s
491 request.headers["X-Redmine-API-Key"].to_s
492 end
492 end
493 end
493 end
494
494
495 # Renders a warning flash if obj has unsaved attachments
495 # Renders a warning flash if obj has unsaved attachments
496 def render_attachment_warning_if_needed(obj)
496 def render_attachment_warning_if_needed(obj)
497 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
497 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
498 end
498 end
499
499
500 # Sets the `flash` notice or error based the number of issues that did not save
500 # Sets the `flash` notice or error based the number of issues that did not save
501 #
501 #
502 # @param [Array, Issue] issues all of the saved and unsaved Issues
502 # @param [Array, Issue] issues all of the saved and unsaved Issues
503 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
503 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
504 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
504 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
505 if unsaved_issue_ids.empty?
505 if unsaved_issue_ids.empty?
506 flash[:notice] = l(:notice_successful_update) unless issues.empty?
506 flash[:notice] = l(:notice_successful_update) unless issues.empty?
507 else
507 else
508 flash[:error] = l(:notice_failed_to_save_issues,
508 flash[:error] = l(:notice_failed_to_save_issues,
509 :count => unsaved_issue_ids.size,
509 :count => unsaved_issue_ids.size,
510 :total => issues.size,
510 :total => issues.size,
511 :ids => '#' + unsaved_issue_ids.join(', #'))
511 :ids => '#' + unsaved_issue_ids.join(', #'))
512 end
512 end
513 end
513 end
514
514
515 # Rescues an invalid query statement. Just in case...
515 # Rescues an invalid query statement. Just in case...
516 def query_statement_invalid(exception)
516 def query_statement_invalid(exception)
517 logger.error "Query::StatementInvalid: #{exception.message}" if logger
517 logger.error "Query::StatementInvalid: #{exception.message}" if logger
518 session.delete(:query)
518 session.delete(:query)
519 sort_clear if respond_to?(:sort_clear)
519 sort_clear if respond_to?(:sort_clear)
520 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
520 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
521 end
521 end
522
522
523 # Renders a 200 response for successfull updates or deletions via the API
524 def render_api_ok
525 # head :ok would return a response body with one space
526 render :text => '', :status => :ok, :layout => nil
527 end
528
523 # Renders API response on validation failure
529 # Renders API response on validation failure
524 def render_validation_errors(objects)
530 def render_validation_errors(objects)
525 if objects.is_a?(Array)
531 if objects.is_a?(Array)
526 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
532 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
527 else
533 else
528 @error_messages = objects.errors.full_messages
534 @error_messages = objects.errors.full_messages
529 end
535 end
530 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
536 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
531 end
537 end
532
538
533 # Overrides #_include_layout? so that #render with no arguments
539 # Overrides #_include_layout? so that #render with no arguments
534 # doesn't use the layout for api requests
540 # doesn't use the layout for api requests
535 def _include_layout?(*args)
541 def _include_layout?(*args)
536 api_request? ? false : super
542 api_request? ? false : super
537 end
543 end
538 end
544 end
@@ -1,158 +1,158
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class GroupsController < ApplicationController
18 class GroupsController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin
21 before_filter :require_admin
22 before_filter :find_group, :except => [:index, :new, :create]
22 before_filter :find_group, :except => [:index, :new, :create]
23 accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
23 accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
24
24
25 helper :custom_fields
25 helper :custom_fields
26
26
27 def index
27 def index
28 @groups = Group.sorted.all
28 @groups = Group.sorted.all
29
29
30 respond_to do |format|
30 respond_to do |format|
31 format.html
31 format.html
32 format.api
32 format.api
33 end
33 end
34 end
34 end
35
35
36 def show
36 def show
37 respond_to do |format|
37 respond_to do |format|
38 format.html
38 format.html
39 format.api
39 format.api
40 end
40 end
41 end
41 end
42
42
43 def new
43 def new
44 @group = Group.new
44 @group = Group.new
45 end
45 end
46
46
47 def create
47 def create
48 @group = Group.new
48 @group = Group.new
49 @group.safe_attributes = params[:group]
49 @group.safe_attributes = params[:group]
50
50
51 respond_to do |format|
51 respond_to do |format|
52 if @group.save
52 if @group.save
53 format.html {
53 format.html {
54 flash[:notice] = l(:notice_successful_create)
54 flash[:notice] = l(:notice_successful_create)
55 redirect_to(params[:continue] ? new_group_path : groups_path)
55 redirect_to(params[:continue] ? new_group_path : groups_path)
56 }
56 }
57 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
57 format.api { render :action => 'show', :status => :created, :location => group_url(@group) }
58 else
58 else
59 format.html { render :action => "new" }
59 format.html { render :action => "new" }
60 format.api { render_validation_errors(@group) }
60 format.api { render_validation_errors(@group) }
61 end
61 end
62 end
62 end
63 end
63 end
64
64
65 def edit
65 def edit
66 end
66 end
67
67
68 def update
68 def update
69 @group.safe_attributes = params[:group]
69 @group.safe_attributes = params[:group]
70
70
71 respond_to do |format|
71 respond_to do |format|
72 if @group.save
72 if @group.save
73 flash[:notice] = l(:notice_successful_update)
73 flash[:notice] = l(:notice_successful_update)
74 format.html { redirect_to(groups_path) }
74 format.html { redirect_to(groups_path) }
75 format.api { head :ok }
75 format.api { render_api_ok }
76 else
76 else
77 format.html { render :action => "edit" }
77 format.html { render :action => "edit" }
78 format.api { render_validation_errors(@group) }
78 format.api { render_validation_errors(@group) }
79 end
79 end
80 end
80 end
81 end
81 end
82
82
83 def destroy
83 def destroy
84 @group.destroy
84 @group.destroy
85
85
86 respond_to do |format|
86 respond_to do |format|
87 format.html { redirect_to(groups_url) }
87 format.html { redirect_to(groups_url) }
88 format.api { head :ok }
88 format.api { render_api_ok }
89 end
89 end
90 end
90 end
91
91
92 def add_users
92 def add_users
93 users = User.find_all_by_id(params[:user_id] || params[:user_ids])
93 users = User.find_all_by_id(params[:user_id] || params[:user_ids])
94 @group.users << users if request.post?
94 @group.users << users if request.post?
95 respond_to do |format|
95 respond_to do |format|
96 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
96 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
97 format.js {
97 format.js {
98 render(:update) {|page|
98 render(:update) {|page|
99 page.replace_html "tab-content-users", :partial => 'groups/users'
99 page.replace_html "tab-content-users", :partial => 'groups/users'
100 users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
100 users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
101 }
101 }
102 }
102 }
103 format.api { head :ok }
103 format.api { render_api_ok }
104 end
104 end
105 end
105 end
106
106
107 def remove_user
107 def remove_user
108 @group.users.delete(User.find(params[:user_id])) if request.delete?
108 @group.users.delete(User.find(params[:user_id])) if request.delete?
109 respond_to do |format|
109 respond_to do |format|
110 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
110 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
111 format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
111 format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
112 format.api { head :ok }
112 format.api { render_api_ok }
113 end
113 end
114 end
114 end
115
115
116 def autocomplete_for_user
116 def autocomplete_for_user
117 @users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
117 @users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
118 render :layout => false
118 render :layout => false
119 end
119 end
120
120
121 def edit_membership
121 def edit_membership
122 @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
122 @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
123 @membership.save if request.post?
123 @membership.save if request.post?
124 respond_to do |format|
124 respond_to do |format|
125 if @membership.valid?
125 if @membership.valid?
126 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
126 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
127 format.js {
127 format.js {
128 render(:update) {|page|
128 render(:update) {|page|
129 page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
129 page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
130 page.visual_effect(:highlight, "member-#{@membership.id}")
130 page.visual_effect(:highlight, "member-#{@membership.id}")
131 }
131 }
132 }
132 }
133 else
133 else
134 format.js {
134 format.js {
135 render(:update) {|page|
135 render(:update) {|page|
136 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
136 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
137 }
137 }
138 }
138 }
139 end
139 end
140 end
140 end
141 end
141 end
142
142
143 def destroy_membership
143 def destroy_membership
144 Member.find(params[:membership_id]).destroy if request.post?
144 Member.find(params[:membership_id]).destroy if request.post?
145 respond_to do |format|
145 respond_to do |format|
146 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
146 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
147 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
147 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
148 end
148 end
149 end
149 end
150
150
151 private
151 private
152
152
153 def find_group
153 def find_group
154 @group = Group.find(params[:id])
154 @group = Group.find(params[:id])
155 rescue ActiveRecord::RecordNotFound
155 rescue ActiveRecord::RecordNotFound
156 render_404
156 render_404
157 end
157 end
158 end
158 end
@@ -1,135 +1,135
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueCategoriesController < ApplicationController
18 class IssueCategoriesController < ApplicationController
19 menu_item :settings
19 menu_item :settings
20 model_object IssueCategory
20 model_object IssueCategory
21 before_filter :find_model_object, :except => [:index, :new, :create]
21 before_filter :find_model_object, :except => [:index, :new, :create]
22 before_filter :find_project_from_association, :except => [:index, :new, :create]
22 before_filter :find_project_from_association, :except => [:index, :new, :create]
23 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
23 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
24 before_filter :authorize
24 before_filter :authorize
25 accept_api_auth :index, :show, :create, :update, :destroy
25 accept_api_auth :index, :show, :create, :update, :destroy
26
26
27 def index
27 def index
28 respond_to do |format|
28 respond_to do |format|
29 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
29 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
30 format.api { @categories = @project.issue_categories.all }
30 format.api { @categories = @project.issue_categories.all }
31 end
31 end
32 end
32 end
33
33
34 def show
34 def show
35 respond_to do |format|
35 respond_to do |format|
36 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
36 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
37 format.api
37 format.api
38 end
38 end
39 end
39 end
40
40
41 def new
41 def new
42 @category = @project.issue_categories.build
42 @category = @project.issue_categories.build
43 @category.safe_attributes = params[:issue_category]
43 @category.safe_attributes = params[:issue_category]
44
44
45 respond_to do |format|
45 respond_to do |format|
46 format.html
46 format.html
47 format.js do
47 format.js do
48 render :update do |page|
48 render :update do |page|
49 page.replace_html 'ajax-modal', :partial => 'issue_categories/new_modal'
49 page.replace_html 'ajax-modal', :partial => 'issue_categories/new_modal'
50 page << "showModal('ajax-modal', '600px');"
50 page << "showModal('ajax-modal', '600px');"
51 page << "Form.Element.focus('issue_category_name');"
51 page << "Form.Element.focus('issue_category_name');"
52 end
52 end
53 end
53 end
54 end
54 end
55 end
55 end
56
56
57 def create
57 def create
58 @category = @project.issue_categories.build
58 @category = @project.issue_categories.build
59 @category.safe_attributes = params[:issue_category]
59 @category.safe_attributes = params[:issue_category]
60 if @category.save
60 if @category.save
61 respond_to do |format|
61 respond_to do |format|
62 format.html do
62 format.html do
63 flash[:notice] = l(:notice_successful_create)
63 flash[:notice] = l(:notice_successful_create)
64 redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
64 redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
65 end
65 end
66 format.js do
66 format.js do
67 render(:update) {|page|
67 render(:update) {|page|
68 page << 'hideModal();'
68 page << 'hideModal();'
69 # IE doesn't support the replace_html rjs method for select box options
69 # IE doesn't support the replace_html rjs method for select box options
70 page.replace "issue_category_id",
70 page.replace "issue_category_id",
71 content_tag('select', content_tag('option') + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
71 content_tag('select', content_tag('option') + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
72 }
72 }
73 end
73 end
74 format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
74 format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
75 end
75 end
76 else
76 else
77 respond_to do |format|
77 respond_to do |format|
78 format.html { render :action => 'new'}
78 format.html { render :action => 'new'}
79 format.js do
79 format.js do
80 render :update do |page|
80 render :update do |page|
81 page.replace_html 'ajax-modal', :partial => 'issue_categories/new_modal'
81 page.replace_html 'ajax-modal', :partial => 'issue_categories/new_modal'
82 page << "Form.Element.focus('version_name');"
82 page << "Form.Element.focus('version_name');"
83 end
83 end
84 end
84 end
85 format.api { render_validation_errors(@category) }
85 format.api { render_validation_errors(@category) }
86 end
86 end
87 end
87 end
88 end
88 end
89
89
90 def edit
90 def edit
91 end
91 end
92
92
93 def update
93 def update
94 @category.safe_attributes = params[:issue_category]
94 @category.safe_attributes = params[:issue_category]
95 if @category.save
95 if @category.save
96 respond_to do |format|
96 respond_to do |format|
97 format.html {
97 format.html {
98 flash[:notice] = l(:notice_successful_update)
98 flash[:notice] = l(:notice_successful_update)
99 redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
99 redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
100 }
100 }
101 format.api { head :ok }
101 format.api { render_api_ok }
102 end
102 end
103 else
103 else
104 respond_to do |format|
104 respond_to do |format|
105 format.html { render :action => 'edit' }
105 format.html { render :action => 'edit' }
106 format.api { render_validation_errors(@category) }
106 format.api { render_validation_errors(@category) }
107 end
107 end
108 end
108 end
109 end
109 end
110
110
111 def destroy
111 def destroy
112 @issue_count = @category.issues.size
112 @issue_count = @category.issues.size
113 if @issue_count == 0 || params[:todo] || api_request?
113 if @issue_count == 0 || params[:todo] || api_request?
114 reassign_to = nil
114 reassign_to = nil
115 if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
115 if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
116 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
116 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
117 end
117 end
118 @category.destroy(reassign_to)
118 @category.destroy(reassign_to)
119 respond_to do |format|
119 respond_to do |format|
120 format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' }
120 format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' }
121 format.api { head :ok }
121 format.api { render_api_ok }
122 end
122 end
123 return
123 return
124 end
124 end
125 @categories = @project.issue_categories - [@category]
125 @categories = @project.issue_categories - [@category]
126 end
126 end
127
127
128 private
128 private
129 # Wrap ApplicationController's find_model_object method to set
129 # Wrap ApplicationController's find_model_object method to set
130 # @category instead of just @issue_category
130 # @category instead of just @issue_category
131 def find_model_object
131 def find_model_object
132 super
132 super
133 @category = @object
133 @category = @object
134 end
134 end
135 end
135 end
@@ -1,95 +1,95
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueRelationsController < ApplicationController
18 class IssueRelationsController < ApplicationController
19 before_filter :find_issue, :find_project_from_association, :authorize, :only => [:index, :create]
19 before_filter :find_issue, :find_project_from_association, :authorize, :only => [:index, :create]
20 before_filter :find_relation, :except => [:index, :create]
20 before_filter :find_relation, :except => [:index, :create]
21
21
22 accept_api_auth :index, :show, :create, :destroy
22 accept_api_auth :index, :show, :create, :destroy
23
23
24 def index
24 def index
25 @relations = @issue.relations
25 @relations = @issue.relations
26
26
27 respond_to do |format|
27 respond_to do |format|
28 format.html { render :nothing => true }
28 format.html { render :nothing => true }
29 format.api
29 format.api
30 end
30 end
31 end
31 end
32
32
33 def show
33 def show
34 raise Unauthorized unless @relation.visible?
34 raise Unauthorized unless @relation.visible?
35
35
36 respond_to do |format|
36 respond_to do |format|
37 format.html { render :nothing => true }
37 format.html { render :nothing => true }
38 format.api
38 format.api
39 end
39 end
40 end
40 end
41
41
42 def create
42 def create
43 @relation = IssueRelation.new(params[:relation])
43 @relation = IssueRelation.new(params[:relation])
44 @relation.issue_from = @issue
44 @relation.issue_from = @issue
45 if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
45 if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
46 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
46 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
47 end
47 end
48 saved = @relation.save
48 saved = @relation.save
49
49
50 respond_to do |format|
50 respond_to do |format|
51 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
51 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
52 format.js do
52 format.js do
53 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
53 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
54 render :update do |page|
54 render :update do |page|
55 page.replace_html "relations", :partial => 'issues/relations'
55 page.replace_html "relations", :partial => 'issues/relations'
56 if @relation.errors.empty?
56 if @relation.errors.empty?
57 page << "$('relation_delay').value = ''"
57 page << "$('relation_delay').value = ''"
58 page << "$('relation_issue_to_id').value = ''"
58 page << "$('relation_issue_to_id').value = ''"
59 end
59 end
60 end
60 end
61 end
61 end
62 format.api {
62 format.api {
63 if saved
63 if saved
64 render :action => 'show', :status => :created, :location => relation_url(@relation)
64 render :action => 'show', :status => :created, :location => relation_url(@relation)
65 else
65 else
66 render_validation_errors(@relation)
66 render_validation_errors(@relation)
67 end
67 end
68 }
68 }
69 end
69 end
70 end
70 end
71
71
72 def destroy
72 def destroy
73 raise Unauthorized unless @relation.deletable?
73 raise Unauthorized unless @relation.deletable?
74 @relation.destroy
74 @relation.destroy
75
75
76 respond_to do |format|
76 respond_to do |format|
77 format.html { redirect_to issue_path } # TODO : does this really work since @issue is always nil? What is it useful to?
77 format.html { redirect_to issue_path } # TODO : does this really work since @issue is always nil? What is it useful to?
78 format.js { render(:update) {|page| page.remove "relation-#{@relation.id}"} }
78 format.js { render(:update) {|page| page.remove "relation-#{@relation.id}"} }
79 format.api { head :ok }
79 format.api { render_api_ok }
80 end
80 end
81 end
81 end
82
82
83 private
83 private
84 def find_issue
84 def find_issue
85 @issue = @object = Issue.find(params[:issue_id])
85 @issue = @object = Issue.find(params[:issue_id])
86 rescue ActiveRecord::RecordNotFound
86 rescue ActiveRecord::RecordNotFound
87 render_404
87 render_404
88 end
88 end
89
89
90 def find_relation
90 def find_relation
91 @relation = IssueRelation.find(params[:id])
91 @relation = IssueRelation.find(params[:id])
92 rescue ActiveRecord::RecordNotFound
92 rescue ActiveRecord::RecordNotFound
93 render_404
93 render_404
94 end
94 end
95 end
95 end
@@ -1,441 +1,441
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 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]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :find_project, :only => [:new, :create]
24 before_filter :find_project, :only => [:new, :create]
25 before_filter :authorize, :except => [:index]
25 before_filter :authorize, :except => [:index]
26 before_filter :find_optional_project, :only => [:index]
26 before_filter :find_optional_project, :only => [:index]
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_rss_auth :index, :show
29 accept_rss_auth :index, :show
30 accept_api_auth :index, :show, :create, :update, :destroy
30 accept_api_auth :index, :show, :create, :update, :destroy
31
31
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33
33
34 helper :journals
34 helper :journals
35 helper :projects
35 helper :projects
36 include ProjectsHelper
36 include ProjectsHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :issue_relations
39 helper :issue_relations
40 include IssueRelationsHelper
40 include IssueRelationsHelper
41 helper :watchers
41 helper :watchers
42 include WatchersHelper
42 include WatchersHelper
43 helper :attachments
43 helper :attachments
44 include AttachmentsHelper
44 include AttachmentsHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :repositories
47 helper :repositories
48 include RepositoriesHelper
48 include RepositoriesHelper
49 helper :sort
49 helper :sort
50 include SortHelper
50 include SortHelper
51 include IssuesHelper
51 include IssuesHelper
52 helper :timelog
52 helper :timelog
53 include Redmine::Export::PDF
53 include Redmine::Export::PDF
54
54
55 def index
55 def index
56 retrieve_query
56 retrieve_query
57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
57 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 sort_update(@query.sortable_columns)
58 sort_update(@query.sortable_columns)
59
59
60 if @query.valid?
60 if @query.valid?
61 case params[:format]
61 case params[:format]
62 when 'csv', 'pdf'
62 when 'csv', 'pdf'
63 @limit = Setting.issues_export_limit.to_i
63 @limit = Setting.issues_export_limit.to_i
64 when 'atom'
64 when 'atom'
65 @limit = Setting.feeds_limit.to_i
65 @limit = Setting.feeds_limit.to_i
66 when 'xml', 'json'
66 when 'xml', 'json'
67 @offset, @limit = api_offset_and_limit
67 @offset, @limit = api_offset_and_limit
68 else
68 else
69 @limit = per_page_option
69 @limit = per_page_option
70 end
70 end
71
71
72 @issue_count = @query.issue_count
72 @issue_count = @query.issue_count
73 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
73 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
74 @offset ||= @issue_pages.current.offset
74 @offset ||= @issue_pages.current.offset
75 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
75 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
76 :order => sort_clause,
76 :order => sort_clause,
77 :offset => @offset,
77 :offset => @offset,
78 :limit => @limit)
78 :limit => @limit)
79 @issue_count_by_group = @query.issue_count_by_group
79 @issue_count_by_group = @query.issue_count_by_group
80
80
81 respond_to do |format|
81 respond_to do |format|
82 format.html { render :template => 'issues/index', :layout => !request.xhr? }
82 format.html { render :template => 'issues/index', :layout => !request.xhr? }
83 format.api {
83 format.api {
84 Issue.load_relations(@issues) if include_in_api_response?('relations')
84 Issue.load_relations(@issues) if include_in_api_response?('relations')
85 }
85 }
86 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)}") }
87 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
87 format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
88 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') }
89 end
89 end
90 else
90 else
91 respond_to do |format|
91 respond_to do |format|
92 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
92 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
93 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
93 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
94 format.api { render_validation_errors(@query) }
94 format.api { render_validation_errors(@query) }
95 end
95 end
96 end
96 end
97 rescue ActiveRecord::RecordNotFound
97 rescue ActiveRecord::RecordNotFound
98 render_404
98 render_404
99 end
99 end
100
100
101 def show
101 def show
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 @journals.each_with_index {|j,i| j.indice = i+1}
103 @journals.each_with_index {|j,i| j.indice = i+1}
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105
105
106 @changesets = @issue.changesets.visible.all
106 @changesets = @issue.changesets.visible.all
107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
108
108
109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 @priorities = IssuePriority.active
112 @priorities = IssuePriority.active
113 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
113 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
114 respond_to do |format|
114 respond_to do |format|
115 format.html {
115 format.html {
116 retrieve_previous_and_next_issue_ids
116 retrieve_previous_and_next_issue_ids
117 render :template => 'issues/show'
117 render :template => 'issues/show'
118 }
118 }
119 format.api
119 format.api
120 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
120 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 end
122 end
123 end
123 end
124
124
125 # Add a new issue
125 # Add a new issue
126 # The new issue will be created from an existing one if copy_from parameter is given
126 # The new issue will be created from an existing one if copy_from parameter is given
127 def new
127 def new
128 respond_to do |format|
128 respond_to do |format|
129 format.html { render :action => 'new', :layout => !request.xhr? }
129 format.html { render :action => 'new', :layout => !request.xhr? }
130 format.js {
130 format.js {
131 render(:update) { |page|
131 render(:update) { |page|
132 if params[:project_change]
132 if params[:project_change]
133 page.replace_html 'all_attributes', :partial => 'form'
133 page.replace_html 'all_attributes', :partial => 'form'
134 else
134 else
135 page.replace_html 'attributes', :partial => 'attributes'
135 page.replace_html 'attributes', :partial => 'attributes'
136 end
136 end
137 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
137 m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
138 page << "if ($('log_time')) {Element.#{m}('log_time');}"
138 page << "if ($('log_time')) {Element.#{m}('log_time');}"
139 }
139 }
140 }
140 }
141 end
141 end
142 end
142 end
143
143
144 def create
144 def create
145 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
145 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
146 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
146 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
147 if @issue.save
147 if @issue.save
148 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
148 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
149 respond_to do |format|
149 respond_to do |format|
150 format.html {
150 format.html {
151 render_attachment_warning_if_needed(@issue)
151 render_attachment_warning_if_needed(@issue)
152 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue)))
152 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue)))
153 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
153 redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
154 { :action => 'show', :id => @issue })
154 { :action => 'show', :id => @issue })
155 }
155 }
156 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
156 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
157 end
157 end
158 return
158 return
159 else
159 else
160 respond_to do |format|
160 respond_to do |format|
161 format.html { render :action => 'new' }
161 format.html { render :action => 'new' }
162 format.api { render_validation_errors(@issue) }
162 format.api { render_validation_errors(@issue) }
163 end
163 end
164 end
164 end
165 end
165 end
166
166
167 def edit
167 def edit
168 return unless update_issue_from_params
168 return unless update_issue_from_params
169
169
170 respond_to do |format|
170 respond_to do |format|
171 format.html { }
171 format.html { }
172 format.xml { }
172 format.xml { }
173 end
173 end
174 end
174 end
175
175
176 def update
176 def update
177 return unless update_issue_from_params
177 return unless update_issue_from_params
178 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
178 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
179 saved = false
179 saved = false
180 begin
180 begin
181 saved = @issue.save_issue_with_child_records(params, @time_entry)
181 saved = @issue.save_issue_with_child_records(params, @time_entry)
182 rescue ActiveRecord::StaleObjectError
182 rescue ActiveRecord::StaleObjectError
183 @conflict = true
183 @conflict = true
184 if params[:last_journal_id]
184 if params[:last_journal_id]
185 if params[:last_journal_id].present?
185 if params[:last_journal_id].present?
186 last_journal_id = params[:last_journal_id].to_i
186 last_journal_id = params[:last_journal_id].to_i
187 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
187 @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
188 else
188 else
189 @conflict_journals = @issue.journals.all
189 @conflict_journals = @issue.journals.all
190 end
190 end
191 end
191 end
192 end
192 end
193
193
194 if saved
194 if saved
195 render_attachment_warning_if_needed(@issue)
195 render_attachment_warning_if_needed(@issue)
196 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
196 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
197
197
198 respond_to do |format|
198 respond_to do |format|
199 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
199 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
200 format.api { head :ok }
200 format.api { render_api_ok }
201 end
201 end
202 else
202 else
203 respond_to do |format|
203 respond_to do |format|
204 format.html { render :action => 'edit' }
204 format.html { render :action => 'edit' }
205 format.api { render_validation_errors(@issue) }
205 format.api { render_validation_errors(@issue) }
206 end
206 end
207 end
207 end
208 end
208 end
209
209
210 # Bulk edit/copy a set of issues
210 # Bulk edit/copy a set of issues
211 def bulk_edit
211 def bulk_edit
212 @issues.sort!
212 @issues.sort!
213 @copy = params[:copy].present?
213 @copy = params[:copy].present?
214 @notes = params[:notes]
214 @notes = params[:notes]
215
215
216 if User.current.allowed_to?(:move_issues, @projects)
216 if User.current.allowed_to?(:move_issues, @projects)
217 @allowed_projects = Issue.allowed_target_projects_on_move
217 @allowed_projects = Issue.allowed_target_projects_on_move
218 if params[:issue]
218 if params[:issue]
219 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
219 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
220 if @target_project
220 if @target_project
221 target_projects = [@target_project]
221 target_projects = [@target_project]
222 end
222 end
223 end
223 end
224 end
224 end
225 target_projects ||= @projects
225 target_projects ||= @projects
226
226
227 if @copy
227 if @copy
228 @available_statuses = [IssueStatus.default]
228 @available_statuses = [IssueStatus.default]
229 else
229 else
230 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
230 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
231 end
231 end
232 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
232 @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
233 @assignables = target_projects.map(&:assignable_users).reduce(:&)
233 @assignables = target_projects.map(&:assignable_users).reduce(:&)
234 @trackers = target_projects.map(&:trackers).reduce(:&)
234 @trackers = target_projects.map(&:trackers).reduce(:&)
235 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
235 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
236 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
236 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
237 if @copy
237 if @copy
238 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
238 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
239 end
239 end
240
240
241 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
241 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
242 render :layout => false if request.xhr?
242 render :layout => false if request.xhr?
243 end
243 end
244
244
245 def bulk_update
245 def bulk_update
246 @issues.sort!
246 @issues.sort!
247 @copy = params[:copy].present?
247 @copy = params[:copy].present?
248 attributes = parse_params_for_bulk_issue_attributes(params)
248 attributes = parse_params_for_bulk_issue_attributes(params)
249
249
250 unsaved_issue_ids = []
250 unsaved_issue_ids = []
251 moved_issues = []
251 moved_issues = []
252 @issues.each do |issue|
252 @issues.each do |issue|
253 issue.reload
253 issue.reload
254 if @copy
254 if @copy
255 issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
255 issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
256 end
256 end
257 journal = issue.init_journal(User.current, params[:notes])
257 journal = issue.init_journal(User.current, params[:notes])
258 issue.safe_attributes = attributes
258 issue.safe_attributes = attributes
259 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
259 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
260 if issue.save
260 if issue.save
261 moved_issues << issue
261 moved_issues << issue
262 else
262 else
263 # Keep unsaved issue ids to display them in flash error
263 # Keep unsaved issue ids to display them in flash error
264 unsaved_issue_ids << issue.id
264 unsaved_issue_ids << issue.id
265 end
265 end
266 end
266 end
267 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
267 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
268
268
269 if params[:follow]
269 if params[:follow]
270 if @issues.size == 1 && moved_issues.size == 1
270 if @issues.size == 1 && moved_issues.size == 1
271 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
271 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
272 elsif moved_issues.map(&:project).uniq.size == 1
272 elsif moved_issues.map(&:project).uniq.size == 1
273 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
273 redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
274 end
274 end
275 else
275 else
276 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
276 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
277 end
277 end
278 end
278 end
279
279
280 def destroy
280 def destroy
281 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
281 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
282 if @hours > 0
282 if @hours > 0
283 case params[:todo]
283 case params[:todo]
284 when 'destroy'
284 when 'destroy'
285 # nothing to do
285 # nothing to do
286 when 'nullify'
286 when 'nullify'
287 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
287 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
288 when 'reassign'
288 when 'reassign'
289 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
289 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
290 if reassign_to.nil?
290 if reassign_to.nil?
291 flash.now[:error] = l(:error_issue_not_found_in_project)
291 flash.now[:error] = l(:error_issue_not_found_in_project)
292 return
292 return
293 else
293 else
294 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
294 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
295 end
295 end
296 else
296 else
297 # display the destroy form if it's a user request
297 # display the destroy form if it's a user request
298 return unless api_request?
298 return unless api_request?
299 end
299 end
300 end
300 end
301 @issues.each do |issue|
301 @issues.each do |issue|
302 begin
302 begin
303 issue.reload.destroy
303 issue.reload.destroy
304 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
304 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
305 # nothing to do, issue was already deleted (eg. by a parent)
305 # nothing to do, issue was already deleted (eg. by a parent)
306 end
306 end
307 end
307 end
308 respond_to do |format|
308 respond_to do |format|
309 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
309 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
310 format.api { head :ok }
310 format.api { render_api_ok }
311 end
311 end
312 end
312 end
313
313
314 private
314 private
315 def find_issue
315 def find_issue
316 # Issue.visible.find(...) can not be used to redirect user to the login form
316 # Issue.visible.find(...) can not be used to redirect user to the login form
317 # if the issue actually exists but requires authentication
317 # if the issue actually exists but requires authentication
318 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
318 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
319 unless @issue.visible?
319 unless @issue.visible?
320 deny_access
320 deny_access
321 return
321 return
322 end
322 end
323 @project = @issue.project
323 @project = @issue.project
324 rescue ActiveRecord::RecordNotFound
324 rescue ActiveRecord::RecordNotFound
325 render_404
325 render_404
326 end
326 end
327
327
328 def find_project
328 def find_project
329 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
329 project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
330 @project = Project.find(project_id)
330 @project = Project.find(project_id)
331 rescue ActiveRecord::RecordNotFound
331 rescue ActiveRecord::RecordNotFound
332 render_404
332 render_404
333 end
333 end
334
334
335 def retrieve_previous_and_next_issue_ids
335 def retrieve_previous_and_next_issue_ids
336 retrieve_query_from_session
336 retrieve_query_from_session
337 if @query
337 if @query
338 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
338 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
339 sort_update(@query.sortable_columns, 'issues_index_sort')
339 sort_update(@query.sortable_columns, 'issues_index_sort')
340 limit = 500
340 limit = 500
341 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
341 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
342 if (idx = issue_ids.index(@issue.id)) && idx < limit
342 if (idx = issue_ids.index(@issue.id)) && idx < limit
343 if issue_ids.size < 500
343 if issue_ids.size < 500
344 @issue_position = idx + 1
344 @issue_position = idx + 1
345 @issue_count = issue_ids.size
345 @issue_count = issue_ids.size
346 end
346 end
347 @prev_issue_id = issue_ids[idx - 1] if idx > 0
347 @prev_issue_id = issue_ids[idx - 1] if idx > 0
348 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
348 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
349 end
349 end
350 end
350 end
351 end
351 end
352
352
353 # Used by #edit and #update to set some common instance variables
353 # Used by #edit and #update to set some common instance variables
354 # from the params
354 # from the params
355 # TODO: Refactor, not everything in here is needed by #edit
355 # TODO: Refactor, not everything in here is needed by #edit
356 def update_issue_from_params
356 def update_issue_from_params
357 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
357 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
358 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
358 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
359 @time_entry.attributes = params[:time_entry]
359 @time_entry.attributes = params[:time_entry]
360
360
361 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
361 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
362 @issue.init_journal(User.current, @notes)
362 @issue.init_journal(User.current, @notes)
363
363
364 issue_attributes = params[:issue]
364 issue_attributes = params[:issue]
365 if issue_attributes && params[:conflict_resolution]
365 if issue_attributes && params[:conflict_resolution]
366 case params[:conflict_resolution]
366 case params[:conflict_resolution]
367 when 'overwrite'
367 when 'overwrite'
368 issue_attributes = issue_attributes.dup
368 issue_attributes = issue_attributes.dup
369 issue_attributes.delete(:lock_version)
369 issue_attributes.delete(:lock_version)
370 when 'add_notes'
370 when 'add_notes'
371 issue_attributes = {}
371 issue_attributes = {}
372 when 'cancel'
372 when 'cancel'
373 redirect_to issue_path(@issue)
373 redirect_to issue_path(@issue)
374 return false
374 return false
375 end
375 end
376 end
376 end
377 @issue.safe_attributes = issue_attributes
377 @issue.safe_attributes = issue_attributes
378 @priorities = IssuePriority.active
378 @priorities = IssuePriority.active
379 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
379 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
380 true
380 true
381 end
381 end
382
382
383 # TODO: Refactor, lots of extra code in here
383 # TODO: Refactor, lots of extra code in here
384 # TODO: Changing tracker on an existing issue should not trigger this
384 # TODO: Changing tracker on an existing issue should not trigger this
385 def build_new_issue_from_params
385 def build_new_issue_from_params
386 if params[:id].blank?
386 if params[:id].blank?
387 @issue = Issue.new
387 @issue = Issue.new
388 if params[:copy_from]
388 if params[:copy_from]
389 begin
389 begin
390 @copy_from = Issue.visible.find(params[:copy_from])
390 @copy_from = Issue.visible.find(params[:copy_from])
391 @copy_attachments = params[:copy_attachments].present? || request.get?
391 @copy_attachments = params[:copy_attachments].present? || request.get?
392 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
392 @issue.copy_from(@copy_from, :attachments => @copy_attachments)
393 rescue ActiveRecord::RecordNotFound
393 rescue ActiveRecord::RecordNotFound
394 render_404
394 render_404
395 return
395 return
396 end
396 end
397 end
397 end
398 @issue.project = @project
398 @issue.project = @project
399 else
399 else
400 @issue = @project.issues.visible.find(params[:id])
400 @issue = @project.issues.visible.find(params[:id])
401 end
401 end
402
402
403 @issue.project = @project
403 @issue.project = @project
404 @issue.author = User.current
404 @issue.author = User.current
405 # Tracker must be set before custom field values
405 # Tracker must be set before custom field values
406 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
406 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
407 if @issue.tracker.nil?
407 if @issue.tracker.nil?
408 render_error l(:error_no_tracker_in_project)
408 render_error l(:error_no_tracker_in_project)
409 return false
409 return false
410 end
410 end
411 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
411 @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
412 @issue.safe_attributes = params[:issue]
412 @issue.safe_attributes = params[:issue]
413
413
414 @priorities = IssuePriority.active
414 @priorities = IssuePriority.active
415 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
415 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
416 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
416 @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
417 end
417 end
418
418
419 def check_for_default_issue_status
419 def check_for_default_issue_status
420 if IssueStatus.default.nil?
420 if IssueStatus.default.nil?
421 render_error l(:error_no_default_issue_status)
421 render_error l(:error_no_default_issue_status)
422 return false
422 return false
423 end
423 end
424 end
424 end
425
425
426 def parse_params_for_bulk_issue_attributes(params)
426 def parse_params_for_bulk_issue_attributes(params)
427 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
427 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
428 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
428 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
429 if custom = attributes[:custom_field_values]
429 if custom = attributes[:custom_field_values]
430 custom.reject! {|k,v| v.blank?}
430 custom.reject! {|k,v| v.blank?}
431 custom.keys.each do |k|
431 custom.keys.each do |k|
432 if custom[k].is_a?(Array)
432 if custom[k].is_a?(Array)
433 custom[k] << '' if custom[k].delete('__none__')
433 custom[k] << '' if custom[k].delete('__none__')
434 else
434 else
435 custom[k] = '' if custom[k] == '__none__'
435 custom[k] = '' if custom[k] == '__none__'
436 end
436 end
437 end
437 end
438 end
438 end
439 attributes
439 attributes
440 end
440 end
441 end
441 end
@@ -1,144 +1,144
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MembersController < ApplicationController
18 class MembersController < ApplicationController
19 model_object Member
19 model_object Member
20 before_filter :find_model_object, :except => [:index, :create, :autocomplete]
20 before_filter :find_model_object, :except => [:index, :create, :autocomplete]
21 before_filter :find_project_from_association, :except => [:index, :create, :autocomplete]
21 before_filter :find_project_from_association, :except => [:index, :create, :autocomplete]
22 before_filter :find_project_by_project_id, :only => [:index, :create, :autocomplete]
22 before_filter :find_project_by_project_id, :only => [:index, :create, :autocomplete]
23 before_filter :authorize
23 before_filter :authorize
24 accept_api_auth :index, :show, :create, :update, :destroy
24 accept_api_auth :index, :show, :create, :update, :destroy
25
25
26 def index
26 def index
27 @offset, @limit = api_offset_and_limit
27 @offset, @limit = api_offset_and_limit
28 @member_count = @project.member_principals.count
28 @member_count = @project.member_principals.count
29 @member_pages = Paginator.new self, @member_count, @limit, params['page']
29 @member_pages = Paginator.new self, @member_count, @limit, params['page']
30 @offset ||= @member_pages.current.offset
30 @offset ||= @member_pages.current.offset
31 @members = @project.member_principals.all(
31 @members = @project.member_principals.all(
32 :order => "#{Member.table_name}.id",
32 :order => "#{Member.table_name}.id",
33 :limit => @limit,
33 :limit => @limit,
34 :offset => @offset
34 :offset => @offset
35 )
35 )
36
36
37 respond_to do |format|
37 respond_to do |format|
38 format.html { head 406 }
38 format.html { head 406 }
39 format.api
39 format.api
40 end
40 end
41 end
41 end
42
42
43 def show
43 def show
44 respond_to do |format|
44 respond_to do |format|
45 format.html { head 406 }
45 format.html { head 406 }
46 format.api
46 format.api
47 end
47 end
48 end
48 end
49
49
50 def create
50 def create
51 members = []
51 members = []
52 if params[:membership]
52 if params[:membership]
53 if params[:membership][:user_ids]
53 if params[:membership][:user_ids]
54 attrs = params[:membership].dup
54 attrs = params[:membership].dup
55 user_ids = attrs.delete(:user_ids)
55 user_ids = attrs.delete(:user_ids)
56 user_ids.each do |user_id|
56 user_ids.each do |user_id|
57 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
57 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
58 end
58 end
59 else
59 else
60 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
60 members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
61 end
61 end
62 @project.members << members
62 @project.members << members
63 end
63 end
64
64
65 respond_to do |format|
65 respond_to do |format|
66 if members.present? && members.all? {|m| m.valid? }
66 if members.present? && members.all? {|m| m.valid? }
67 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
67 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
68 format.js {
68 format.js {
69 render(:update) {|page|
69 render(:update) {|page|
70 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
70 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
71 page << 'hideOnLoad()'
71 page << 'hideOnLoad()'
72 members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
72 members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
73 }
73 }
74 }
74 }
75 format.api {
75 format.api {
76 @member = members.first
76 @member = members.first
77 render :action => 'show', :status => :created, :location => membership_url(@member)
77 render :action => 'show', :status => :created, :location => membership_url(@member)
78 }
78 }
79 else
79 else
80 format.js {
80 format.js {
81 render(:update) {|page|
81 render(:update) {|page|
82 errors = members.collect {|m|
82 errors = members.collect {|m|
83 m.errors.full_messages
83 m.errors.full_messages
84 }.flatten.uniq
84 }.flatten.uniq
85
85
86 page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', ')))
86 page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', ')))
87 }
87 }
88 }
88 }
89 format.api { render_validation_errors(members.first) }
89 format.api { render_validation_errors(members.first) }
90 end
90 end
91 end
91 end
92 end
92 end
93
93
94 def update
94 def update
95 if params[:membership]
95 if params[:membership]
96 @member.role_ids = params[:membership][:role_ids]
96 @member.role_ids = params[:membership][:role_ids]
97 end
97 end
98 saved = @member.save
98 saved = @member.save
99 respond_to do |format|
99 respond_to do |format|
100 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
100 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
101 format.js {
101 format.js {
102 render(:update) {|page|
102 render(:update) {|page|
103 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
103 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
104 page << 'hideOnLoad()'
104 page << 'hideOnLoad()'
105 page.visual_effect(:highlight, "member-#{@member.id}")
105 page.visual_effect(:highlight, "member-#{@member.id}")
106 }
106 }
107 }
107 }
108 format.api {
108 format.api {
109 if saved
109 if saved
110 head :ok
110 render_api_ok
111 else
111 else
112 render_validation_errors(@member)
112 render_validation_errors(@member)
113 end
113 end
114 }
114 }
115 end
115 end
116 end
116 end
117
117
118 def destroy
118 def destroy
119 if request.delete? && @member.deletable?
119 if request.delete? && @member.deletable?
120 @member.destroy
120 @member.destroy
121 end
121 end
122 respond_to do |format|
122 respond_to do |format|
123 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
123 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
124 format.js { render(:update) {|page|
124 format.js { render(:update) {|page|
125 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
125 page.replace_html "tab-content-members", :partial => 'projects/settings/members'
126 page << 'hideOnLoad()'
126 page << 'hideOnLoad()'
127 }
127 }
128 }
128 }
129 format.api {
129 format.api {
130 if @member.destroyed?
130 if @member.destroyed?
131 head :ok
131 render_api_ok
132 else
132 else
133 head :unprocessable_entity
133 head :unprocessable_entity
134 end
134 end
135 }
135 }
136 end
136 end
137 end
137 end
138
138
139 def autocomplete
139 def autocomplete
140 @principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
140 @principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
141 render :layout => false
141 render :layout => false
142 end
142 end
143
143
144 end
144 end
@@ -1,267 +1,267
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :roadmap, :only => :roadmap
20 menu_item :roadmap, :only => :roadmap
21 menu_item :settings, :only => :settings
21 menu_item :settings, :only => :settings
22
22
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 before_filter :authorize_global, :only => [:new, :create]
25 before_filter :authorize_global, :only => [:new, :create]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 accept_rss_auth :index
27 accept_rss_auth :index
28 accept_api_auth :index, :show, :create, :update, :destroy
28 accept_api_auth :index, :show, :create, :update, :destroy
29
29
30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
31 if controller.request.post?
31 if controller.request.post?
32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
32 controller.send :expire_action, :controller => 'welcome', :action => 'robots'
33 end
33 end
34 end
34 end
35
35
36 helper :sort
36 helper :sort
37 include SortHelper
37 include SortHelper
38 helper :custom_fields
38 helper :custom_fields
39 include CustomFieldsHelper
39 include CustomFieldsHelper
40 helper :issues
40 helper :issues
41 helper :queries
41 helper :queries
42 include QueriesHelper
42 include QueriesHelper
43 helper :repositories
43 helper :repositories
44 include RepositoriesHelper
44 include RepositoriesHelper
45 include ProjectsHelper
45 include ProjectsHelper
46
46
47 # Lists visible projects
47 # Lists visible projects
48 def index
48 def index
49 respond_to do |format|
49 respond_to do |format|
50 format.html {
50 format.html {
51 scope = Project
51 scope = Project
52 unless params[:closed]
52 unless params[:closed]
53 scope = scope.active
53 scope = scope.active
54 end
54 end
55 @projects = scope.visible.order('lft').all
55 @projects = scope.visible.order('lft').all
56 }
56 }
57 format.api {
57 format.api {
58 @offset, @limit = api_offset_and_limit
58 @offset, @limit = api_offset_and_limit
59 @project_count = Project.visible.count
59 @project_count = Project.visible.count
60 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
60 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
61 }
61 }
62 format.atom {
62 format.atom {
63 projects = Project.visible.find(:all, :order => 'created_on DESC',
63 projects = Project.visible.find(:all, :order => 'created_on DESC',
64 :limit => Setting.feeds_limit.to_i)
64 :limit => Setting.feeds_limit.to_i)
65 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
65 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
66 }
66 }
67 end
67 end
68 end
68 end
69
69
70 def new
70 def new
71 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
71 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
72 @trackers = Tracker.sorted.all
72 @trackers = Tracker.sorted.all
73 @project = Project.new
73 @project = Project.new
74 @project.safe_attributes = params[:project]
74 @project.safe_attributes = params[:project]
75 end
75 end
76
76
77 def create
77 def create
78 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
78 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
79 @trackers = Tracker.sorted.all
79 @trackers = Tracker.sorted.all
80 @project = Project.new
80 @project = Project.new
81 @project.safe_attributes = params[:project]
81 @project.safe_attributes = params[:project]
82
82
83 if validate_parent_id && @project.save
83 if validate_parent_id && @project.save
84 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
84 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
85 # Add current user as a project member if he is not admin
85 # Add current user as a project member if he is not admin
86 unless User.current.admin?
86 unless User.current.admin?
87 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
87 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
88 m = Member.new(:user => User.current, :roles => [r])
88 m = Member.new(:user => User.current, :roles => [r])
89 @project.members << m
89 @project.members << m
90 end
90 end
91 respond_to do |format|
91 respond_to do |format|
92 format.html {
92 format.html {
93 flash[:notice] = l(:notice_successful_create)
93 flash[:notice] = l(:notice_successful_create)
94 redirect_to(params[:continue] ?
94 redirect_to(params[:continue] ?
95 {:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} :
95 {:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} :
96 {:controller => 'projects', :action => 'settings', :id => @project}
96 {:controller => 'projects', :action => 'settings', :id => @project}
97 )
97 )
98 }
98 }
99 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
99 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
100 end
100 end
101 else
101 else
102 respond_to do |format|
102 respond_to do |format|
103 format.html { render :action => 'new' }
103 format.html { render :action => 'new' }
104 format.api { render_validation_errors(@project) }
104 format.api { render_validation_errors(@project) }
105 end
105 end
106 end
106 end
107
107
108 end
108 end
109
109
110 def copy
110 def copy
111 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
111 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
112 @trackers = Tracker.sorted.all
112 @trackers = Tracker.sorted.all
113 @root_projects = Project.find(:all,
113 @root_projects = Project.find(:all,
114 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
114 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
115 :order => 'name')
115 :order => 'name')
116 @source_project = Project.find(params[:id])
116 @source_project = Project.find(params[:id])
117 if request.get?
117 if request.get?
118 @project = Project.copy_from(@source_project)
118 @project = Project.copy_from(@source_project)
119 if @project
119 if @project
120 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
120 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
121 else
121 else
122 redirect_to :controller => 'admin', :action => 'projects'
122 redirect_to :controller => 'admin', :action => 'projects'
123 end
123 end
124 else
124 else
125 Mailer.with_deliveries(params[:notifications] == '1') do
125 Mailer.with_deliveries(params[:notifications] == '1') do
126 @project = Project.new
126 @project = Project.new
127 @project.safe_attributes = params[:project]
127 @project.safe_attributes = params[:project]
128 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
128 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
129 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
129 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
130 flash[:notice] = l(:notice_successful_create)
130 flash[:notice] = l(:notice_successful_create)
131 redirect_to :controller => 'projects', :action => 'settings', :id => @project
131 redirect_to :controller => 'projects', :action => 'settings', :id => @project
132 elsif !@project.new_record?
132 elsif !@project.new_record?
133 # Project was created
133 # Project was created
134 # But some objects were not copied due to validation failures
134 # But some objects were not copied due to validation failures
135 # (eg. issues from disabled trackers)
135 # (eg. issues from disabled trackers)
136 # TODO: inform about that
136 # TODO: inform about that
137 redirect_to :controller => 'projects', :action => 'settings', :id => @project
137 redirect_to :controller => 'projects', :action => 'settings', :id => @project
138 end
138 end
139 end
139 end
140 end
140 end
141 rescue ActiveRecord::RecordNotFound
141 rescue ActiveRecord::RecordNotFound
142 redirect_to :controller => 'admin', :action => 'projects'
142 redirect_to :controller => 'admin', :action => 'projects'
143 end
143 end
144
144
145 # Show @project
145 # Show @project
146 def show
146 def show
147 if params[:jump]
147 if params[:jump]
148 # try to redirect to the requested menu item
148 # try to redirect to the requested menu item
149 redirect_to_project_menu_item(@project, params[:jump]) && return
149 redirect_to_project_menu_item(@project, params[:jump]) && return
150 end
150 end
151
151
152 @users_by_role = @project.users_by_role
152 @users_by_role = @project.users_by_role
153 @subprojects = @project.children.visible.all
153 @subprojects = @project.children.visible.all
154 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
154 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
155 @trackers = @project.rolled_up_trackers
155 @trackers = @project.rolled_up_trackers
156
156
157 cond = @project.project_condition(Setting.display_subprojects_issues?)
157 cond = @project.project_condition(Setting.display_subprojects_issues?)
158
158
159 @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker)
159 @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker)
160 @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
160 @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker)
161
161
162 if User.current.allowed_to?(:view_time_entries, @project)
162 if User.current.allowed_to?(:view_time_entries, @project)
163 @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
163 @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
164 end
164 end
165
165
166 @key = User.current.rss_key
166 @key = User.current.rss_key
167
167
168 respond_to do |format|
168 respond_to do |format|
169 format.html
169 format.html
170 format.api
170 format.api
171 end
171 end
172 end
172 end
173
173
174 def settings
174 def settings
175 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
175 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
176 @issue_category ||= IssueCategory.new
176 @issue_category ||= IssueCategory.new
177 @member ||= @project.members.new
177 @member ||= @project.members.new
178 @trackers = Tracker.sorted.all
178 @trackers = Tracker.sorted.all
179 @wiki ||= @project.wiki
179 @wiki ||= @project.wiki
180 end
180 end
181
181
182 def edit
182 def edit
183 end
183 end
184
184
185 def update
185 def update
186 @project.safe_attributes = params[:project]
186 @project.safe_attributes = params[:project]
187 if validate_parent_id && @project.save
187 if validate_parent_id && @project.save
188 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
188 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
189 respond_to do |format|
189 respond_to do |format|
190 format.html {
190 format.html {
191 flash[:notice] = l(:notice_successful_update)
191 flash[:notice] = l(:notice_successful_update)
192 redirect_to :action => 'settings', :id => @project
192 redirect_to :action => 'settings', :id => @project
193 }
193 }
194 format.api { head :ok }
194 format.api { render_api_ok }
195 end
195 end
196 else
196 else
197 respond_to do |format|
197 respond_to do |format|
198 format.html {
198 format.html {
199 settings
199 settings
200 render :action => 'settings'
200 render :action => 'settings'
201 }
201 }
202 format.api { render_validation_errors(@project) }
202 format.api { render_validation_errors(@project) }
203 end
203 end
204 end
204 end
205 end
205 end
206
206
207 def modules
207 def modules
208 @project.enabled_module_names = params[:enabled_module_names]
208 @project.enabled_module_names = params[:enabled_module_names]
209 flash[:notice] = l(:notice_successful_update)
209 flash[:notice] = l(:notice_successful_update)
210 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
210 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
211 end
211 end
212
212
213 def archive
213 def archive
214 if request.post?
214 if request.post?
215 unless @project.archive
215 unless @project.archive
216 flash[:error] = l(:error_can_not_archive_project)
216 flash[:error] = l(:error_can_not_archive_project)
217 end
217 end
218 end
218 end
219 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
219 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
220 end
220 end
221
221
222 def unarchive
222 def unarchive
223 @project.unarchive if request.post? && !@project.active?
223 @project.unarchive if request.post? && !@project.active?
224 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
224 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
225 end
225 end
226
226
227 def close
227 def close
228 @project.close
228 @project.close
229 redirect_to project_path(@project)
229 redirect_to project_path(@project)
230 end
230 end
231
231
232 def reopen
232 def reopen
233 @project.reopen
233 @project.reopen
234 redirect_to project_path(@project)
234 redirect_to project_path(@project)
235 end
235 end
236
236
237 # Delete @project
237 # Delete @project
238 def destroy
238 def destroy
239 @project_to_destroy = @project
239 @project_to_destroy = @project
240 if api_request? || params[:confirm]
240 if api_request? || params[:confirm]
241 @project_to_destroy.destroy
241 @project_to_destroy.destroy
242 respond_to do |format|
242 respond_to do |format|
243 format.html { redirect_to :controller => 'admin', :action => 'projects' }
243 format.html { redirect_to :controller => 'admin', :action => 'projects' }
244 format.api { head :ok }
244 format.api { render_api_ok }
245 end
245 end
246 end
246 end
247 # hide project in layout
247 # hide project in layout
248 @project = nil
248 @project = nil
249 end
249 end
250
250
251 private
251 private
252
252
253 # Validates parent_id param according to user's permissions
253 # Validates parent_id param according to user's permissions
254 # TODO: move it to Project model in a validation that depends on User.current
254 # TODO: move it to Project model in a validation that depends on User.current
255 def validate_parent_id
255 def validate_parent_id
256 return true if User.current.admin?
256 return true if User.current.admin?
257 parent_id = params[:project] && params[:project][:parent_id]
257 parent_id = params[:project] && params[:project][:parent_id]
258 if parent_id || @project.new_record?
258 if parent_id || @project.new_record?
259 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
259 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
260 unless @project.allowed_parents.include?(parent)
260 unless @project.allowed_parents.include?(parent)
261 @project.errors.add :parent_id, :invalid
261 @project.errors.add :parent_id, :invalid
262 return false
262 return false
263 end
263 end
264 end
264 end
265 true
265 true
266 end
266 end
267 end
267 end
@@ -1,344 +1,344
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20
20
21 before_filter :find_project_for_new_time_entry, :only => [:create]
21 before_filter :find_project_for_new_time_entry, :only => [:create]
22 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 before_filter :find_time_entry, :only => [:show, :edit, :update]
23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
24 before_filter :authorize, :except => [:new, :index, :report]
24 before_filter :authorize, :except => [:new, :index, :report]
25
25
26 before_filter :find_optional_project, :only => [:index, :report]
26 before_filter :find_optional_project, :only => [:index, :report]
27 before_filter :find_optional_project_for_new_time_entry, :only => [:new]
27 before_filter :find_optional_project_for_new_time_entry, :only => [:new]
28 before_filter :authorize_global, :only => [:new, :index, :report]
28 before_filter :authorize_global, :only => [:new, :index, :report]
29
29
30 accept_rss_auth :index
30 accept_rss_auth :index
31 accept_api_auth :index, :show, :create, :update, :destroy
31 accept_api_auth :index, :show, :create, :update, :destroy
32
32
33 helper :sort
33 helper :sort
34 include SortHelper
34 include SortHelper
35 helper :issues
35 helper :issues
36 include TimelogHelper
36 include TimelogHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39
39
40 def index
40 def index
41 sort_init 'spent_on', 'desc'
41 sort_init 'spent_on', 'desc'
42 sort_update 'spent_on' => ['spent_on', "#{TimeEntry.table_name}.created_on"],
42 sort_update 'spent_on' => ['spent_on', "#{TimeEntry.table_name}.created_on"],
43 'user' => 'user_id',
43 'user' => 'user_id',
44 'activity' => 'activity_id',
44 'activity' => 'activity_id',
45 'project' => "#{Project.table_name}.name",
45 'project' => "#{Project.table_name}.name",
46 'issue' => 'issue_id',
46 'issue' => 'issue_id',
47 'hours' => 'hours'
47 'hours' => 'hours'
48
48
49 retrieve_date_range
49 retrieve_date_range
50
50
51 scope = TimeEntry.visible.spent_between(@from, @to)
51 scope = TimeEntry.visible.spent_between(@from, @to)
52 if @issue
52 if @issue
53 scope = scope.on_issue(@issue)
53 scope = scope.on_issue(@issue)
54 elsif @project
54 elsif @project
55 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
55 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
56 end
56 end
57
57
58 respond_to do |format|
58 respond_to do |format|
59 format.html {
59 format.html {
60 # Paginate results
60 # Paginate results
61 @entry_count = scope.count
61 @entry_count = scope.count
62 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
62 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
63 @entries = scope.all(
63 @entries = scope.all(
64 :include => [:project, :activity, :user, {:issue => :tracker}],
64 :include => [:project, :activity, :user, {:issue => :tracker}],
65 :order => sort_clause,
65 :order => sort_clause,
66 :limit => @entry_pages.items_per_page,
66 :limit => @entry_pages.items_per_page,
67 :offset => @entry_pages.current.offset
67 :offset => @entry_pages.current.offset
68 )
68 )
69 @total_hours = scope.sum(:hours).to_f
69 @total_hours = scope.sum(:hours).to_f
70
70
71 render :layout => !request.xhr?
71 render :layout => !request.xhr?
72 }
72 }
73 format.api {
73 format.api {
74 @entry_count = scope.count
74 @entry_count = scope.count
75 @offset, @limit = api_offset_and_limit
75 @offset, @limit = api_offset_and_limit
76 @entries = scope.all(
76 @entries = scope.all(
77 :include => [:project, :activity, :user, {:issue => :tracker}],
77 :include => [:project, :activity, :user, {:issue => :tracker}],
78 :order => sort_clause,
78 :order => sort_clause,
79 :limit => @limit,
79 :limit => @limit,
80 :offset => @offset
80 :offset => @offset
81 )
81 )
82 }
82 }
83 format.atom {
83 format.atom {
84 entries = scope.all(
84 entries = scope.all(
85 :include => [:project, :activity, :user, {:issue => :tracker}],
85 :include => [:project, :activity, :user, {:issue => :tracker}],
86 :order => "#{TimeEntry.table_name}.created_on DESC",
86 :order => "#{TimeEntry.table_name}.created_on DESC",
87 :limit => Setting.feeds_limit.to_i
87 :limit => Setting.feeds_limit.to_i
88 )
88 )
89 render_feed(entries, :title => l(:label_spent_time))
89 render_feed(entries, :title => l(:label_spent_time))
90 }
90 }
91 format.csv {
91 format.csv {
92 # Export all entries
92 # Export all entries
93 @entries = scope.all(
93 @entries = scope.all(
94 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
94 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
95 :order => sort_clause
95 :order => sort_clause
96 )
96 )
97 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
97 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
98 }
98 }
99 end
99 end
100 end
100 end
101
101
102 def report
102 def report
103 retrieve_date_range
103 retrieve_date_range
104 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
104 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
105
105
106 respond_to do |format|
106 respond_to do |format|
107 format.html { render :layout => !request.xhr? }
107 format.html { render :layout => !request.xhr? }
108 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
108 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
109 end
109 end
110 end
110 end
111
111
112 def show
112 def show
113 respond_to do |format|
113 respond_to do |format|
114 # TODO: Implement html response
114 # TODO: Implement html response
115 format.html { render :nothing => true, :status => 406 }
115 format.html { render :nothing => true, :status => 406 }
116 format.api
116 format.api
117 end
117 end
118 end
118 end
119
119
120 def new
120 def new
121 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
121 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
122 @time_entry.safe_attributes = params[:time_entry]
122 @time_entry.safe_attributes = params[:time_entry]
123 end
123 end
124
124
125 def create
125 def create
126 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
126 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
127 @time_entry.safe_attributes = params[:time_entry]
127 @time_entry.safe_attributes = params[:time_entry]
128
128
129 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
129 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
130
130
131 if @time_entry.save
131 if @time_entry.save
132 respond_to do |format|
132 respond_to do |format|
133 format.html {
133 format.html {
134 flash[:notice] = l(:notice_successful_create)
134 flash[:notice] = l(:notice_successful_create)
135 if params[:continue]
135 if params[:continue]
136 if params[:project_id]
136 if params[:project_id]
137 redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue,
137 redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue,
138 :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
138 :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
139 :back_url => params[:back_url]
139 :back_url => params[:back_url]
140 else
140 else
141 redirect_to :action => 'new',
141 redirect_to :action => 'new',
142 :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
142 :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
143 :back_url => params[:back_url]
143 :back_url => params[:back_url]
144 end
144 end
145 else
145 else
146 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
146 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
147 end
147 end
148 }
148 }
149 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
149 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
150 end
150 end
151 else
151 else
152 respond_to do |format|
152 respond_to do |format|
153 format.html { render :action => 'new' }
153 format.html { render :action => 'new' }
154 format.api { render_validation_errors(@time_entry) }
154 format.api { render_validation_errors(@time_entry) }
155 end
155 end
156 end
156 end
157 end
157 end
158
158
159 def edit
159 def edit
160 @time_entry.safe_attributes = params[:time_entry]
160 @time_entry.safe_attributes = params[:time_entry]
161 end
161 end
162
162
163 def update
163 def update
164 @time_entry.safe_attributes = params[:time_entry]
164 @time_entry.safe_attributes = params[:time_entry]
165
165
166 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
166 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
167
167
168 if @time_entry.save
168 if @time_entry.save
169 respond_to do |format|
169 respond_to do |format|
170 format.html {
170 format.html {
171 flash[:notice] = l(:notice_successful_update)
171 flash[:notice] = l(:notice_successful_update)
172 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
172 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
173 }
173 }
174 format.api { head :ok }
174 format.api { render_api_ok }
175 end
175 end
176 else
176 else
177 respond_to do |format|
177 respond_to do |format|
178 format.html { render :action => 'edit' }
178 format.html { render :action => 'edit' }
179 format.api { render_validation_errors(@time_entry) }
179 format.api { render_validation_errors(@time_entry) }
180 end
180 end
181 end
181 end
182 end
182 end
183
183
184 def bulk_edit
184 def bulk_edit
185 @available_activities = TimeEntryActivity.shared.active
185 @available_activities = TimeEntryActivity.shared.active
186 @custom_fields = TimeEntry.first.available_custom_fields
186 @custom_fields = TimeEntry.first.available_custom_fields
187 end
187 end
188
188
189 def bulk_update
189 def bulk_update
190 attributes = parse_params_for_bulk_time_entry_attributes(params)
190 attributes = parse_params_for_bulk_time_entry_attributes(params)
191
191
192 unsaved_time_entry_ids = []
192 unsaved_time_entry_ids = []
193 @time_entries.each do |time_entry|
193 @time_entries.each do |time_entry|
194 time_entry.reload
194 time_entry.reload
195 time_entry.safe_attributes = attributes
195 time_entry.safe_attributes = attributes
196 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
196 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
197 unless time_entry.save
197 unless time_entry.save
198 # Keep unsaved time_entry ids to display them in flash error
198 # Keep unsaved time_entry ids to display them in flash error
199 unsaved_time_entry_ids << time_entry.id
199 unsaved_time_entry_ids << time_entry.id
200 end
200 end
201 end
201 end
202 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
202 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
203 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
203 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
204 end
204 end
205
205
206 def destroy
206 def destroy
207 destroyed = TimeEntry.transaction do
207 destroyed = TimeEntry.transaction do
208 @time_entries.each do |t|
208 @time_entries.each do |t|
209 unless t.destroy && t.destroyed?
209 unless t.destroy && t.destroyed?
210 raise ActiveRecord::Rollback
210 raise ActiveRecord::Rollback
211 end
211 end
212 end
212 end
213 end
213 end
214
214
215 respond_to do |format|
215 respond_to do |format|
216 format.html {
216 format.html {
217 if destroyed
217 if destroyed
218 flash[:notice] = l(:notice_successful_delete)
218 flash[:notice] = l(:notice_successful_delete)
219 else
219 else
220 flash[:error] = l(:notice_unable_delete_time_entry)
220 flash[:error] = l(:notice_unable_delete_time_entry)
221 end
221 end
222 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
222 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
223 }
223 }
224 format.api {
224 format.api {
225 if destroyed
225 if destroyed
226 head :ok
226 render_api_ok
227 else
227 else
228 render_validation_errors(@time_entries)
228 render_validation_errors(@time_entries)
229 end
229 end
230 }
230 }
231 end
231 end
232 end
232 end
233
233
234 private
234 private
235 def find_time_entry
235 def find_time_entry
236 @time_entry = TimeEntry.find(params[:id])
236 @time_entry = TimeEntry.find(params[:id])
237 unless @time_entry.editable_by?(User.current)
237 unless @time_entry.editable_by?(User.current)
238 render_403
238 render_403
239 return false
239 return false
240 end
240 end
241 @project = @time_entry.project
241 @project = @time_entry.project
242 rescue ActiveRecord::RecordNotFound
242 rescue ActiveRecord::RecordNotFound
243 render_404
243 render_404
244 end
244 end
245
245
246 def find_time_entries
246 def find_time_entries
247 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
247 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
248 raise ActiveRecord::RecordNotFound if @time_entries.empty?
248 raise ActiveRecord::RecordNotFound if @time_entries.empty?
249 @projects = @time_entries.collect(&:project).compact.uniq
249 @projects = @time_entries.collect(&:project).compact.uniq
250 @project = @projects.first if @projects.size == 1
250 @project = @projects.first if @projects.size == 1
251 rescue ActiveRecord::RecordNotFound
251 rescue ActiveRecord::RecordNotFound
252 render_404
252 render_404
253 end
253 end
254
254
255 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
255 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
256 if unsaved_time_entry_ids.empty?
256 if unsaved_time_entry_ids.empty?
257 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
257 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
258 else
258 else
259 flash[:error] = l(:notice_failed_to_save_time_entries,
259 flash[:error] = l(:notice_failed_to_save_time_entries,
260 :count => unsaved_time_entry_ids.size,
260 :count => unsaved_time_entry_ids.size,
261 :total => time_entries.size,
261 :total => time_entries.size,
262 :ids => '#' + unsaved_time_entry_ids.join(', #'))
262 :ids => '#' + unsaved_time_entry_ids.join(', #'))
263 end
263 end
264 end
264 end
265
265
266 def find_optional_project_for_new_time_entry
266 def find_optional_project_for_new_time_entry
267 if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
267 if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
268 @project = Project.find(project_id)
268 @project = Project.find(project_id)
269 end
269 end
270 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
270 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
271 @issue = Issue.find(issue_id)
271 @issue = Issue.find(issue_id)
272 @project ||= @issue.project
272 @project ||= @issue.project
273 end
273 end
274 rescue ActiveRecord::RecordNotFound
274 rescue ActiveRecord::RecordNotFound
275 render_404
275 render_404
276 end
276 end
277
277
278 def find_project_for_new_time_entry
278 def find_project_for_new_time_entry
279 find_optional_project_for_new_time_entry
279 find_optional_project_for_new_time_entry
280 if @project.nil?
280 if @project.nil?
281 render_404
281 render_404
282 end
282 end
283 end
283 end
284
284
285 def find_optional_project
285 def find_optional_project
286 if !params[:issue_id].blank?
286 if !params[:issue_id].blank?
287 @issue = Issue.find(params[:issue_id])
287 @issue = Issue.find(params[:issue_id])
288 @project = @issue.project
288 @project = @issue.project
289 elsif !params[:project_id].blank?
289 elsif !params[:project_id].blank?
290 @project = Project.find(params[:project_id])
290 @project = Project.find(params[:project_id])
291 end
291 end
292 end
292 end
293
293
294 # Retrieves the date range based on predefined ranges or specific from/to param dates
294 # Retrieves the date range based on predefined ranges or specific from/to param dates
295 def retrieve_date_range
295 def retrieve_date_range
296 @free_period = false
296 @free_period = false
297 @from, @to = nil, nil
297 @from, @to = nil, nil
298
298
299 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
299 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
300 case params[:period].to_s
300 case params[:period].to_s
301 when 'today'
301 when 'today'
302 @from = @to = Date.today
302 @from = @to = Date.today
303 when 'yesterday'
303 when 'yesterday'
304 @from = @to = Date.today - 1
304 @from = @to = Date.today - 1
305 when 'current_week'
305 when 'current_week'
306 @from = Date.today - (Date.today.cwday - 1)%7
306 @from = Date.today - (Date.today.cwday - 1)%7
307 @to = @from + 6
307 @to = @from + 6
308 when 'last_week'
308 when 'last_week'
309 @from = Date.today - 7 - (Date.today.cwday - 1)%7
309 @from = Date.today - 7 - (Date.today.cwday - 1)%7
310 @to = @from + 6
310 @to = @from + 6
311 when '7_days'
311 when '7_days'
312 @from = Date.today - 7
312 @from = Date.today - 7
313 @to = Date.today
313 @to = Date.today
314 when 'current_month'
314 when 'current_month'
315 @from = Date.civil(Date.today.year, Date.today.month, 1)
315 @from = Date.civil(Date.today.year, Date.today.month, 1)
316 @to = (@from >> 1) - 1
316 @to = (@from >> 1) - 1
317 when 'last_month'
317 when 'last_month'
318 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
318 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
319 @to = (@from >> 1) - 1
319 @to = (@from >> 1) - 1
320 when '30_days'
320 when '30_days'
321 @from = Date.today - 30
321 @from = Date.today - 30
322 @to = Date.today
322 @to = Date.today
323 when 'current_year'
323 when 'current_year'
324 @from = Date.civil(Date.today.year, 1, 1)
324 @from = Date.civil(Date.today.year, 1, 1)
325 @to = Date.civil(Date.today.year, 12, 31)
325 @to = Date.civil(Date.today.year, 12, 31)
326 end
326 end
327 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
327 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
328 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
328 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
329 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
329 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
330 @free_period = true
330 @free_period = true
331 else
331 else
332 # default
332 # default
333 end
333 end
334
334
335 @from, @to = @to, @from if @from && @to && @from > @to
335 @from, @to = @to, @from if @from && @to && @from > @to
336 end
336 end
337
337
338 def parse_params_for_bulk_time_entry_attributes(params)
338 def parse_params_for_bulk_time_entry_attributes(params)
339 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
339 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
340 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
340 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
341 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
341 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
342 attributes
342 attributes
343 end
343 end
344 end
344 end
@@ -1,227 +1,227
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class UsersController < ApplicationController
18 class UsersController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :show
21 before_filter :require_admin, :except => :show
22 before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
22 before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
23 accept_api_auth :index, :show, :create, :update, :destroy
23 accept_api_auth :index, :show, :create, :update, :destroy
24
24
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27 helper :custom_fields
27 helper :custom_fields
28 include CustomFieldsHelper
28 include CustomFieldsHelper
29
29
30 def index
30 def index
31 sort_init 'login', 'asc'
31 sort_init 'login', 'asc'
32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
33
33
34 case params[:format]
34 case params[:format]
35 when 'xml', 'json'
35 when 'xml', 'json'
36 @offset, @limit = api_offset_and_limit
36 @offset, @limit = api_offset_and_limit
37 else
37 else
38 @limit = per_page_option
38 @limit = per_page_option
39 end
39 end
40
40
41 @status = params[:status] || 1
41 @status = params[:status] || 1
42
42
43 scope = User.logged.status(@status)
43 scope = User.logged.status(@status)
44 scope = scope.like(params[:name]) if params[:name].present?
44 scope = scope.like(params[:name]) if params[:name].present?
45 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
45 scope = scope.in_group(params[:group_id]) if params[:group_id].present?
46
46
47 @user_count = scope.count
47 @user_count = scope.count
48 @user_pages = Paginator.new self, @user_count, @limit, params['page']
48 @user_pages = Paginator.new self, @user_count, @limit, params['page']
49 @offset ||= @user_pages.current.offset
49 @offset ||= @user_pages.current.offset
50 @users = scope.find :all,
50 @users = scope.find :all,
51 :order => sort_clause,
51 :order => sort_clause,
52 :limit => @limit,
52 :limit => @limit,
53 :offset => @offset
53 :offset => @offset
54
54
55 respond_to do |format|
55 respond_to do |format|
56 format.html {
56 format.html {
57 @groups = Group.all.sort
57 @groups = Group.all.sort
58 render :layout => !request.xhr?
58 render :layout => !request.xhr?
59 }
59 }
60 format.api
60 format.api
61 end
61 end
62 end
62 end
63
63
64 def show
64 def show
65 # show projects based on current user visibility
65 # show projects based on current user visibility
66 @memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
66 @memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
67
67
68 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
68 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
69 @events_by_day = events.group_by(&:event_date)
69 @events_by_day = events.group_by(&:event_date)
70
70
71 unless User.current.admin?
71 unless User.current.admin?
72 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
72 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
73 render_404
73 render_404
74 return
74 return
75 end
75 end
76 end
76 end
77
77
78 respond_to do |format|
78 respond_to do |format|
79 format.html { render :layout => 'base' }
79 format.html { render :layout => 'base' }
80 format.api
80 format.api
81 end
81 end
82 end
82 end
83
83
84 def new
84 def new
85 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
85 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
86 @auth_sources = AuthSource.find(:all)
86 @auth_sources = AuthSource.find(:all)
87 end
87 end
88
88
89 def create
89 def create
90 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
90 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
91 @user.safe_attributes = params[:user]
91 @user.safe_attributes = params[:user]
92 @user.admin = params[:user][:admin] || false
92 @user.admin = params[:user][:admin] || false
93 @user.login = params[:user][:login]
93 @user.login = params[:user][:login]
94 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
94 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
95
95
96 if @user.save
96 if @user.save
97 @user.pref.attributes = params[:pref]
97 @user.pref.attributes = params[:pref]
98 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
98 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
99 @user.pref.save
99 @user.pref.save
100 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
100 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
101
101
102 Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
102 Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
103
103
104 respond_to do |format|
104 respond_to do |format|
105 format.html {
105 format.html {
106 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
106 flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user)))
107 redirect_to(params[:continue] ?
107 redirect_to(params[:continue] ?
108 {:controller => 'users', :action => 'new'} :
108 {:controller => 'users', :action => 'new'} :
109 {:controller => 'users', :action => 'edit', :id => @user}
109 {:controller => 'users', :action => 'edit', :id => @user}
110 )
110 )
111 }
111 }
112 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
112 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
113 end
113 end
114 else
114 else
115 @auth_sources = AuthSource.find(:all)
115 @auth_sources = AuthSource.find(:all)
116 # Clear password input
116 # Clear password input
117 @user.password = @user.password_confirmation = nil
117 @user.password = @user.password_confirmation = nil
118
118
119 respond_to do |format|
119 respond_to do |format|
120 format.html { render :action => 'new' }
120 format.html { render :action => 'new' }
121 format.api { render_validation_errors(@user) }
121 format.api { render_validation_errors(@user) }
122 end
122 end
123 end
123 end
124 end
124 end
125
125
126 def edit
126 def edit
127 @auth_sources = AuthSource.find(:all)
127 @auth_sources = AuthSource.find(:all)
128 @membership ||= Member.new
128 @membership ||= Member.new
129 end
129 end
130
130
131 def update
131 def update
132 @user.admin = params[:user][:admin] if params[:user][:admin]
132 @user.admin = params[:user][:admin] if params[:user][:admin]
133 @user.login = params[:user][:login] if params[:user][:login]
133 @user.login = params[:user][:login] if params[:user][:login]
134 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
134 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
135 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
135 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
136 end
136 end
137 @user.safe_attributes = params[:user]
137 @user.safe_attributes = params[:user]
138 # Was the account actived ? (do it before User#save clears the change)
138 # Was the account actived ? (do it before User#save clears the change)
139 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
139 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
140 # TODO: Similar to My#account
140 # TODO: Similar to My#account
141 @user.pref.attributes = params[:pref]
141 @user.pref.attributes = params[:pref]
142 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
142 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
143
143
144 if @user.save
144 if @user.save
145 @user.pref.save
145 @user.pref.save
146 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
146 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
147
147
148 if was_activated
148 if was_activated
149 Mailer.account_activated(@user).deliver
149 Mailer.account_activated(@user).deliver
150 elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
150 elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
151 Mailer.account_information(@user, params[:user][:password]).deliver
151 Mailer.account_information(@user, params[:user][:password]).deliver
152 end
152 end
153
153
154 respond_to do |format|
154 respond_to do |format|
155 format.html {
155 format.html {
156 flash[:notice] = l(:notice_successful_update)
156 flash[:notice] = l(:notice_successful_update)
157 redirect_to_referer_or edit_user_path(@user)
157 redirect_to_referer_or edit_user_path(@user)
158 }
158 }
159 format.api { head :ok }
159 format.api { render_api_ok }
160 end
160 end
161 else
161 else
162 @auth_sources = AuthSource.find(:all)
162 @auth_sources = AuthSource.find(:all)
163 @membership ||= Member.new
163 @membership ||= Member.new
164 # Clear password input
164 # Clear password input
165 @user.password = @user.password_confirmation = nil
165 @user.password = @user.password_confirmation = nil
166
166
167 respond_to do |format|
167 respond_to do |format|
168 format.html { render :action => :edit }
168 format.html { render :action => :edit }
169 format.api { render_validation_errors(@user) }
169 format.api { render_validation_errors(@user) }
170 end
170 end
171 end
171 end
172 end
172 end
173
173
174 def destroy
174 def destroy
175 @user.destroy
175 @user.destroy
176 respond_to do |format|
176 respond_to do |format|
177 format.html { redirect_to_referer_or(users_url) }
177 format.html { redirect_to_referer_or(users_url) }
178 format.api { head :ok }
178 format.api { render_api_ok }
179 end
179 end
180 end
180 end
181
181
182 def edit_membership
182 def edit_membership
183 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
183 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
184 @membership.save
184 @membership.save
185 respond_to do |format|
185 respond_to do |format|
186 if @membership.valid?
186 if @membership.valid?
187 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
187 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
188 format.js {
188 format.js {
189 render(:update) {|page|
189 render(:update) {|page|
190 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
190 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
191 page.visual_effect(:highlight, "member-#{@membership.id}")
191 page.visual_effect(:highlight, "member-#{@membership.id}")
192 }
192 }
193 }
193 }
194 else
194 else
195 format.js {
195 format.js {
196 render(:update) {|page|
196 render(:update) {|page|
197 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
197 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
198 }
198 }
199 }
199 }
200 end
200 end
201 end
201 end
202 end
202 end
203
203
204 def destroy_membership
204 def destroy_membership
205 @membership = Member.find(params[:membership_id])
205 @membership = Member.find(params[:membership_id])
206 if @membership.deletable?
206 if @membership.deletable?
207 @membership.destroy
207 @membership.destroy
208 end
208 end
209 respond_to do |format|
209 respond_to do |format|
210 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
210 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
211 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
211 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
212 end
212 end
213 end
213 end
214
214
215 private
215 private
216
216
217 def find_user
217 def find_user
218 if params[:id] == 'current'
218 if params[:id] == 'current'
219 require_login || return
219 require_login || return
220 @user = User.current
220 @user = User.current
221 else
221 else
222 @user = User.find(params[:id])
222 @user = User.find(params[:id])
223 end
223 end
224 rescue ActiveRecord::RecordNotFound
224 rescue ActiveRecord::RecordNotFound
225 render_404
225 render_404
226 end
226 end
227 end
227 end
@@ -1,205 +1,205
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class VersionsController < ApplicationController
18 class VersionsController < ApplicationController
19 menu_item :roadmap
19 menu_item :roadmap
20 model_object Version
20 model_object Version
21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23 before_filter :find_project, :only => [:index, :new, :create, :close_completed]
23 before_filter :find_project, :only => [:index, :new, :create, :close_completed]
24 before_filter :authorize
24 before_filter :authorize
25
25
26 accept_api_auth :index, :show, :create, :update, :destroy
26 accept_api_auth :index, :show, :create, :update, :destroy
27
27
28 helper :custom_fields
28 helper :custom_fields
29 helper :projects
29 helper :projects
30
30
31 def index
31 def index
32 respond_to do |format|
32 respond_to do |format|
33 format.html {
33 format.html {
34 @trackers = @project.trackers.find(:all, :order => 'position')
34 @trackers = @project.trackers.find(:all, :order => 'position')
35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
38
38
39 @versions = @project.shared_versions || []
39 @versions = @project.shared_versions || []
40 @versions += @project.rolled_up_versions.visible if @with_subprojects
40 @versions += @project.rolled_up_versions.visible if @with_subprojects
41 @versions = @versions.uniq.sort
41 @versions = @versions.uniq.sort
42 unless params[:completed]
42 unless params[:completed]
43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
44 @versions -= @completed_versions
44 @versions -= @completed_versions
45 end
45 end
46
46
47 @issues_by_version = {}
47 @issues_by_version = {}
48 if @selected_tracker_ids.any? && @versions.any?
48 if @selected_tracker_ids.any? && @versions.any?
49 issues = Issue.visible.all(
49 issues = Issue.visible.all(
50 :include => [:project, :status, :tracker, :priority, :fixed_version],
50 :include => [:project, :status, :tracker, :priority, :fixed_version],
51 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)},
51 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)},
52 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id"
52 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id"
53 )
53 )
54 @issues_by_version = issues.group_by(&:fixed_version)
54 @issues_by_version = issues.group_by(&:fixed_version)
55 end
55 end
56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
57 }
57 }
58 format.api {
58 format.api {
59 @versions = @project.shared_versions.all
59 @versions = @project.shared_versions.all
60 }
60 }
61 end
61 end
62 end
62 end
63
63
64 def show
64 def show
65 respond_to do |format|
65 respond_to do |format|
66 format.html {
66 format.html {
67 @issues = @version.fixed_issues.visible.find(:all,
67 @issues = @version.fixed_issues.visible.find(:all,
68 :include => [:status, :tracker, :priority],
68 :include => [:status, :tracker, :priority],
69 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
69 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
70 }
70 }
71 format.api
71 format.api
72 end
72 end
73 end
73 end
74
74
75 def new
75 def new
76 @version = @project.versions.build
76 @version = @project.versions.build
77 @version.safe_attributes = params[:version]
77 @version.safe_attributes = params[:version]
78
78
79 respond_to do |format|
79 respond_to do |format|
80 format.html
80 format.html
81 format.js do
81 format.js do
82 render :update do |page|
82 render :update do |page|
83 page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
83 page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
84 page << "showModal('ajax-modal', '600px');"
84 page << "showModal('ajax-modal', '600px');"
85 page << "Form.Element.focus('version_name');"
85 page << "Form.Element.focus('version_name');"
86 end
86 end
87 end
87 end
88 end
88 end
89 end
89 end
90
90
91 def create
91 def create
92 @version = @project.versions.build
92 @version = @project.versions.build
93 if params[:version]
93 if params[:version]
94 attributes = params[:version].dup
94 attributes = params[:version].dup
95 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
95 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
96 @version.safe_attributes = attributes
96 @version.safe_attributes = attributes
97 end
97 end
98
98
99 if request.post?
99 if request.post?
100 if @version.save
100 if @version.save
101 respond_to do |format|
101 respond_to do |format|
102 format.html do
102 format.html do
103 flash[:notice] = l(:notice_successful_create)
103 flash[:notice] = l(:notice_successful_create)
104 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
104 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
105 end
105 end
106 format.js do
106 format.js do
107 render(:update) {|page|
107 render(:update) {|page|
108 page << 'hideModal();'
108 page << 'hideModal();'
109 # IE doesn't support the replace_html rjs method for select box options
109 # IE doesn't support the replace_html rjs method for select box options
110 page.replace "issue_fixed_version_id",
110 page.replace "issue_fixed_version_id",
111 content_tag('select', content_tag('option') + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
111 content_tag('select', content_tag('option') + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
112 }
112 }
113 end
113 end
114 format.api do
114 format.api do
115 render :action => 'show', :status => :created, :location => version_url(@version)
115 render :action => 'show', :status => :created, :location => version_url(@version)
116 end
116 end
117 end
117 end
118 else
118 else
119 respond_to do |format|
119 respond_to do |format|
120 format.html { render :action => 'new' }
120 format.html { render :action => 'new' }
121 format.js do
121 format.js do
122 render :update do |page|
122 render :update do |page|
123 page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
123 page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
124 page << "Form.Element.focus('version_name');"
124 page << "Form.Element.focus('version_name');"
125 end
125 end
126 end
126 end
127 format.api { render_validation_errors(@version) }
127 format.api { render_validation_errors(@version) }
128 end
128 end
129 end
129 end
130 end
130 end
131 end
131 end
132
132
133 def edit
133 def edit
134 end
134 end
135
135
136 def update
136 def update
137 if request.put? && params[:version]
137 if request.put? && params[:version]
138 attributes = params[:version].dup
138 attributes = params[:version].dup
139 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
139 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
140 @version.safe_attributes = attributes
140 @version.safe_attributes = attributes
141 if @version.save
141 if @version.save
142 respond_to do |format|
142 respond_to do |format|
143 format.html {
143 format.html {
144 flash[:notice] = l(:notice_successful_update)
144 flash[:notice] = l(:notice_successful_update)
145 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
145 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
146 }
146 }
147 format.api { head :ok }
147 format.api { render_api_ok }
148 end
148 end
149 else
149 else
150 respond_to do |format|
150 respond_to do |format|
151 format.html { render :action => 'edit' }
151 format.html { render :action => 'edit' }
152 format.api { render_validation_errors(@version) }
152 format.api { render_validation_errors(@version) }
153 end
153 end
154 end
154 end
155 end
155 end
156 end
156 end
157
157
158 def close_completed
158 def close_completed
159 if request.put?
159 if request.put?
160 @project.close_completed_versions
160 @project.close_completed_versions
161 end
161 end
162 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
162 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
163 end
163 end
164
164
165 def destroy
165 def destroy
166 if @version.fixed_issues.empty?
166 if @version.fixed_issues.empty?
167 @version.destroy
167 @version.destroy
168 respond_to do |format|
168 respond_to do |format|
169 format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
169 format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
170 format.api { head :ok }
170 format.api { render_api_ok }
171 end
171 end
172 else
172 else
173 respond_to do |format|
173 respond_to do |format|
174 format.html {
174 format.html {
175 flash[:error] = l(:notice_unable_delete_version)
175 flash[:error] = l(:notice_unable_delete_version)
176 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
176 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
177 }
177 }
178 format.api { head :unprocessable_entity }
178 format.api { head :unprocessable_entity }
179 end
179 end
180 end
180 end
181 end
181 end
182
182
183 def status_by
183 def status_by
184 respond_to do |format|
184 respond_to do |format|
185 format.html { render :action => 'show' }
185 format.html { render :action => 'show' }
186 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
186 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
187 end
187 end
188 end
188 end
189
189
190 private
190 private
191 def find_project
191 def find_project
192 @project = Project.find(params[:project_id])
192 @project = Project.find(params[:project_id])
193 rescue ActiveRecord::RecordNotFound
193 rescue ActiveRecord::RecordNotFound
194 render_404
194 render_404
195 end
195 end
196
196
197 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
197 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
198 if ids = params[:tracker_ids]
198 if ids = params[:tracker_ids]
199 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
199 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
200 else
200 else
201 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
201 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
202 end
202 end
203 end
203 end
204
204
205 end
205 end
@@ -1,208 +1,212
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::GroupsTest < ActionController::IntegrationTest
20 class ApiTest::GroupsTest < ActionController::IntegrationTest
21 fixtures :users, :groups_users
21 fixtures :users, :groups_users
22
22
23 def setup
23 def setup
24 Setting.rest_api_enabled = '1'
24 Setting.rest_api_enabled = '1'
25 end
25 end
26
26
27 context "GET /groups" do
27 context "GET /groups" do
28 context ".xml" do
28 context ".xml" do
29 should "require authentication" do
29 should "require authentication" do
30 get '/groups.xml'
30 get '/groups.xml'
31 assert_response 401
31 assert_response 401
32 end
32 end
33
33
34 should "return groups" do
34 should "return groups" do
35 get '/groups.xml', {}, credentials('admin')
35 get '/groups.xml', {}, credentials('admin')
36 assert_response :success
36 assert_response :success
37 assert_equal 'application/xml', response.content_type
37 assert_equal 'application/xml', response.content_type
38
38
39 assert_select 'groups' do
39 assert_select 'groups' do
40 assert_select 'group' do
40 assert_select 'group' do
41 assert_select 'name', :text => 'A Team'
41 assert_select 'name', :text => 'A Team'
42 assert_select 'id', :text => '10'
42 assert_select 'id', :text => '10'
43 end
43 end
44 end
44 end
45 end
45 end
46 end
46 end
47
47
48 context ".json" do
48 context ".json" do
49 should "require authentication" do
49 should "require authentication" do
50 get '/groups.json'
50 get '/groups.json'
51 assert_response 401
51 assert_response 401
52 end
52 end
53
53
54 should "return groups" do
54 should "return groups" do
55 get '/groups.json', {}, credentials('admin')
55 get '/groups.json', {}, credentials('admin')
56 assert_response :success
56 assert_response :success
57 assert_equal 'application/json', response.content_type
57 assert_equal 'application/json', response.content_type
58
58
59 json = MultiJson.load(response.body)
59 json = MultiJson.load(response.body)
60 groups = json['groups']
60 groups = json['groups']
61 assert_kind_of Array, groups
61 assert_kind_of Array, groups
62 group = groups.detect {|g| g['name'] == 'A Team'}
62 group = groups.detect {|g| g['name'] == 'A Team'}
63 assert_not_nil group
63 assert_not_nil group
64 assert_equal({'id' => 10, 'name' => 'A Team'}, group)
64 assert_equal({'id' => 10, 'name' => 'A Team'}, group)
65 end
65 end
66 end
66 end
67 end
67 end
68
68
69 context "GET /groups/:id" do
69 context "GET /groups/:id" do
70 context ".xml" do
70 context ".xml" do
71 should "return the group with its users" do
71 should "return the group with its users" do
72 get '/groups/10.xml', {}, credentials('admin')
72 get '/groups/10.xml', {}, credentials('admin')
73 assert_response :success
73 assert_response :success
74 assert_equal 'application/xml', response.content_type
74 assert_equal 'application/xml', response.content_type
75
75
76 assert_select 'group' do
76 assert_select 'group' do
77 assert_select 'name', :text => 'A Team'
77 assert_select 'name', :text => 'A Team'
78 assert_select 'id', :text => '10'
78 assert_select 'id', :text => '10'
79 end
79 end
80 end
80 end
81
81
82 should "include users if requested" do
82 should "include users if requested" do
83 get '/groups/10.xml?include=users', {}, credentials('admin')
83 get '/groups/10.xml?include=users', {}, credentials('admin')
84 assert_response :success
84 assert_response :success
85 assert_equal 'application/xml', response.content_type
85 assert_equal 'application/xml', response.content_type
86
86
87 assert_select 'group' do
87 assert_select 'group' do
88 assert_select 'users' do
88 assert_select 'users' do
89 assert_select 'user', Group.find(10).users.count
89 assert_select 'user', Group.find(10).users.count
90 assert_select 'user[id=8]'
90 assert_select 'user[id=8]'
91 end
91 end
92 end
92 end
93 end
93 end
94
94
95 should "include memberships if requested" do
95 should "include memberships if requested" do
96 get '/groups/10.xml?include=memberships', {}, credentials('admin')
96 get '/groups/10.xml?include=memberships', {}, credentials('admin')
97 assert_response :success
97 assert_response :success
98 assert_equal 'application/xml', response.content_type
98 assert_equal 'application/xml', response.content_type
99
99
100 assert_select 'group' do
100 assert_select 'group' do
101 assert_select 'memberships'
101 assert_select 'memberships'
102 end
102 end
103 end
103 end
104 end
104 end
105 end
105 end
106
106
107 context "POST /groups" do
107 context "POST /groups" do
108 context "with valid parameters" do
108 context "with valid parameters" do
109 context ".xml" do
109 context ".xml" do
110 should "create groups" do
110 should "create groups" do
111 assert_difference('Group.count') do
111 assert_difference('Group.count') do
112 post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin')
112 post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin')
113 assert_response :created
113 assert_response :created
114 assert_equal 'application/xml', response.content_type
114 assert_equal 'application/xml', response.content_type
115 end
115 end
116
116
117 group = Group.order('id DESC').first
117 group = Group.order('id DESC').first
118 assert_equal 'Test', group.name
118 assert_equal 'Test', group.name
119 assert_equal [2, 3], group.users.map(&:id).sort
119 assert_equal [2, 3], group.users.map(&:id).sort
120
120
121 assert_select 'group' do
121 assert_select 'group' do
122 assert_select 'name', :text => 'Test'
122 assert_select 'name', :text => 'Test'
123 end
123 end
124 end
124 end
125 end
125 end
126 end
126 end
127
127
128 context "with invalid parameters" do
128 context "with invalid parameters" do
129 context ".xml" do
129 context ".xml" do
130 should "return errors" do
130 should "return errors" do
131 assert_no_difference('Group.count') do
131 assert_no_difference('Group.count') do
132 post '/groups.xml', {:group => {:name => ''}}, credentials('admin')
132 post '/groups.xml', {:group => {:name => ''}}, credentials('admin')
133 end
133 end
134 assert_response :unprocessable_entity
134 assert_response :unprocessable_entity
135 assert_equal 'application/xml', response.content_type
135 assert_equal 'application/xml', response.content_type
136
136
137 assert_select 'errors' do
137 assert_select 'errors' do
138 assert_select 'error', :text => /Name can't be blank/
138 assert_select 'error', :text => /Name can't be blank/
139 end
139 end
140 end
140 end
141 end
141 end
142 end
142 end
143 end
143 end
144
144
145 context "PUT /groups/:id" do
145 context "PUT /groups/:id" do
146 context "with valid parameters" do
146 context "with valid parameters" do
147 context ".xml" do
147 context ".xml" do
148 should "update the group" do
148 should "update the group" do
149 put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin')
149 put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin')
150 assert_response :ok
150 assert_response :ok
151 assert_equal '', @response.body
151
152
152 group = Group.find(10)
153 group = Group.find(10)
153 assert_equal 'New name', group.name
154 assert_equal 'New name', group.name
154 assert_equal [2, 3], group.users.map(&:id).sort
155 assert_equal [2, 3], group.users.map(&:id).sort
155 end
156 end
156 end
157 end
157 end
158 end
158
159
159 context "with invalid parameters" do
160 context "with invalid parameters" do
160 context ".xml" do
161 context ".xml" do
161 should "return errors" do
162 should "return errors" do
162 put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin')
163 put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin')
163 assert_response :unprocessable_entity
164 assert_response :unprocessable_entity
164 assert_equal 'application/xml', response.content_type
165 assert_equal 'application/xml', response.content_type
165
166
166 assert_select 'errors' do
167 assert_select 'errors' do
167 assert_select 'error', :text => /Name can't be blank/
168 assert_select 'error', :text => /Name can't be blank/
168 end
169 end
169 end
170 end
170 end
171 end
171 end
172 end
172 end
173 end
173
174
174 context "DELETE /groups/:id" do
175 context "DELETE /groups/:id" do
175 context ".xml" do
176 context ".xml" do
176 should "delete the group" do
177 should "delete the group" do
177 assert_difference 'Group.count', -1 do
178 assert_difference 'Group.count', -1 do
178 delete '/groups/10.xml', {}, credentials('admin')
179 delete '/groups/10.xml', {}, credentials('admin')
179 assert_response :ok
180 assert_response :ok
181 assert_equal '', @response.body
180 end
182 end
181 end
183 end
182 end
184 end
183 end
185 end
184
186
185 context "POST /groups/:id/users" do
187 context "POST /groups/:id/users" do
186 context ".xml" do
188 context ".xml" do
187 should "add user to the group" do
189 should "add user to the group" do
188 assert_difference 'Group.find(10).users.count' do
190 assert_difference 'Group.find(10).users.count' do
189 post '/groups/10/users.xml', {:user_id => 5}, credentials('admin')
191 post '/groups/10/users.xml', {:user_id => 5}, credentials('admin')
190 assert_response :ok
192 assert_response :ok
193 assert_equal '', @response.body
191 end
194 end
192 assert_include User.find(5), Group.find(10).users
195 assert_include User.find(5), Group.find(10).users
193 end
196 end
194 end
197 end
195 end
198 end
196
199
197 context "DELETE /groups/:id/users/:user_id" do
200 context "DELETE /groups/:id/users/:user_id" do
198 context ".xml" do
201 context ".xml" do
199 should "remove user from the group" do
202 should "remove user from the group" do
200 assert_difference 'Group.find(10).users.count', -1 do
203 assert_difference 'Group.find(10).users.count', -1 do
201 delete '/groups/10/users/8.xml', {}, credentials('admin')
204 delete '/groups/10/users/8.xml', {}, credentials('admin')
202 assert_response :ok
205 assert_response :ok
206 assert_equal '', @response.body
203 end
207 end
204 assert_not_include User.find(8), Group.find(10).users
208 assert_not_include User.find(8), Group.find(10).users
205 end
209 end
206 end
210 end
207 end
211 end
208 end
212 end
@@ -1,123 +1,126
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::IssueCategoriesTest < ActionController::IntegrationTest
20 class ApiTest::IssueCategoriesTest < ActionController::IntegrationTest
21 fixtures :projects, :users, :issue_categories, :issues,
21 fixtures :projects, :users, :issue_categories, :issues,
22 :roles,
22 :roles,
23 :member_roles,
23 :member_roles,
24 :members,
24 :members,
25 :enabled_modules
25 :enabled_modules
26
26
27 def setup
27 def setup
28 Setting.rest_api_enabled = '1'
28 Setting.rest_api_enabled = '1'
29 end
29 end
30
30
31 context "GET /projects/:project_id/issue_categories.xml" do
31 context "GET /projects/:project_id/issue_categories.xml" do
32 should "return issue categories" do
32 should "return issue categories" do
33 get '/projects/1/issue_categories.xml', {}, credentials('jsmith')
33 get '/projects/1/issue_categories.xml', {}, credentials('jsmith')
34 assert_response :success
34 assert_response :success
35 assert_equal 'application/xml', @response.content_type
35 assert_equal 'application/xml', @response.content_type
36 assert_tag :tag => 'issue_categories',
36 assert_tag :tag => 'issue_categories',
37 :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}}
37 :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}}
38 end
38 end
39 end
39 end
40
40
41 context "GET /issue_categories/2.xml" do
41 context "GET /issue_categories/2.xml" do
42 should "return requested issue category" do
42 should "return requested issue category" do
43 get '/issue_categories/2.xml', {}, credentials('jsmith')
43 get '/issue_categories/2.xml', {}, credentials('jsmith')
44 assert_response :success
44 assert_response :success
45 assert_equal 'application/xml', @response.content_type
45 assert_equal 'application/xml', @response.content_type
46 assert_tag :tag => 'issue_category',
46 assert_tag :tag => 'issue_category',
47 :child => {:tag => 'id', :content => '2'}
47 :child => {:tag => 'id', :content => '2'}
48 end
48 end
49 end
49 end
50
50
51 context "POST /projects/:project_id/issue_categories.xml" do
51 context "POST /projects/:project_id/issue_categories.xml" do
52 should "return create issue category" do
52 should "return create issue category" do
53 assert_difference 'IssueCategory.count' do
53 assert_difference 'IssueCategory.count' do
54 post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith')
54 post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith')
55 end
55 end
56 assert_response :created
56 assert_response :created
57 assert_equal 'application/xml', @response.content_type
57 assert_equal 'application/xml', @response.content_type
58
58
59 category = IssueCategory.first(:order => 'id DESC')
59 category = IssueCategory.first(:order => 'id DESC')
60 assert_equal 'API', category.name
60 assert_equal 'API', category.name
61 assert_equal 1, category.project_id
61 assert_equal 1, category.project_id
62 end
62 end
63
63
64 context "with invalid parameters" do
64 context "with invalid parameters" do
65 should "return errors" do
65 should "return errors" do
66 assert_no_difference 'IssueCategory.count' do
66 assert_no_difference 'IssueCategory.count' do
67 post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith')
67 post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith')
68 end
68 end
69 assert_response :unprocessable_entity
69 assert_response :unprocessable_entity
70 assert_equal 'application/xml', @response.content_type
70 assert_equal 'application/xml', @response.content_type
71
71
72 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
72 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
73 end
73 end
74 end
74 end
75 end
75 end
76
76
77 context "PUT /issue_categories/2.xml" do
77 context "PUT /issue_categories/2.xml" do
78 context "with valid parameters" do
78 context "with valid parameters" do
79 should "update issue category" do
79 should "update issue category" do
80 assert_no_difference 'IssueCategory.count' do
80 assert_no_difference 'IssueCategory.count' do
81 put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith')
81 put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith')
82 end
82 end
83 assert_response :ok
83 assert_response :ok
84 assert_equal '', @response.body
84 assert_equal 'API Update', IssueCategory.find(2).name
85 assert_equal 'API Update', IssueCategory.find(2).name
85 end
86 end
86 end
87 end
87
88
88 context "with invalid parameters" do
89 context "with invalid parameters" do
89 should "return errors" do
90 should "return errors" do
90 assert_no_difference 'IssueCategory.count' do
91 assert_no_difference 'IssueCategory.count' do
91 put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith')
92 put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith')
92 end
93 end
93 assert_response :unprocessable_entity
94 assert_response :unprocessable_entity
94 assert_equal 'application/xml', @response.content_type
95 assert_equal 'application/xml', @response.content_type
95
96
96 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
97 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
97 end
98 end
98 end
99 end
99 end
100 end
100
101
101 context "DELETE /issue_categories/1.xml" do
102 context "DELETE /issue_categories/1.xml" do
102 should "destroy issue categories" do
103 should "destroy issue categories" do
103 assert_difference 'IssueCategory.count', -1 do
104 assert_difference 'IssueCategory.count', -1 do
104 delete '/issue_categories/1.xml', {}, credentials('jsmith')
105 delete '/issue_categories/1.xml', {}, credentials('jsmith')
105 end
106 end
106 assert_response :ok
107 assert_response :ok
108 assert_equal '', @response.body
107 assert_nil IssueCategory.find_by_id(1)
109 assert_nil IssueCategory.find_by_id(1)
108 end
110 end
109
111
110 should "reassign issues with :reassign_to_id param" do
112 should "reassign issues with :reassign_to_id param" do
111 issue_count = Issue.count(:conditions => {:category_id => 1})
113 issue_count = Issue.count(:conditions => {:category_id => 1})
112 assert issue_count > 0
114 assert issue_count > 0
113
115
114 assert_difference 'IssueCategory.count', -1 do
116 assert_difference 'IssueCategory.count', -1 do
115 assert_difference 'Issue.count(:conditions => {:category_id => 2})', 3 do
117 assert_difference 'Issue.count(:conditions => {:category_id => 2})', 3 do
116 delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith')
118 delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith')
117 end
119 end
118 end
120 end
119 assert_response :ok
121 assert_response :ok
122 assert_equal '', @response.body
120 assert_nil IssueCategory.find_by_id(1)
123 assert_nil IssueCategory.find_by_id(1)
121 end
124 end
122 end
125 end
123 end
126 end
@@ -1,106 +1,107
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::IssueRelationsTest < ActionController::IntegrationTest
20 class ApiTest::IssueRelationsTest < ActionController::IntegrationTest
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users, :issue_categories,
22 :enumerations, :users, :issue_categories,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :workflows,
28 :workflows,
29 :issue_relations
29 :issue_relations
30
30
31 def setup
31 def setup
32 Setting.rest_api_enabled = '1'
32 Setting.rest_api_enabled = '1'
33 end
33 end
34
34
35 context "/issues/:issue_id/relations" do
35 context "/issues/:issue_id/relations" do
36 context "GET" do
36 context "GET" do
37 should "return issue relations" do
37 should "return issue relations" do
38 get '/issues/9/relations.xml', {}, credentials('jsmith')
38 get '/issues/9/relations.xml', {}, credentials('jsmith')
39
39
40 assert_response :success
40 assert_response :success
41 assert_equal 'application/xml', @response.content_type
41 assert_equal 'application/xml', @response.content_type
42
42
43 assert_tag :tag => 'relations',
43 assert_tag :tag => 'relations',
44 :attributes => { :type => 'array' },
44 :attributes => { :type => 'array' },
45 :child => {
45 :child => {
46 :tag => 'relation',
46 :tag => 'relation',
47 :child => {
47 :child => {
48 :tag => 'id',
48 :tag => 'id',
49 :content => '1'
49 :content => '1'
50 }
50 }
51 }
51 }
52 end
52 end
53 end
53 end
54
54
55 context "POST" do
55 context "POST" do
56 should "create a relation" do
56 should "create a relation" do
57 assert_difference('IssueRelation.count') do
57 assert_difference('IssueRelation.count') do
58 post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, credentials('jsmith')
58 post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, credentials('jsmith')
59 end
59 end
60
60
61 relation = IssueRelation.first(:order => 'id DESC')
61 relation = IssueRelation.first(:order => 'id DESC')
62 assert_equal 2, relation.issue_from_id
62 assert_equal 2, relation.issue_from_id
63 assert_equal 7, relation.issue_to_id
63 assert_equal 7, relation.issue_to_id
64 assert_equal 'relates', relation.relation_type
64 assert_equal 'relates', relation.relation_type
65
65
66 assert_response :created
66 assert_response :created
67 assert_equal 'application/xml', @response.content_type
67 assert_equal 'application/xml', @response.content_type
68 assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s}
68 assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s}
69 end
69 end
70
70
71 context "with failure" do
71 context "with failure" do
72 should "return the errors" do
72 should "return the errors" do
73 assert_no_difference('IssueRelation.count') do
73 assert_no_difference('IssueRelation.count') do
74 post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, credentials('jsmith')
74 post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, credentials('jsmith')
75 end
75 end
76
76
77 assert_response :unprocessable_entity
77 assert_response :unprocessable_entity
78 assert_tag :errors, :child => {:tag => 'error', :content => /relation_type is not included in the list/}
78 assert_tag :errors, :child => {:tag => 'error', :content => /relation_type is not included in the list/}
79 end
79 end
80 end
80 end
81 end
81 end
82 end
82 end
83
83
84 context "/relations/:id" do
84 context "/relations/:id" do
85 context "GET" do
85 context "GET" do
86 should "return the relation" do
86 should "return the relation" do
87 get '/relations/2.xml', {}, credentials('jsmith')
87 get '/relations/2.xml', {}, credentials('jsmith')
88
88
89 assert_response :success
89 assert_response :success
90 assert_equal 'application/xml', @response.content_type
90 assert_equal 'application/xml', @response.content_type
91 assert_tag 'relation', :child => {:tag => 'id', :content => '2'}
91 assert_tag 'relation', :child => {:tag => 'id', :content => '2'}
92 end
92 end
93 end
93 end
94
94
95 context "DELETE" do
95 context "DELETE" do
96 should "delete the relation" do
96 should "delete the relation" do
97 assert_difference('IssueRelation.count', -1) do
97 assert_difference('IssueRelation.count', -1) do
98 delete '/relations/2.xml', {}, credentials('jsmith')
98 delete '/relations/2.xml', {}, credentials('jsmith')
99 end
99 end
100
100
101 assert_response :ok
101 assert_response :ok
102 assert_equal '', @response.body
102 assert_nil IssueRelation.find_by_id(2)
103 assert_nil IssueRelation.find_by_id(2)
103 end
104 end
104 end
105 end
105 end
106 end
106 end
107 end
@@ -1,778 +1,779
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class 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 :attachments
44 :attachments
45
45
46 def setup
46 def setup
47 Setting.rest_api_enabled = '1'
47 Setting.rest_api_enabled = '1'
48 end
48 end
49
49
50 context "/issues" do
50 context "/issues" do
51 # Use a private project to make sure auth is really working and not just
51 # Use a private project to make sure auth is really working and not just
52 # only showing public issues.
52 # only showing public issues.
53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
54
54
55 should "contain metadata" do
55 should "contain metadata" do
56 get '/issues.xml'
56 get '/issues.xml'
57
57
58 assert_tag :tag => 'issues',
58 assert_tag :tag => 'issues',
59 :attributes => {
59 :attributes => {
60 :type => 'array',
60 :type => 'array',
61 :total_count => assigns(:issue_count),
61 :total_count => assigns(:issue_count),
62 :limit => 25,
62 :limit => 25,
63 :offset => 0
63 :offset => 0
64 }
64 }
65 end
65 end
66
66
67 context "with offset and limit" do
67 context "with offset and limit" do
68 should "use the params" do
68 should "use the params" do
69 get '/issues.xml?offset=2&limit=3'
69 get '/issues.xml?offset=2&limit=3'
70
70
71 assert_equal 3, assigns(:limit)
71 assert_equal 3, assigns(:limit)
72 assert_equal 2, assigns(:offset)
72 assert_equal 2, assigns(:offset)
73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
73 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
74 end
74 end
75 end
75 end
76
76
77 context "with nometa param" do
77 context "with nometa param" do
78 should "not contain metadata" do
78 should "not contain metadata" do
79 get '/issues.xml?nometa=1'
79 get '/issues.xml?nometa=1'
80
80
81 assert_tag :tag => 'issues',
81 assert_tag :tag => 'issues',
82 :attributes => {
82 :attributes => {
83 :type => 'array',
83 :type => 'array',
84 :total_count => nil,
84 :total_count => nil,
85 :limit => nil,
85 :limit => nil,
86 :offset => nil
86 :offset => nil
87 }
87 }
88 end
88 end
89 end
89 end
90
90
91 context "with nometa header" do
91 context "with nometa header" do
92 should "not contain metadata" do
92 should "not contain metadata" do
93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
93 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
94
94
95 assert_tag :tag => 'issues',
95 assert_tag :tag => 'issues',
96 :attributes => {
96 :attributes => {
97 :type => 'array',
97 :type => 'array',
98 :total_count => nil,
98 :total_count => nil,
99 :limit => nil,
99 :limit => nil,
100 :offset => nil
100 :offset => nil
101 }
101 }
102 end
102 end
103 end
103 end
104
104
105 context "with relations" do
105 context "with relations" do
106 should "display relations" do
106 should "display relations" do
107 get '/issues.xml?include=relations'
107 get '/issues.xml?include=relations'
108
108
109 assert_response :success
109 assert_response :success
110 assert_equal 'application/xml', @response.content_type
110 assert_equal 'application/xml', @response.content_type
111 assert_tag 'relations',
111 assert_tag 'relations',
112 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}},
112 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}},
113 :children => {:count => 1},
113 :children => {:count => 1},
114 :child => {
114 :child => {
115 :tag => 'relation',
115 :tag => 'relation',
116 :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', :relation_type => 'relates'}
116 :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', :relation_type => 'relates'}
117 }
117 }
118 assert_tag 'relations',
118 assert_tag 'relations',
119 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}},
119 :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}},
120 :children => {:count => 0}
120 :children => {:count => 0}
121 end
121 end
122 end
122 end
123
123
124 context "with invalid query params" do
124 context "with invalid query params" do
125 should "return errors" do
125 should "return errors" do
126 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
126 get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}}
127
127
128 assert_response :unprocessable_entity
128 assert_response :unprocessable_entity
129 assert_equal 'application/xml', @response.content_type
129 assert_equal 'application/xml', @response.content_type
130 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
130 assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"}
131 end
131 end
132 end
132 end
133
133
134 context "with custom field filter" do
134 context "with custom field filter" do
135 should "show only issues with the custom field value" do
135 should "show only issues with the custom field value" do
136 get '/issues.xml', { :set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
136 get '/issues.xml', { :set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, :v => {:cf_1 => ['MySQL']}}
137
137
138 expected_ids = Issue.visible.all(
138 expected_ids = Issue.visible.all(
139 :include => :custom_values,
139 :include => :custom_values,
140 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
140 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
141
141
142 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
142 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
143 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
143 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
144 end
144 end
145 end
145 end
146 end
146 end
147
147
148 context "with custom field filter (shorthand method)" do
148 context "with custom field filter (shorthand method)" do
149 should "show only issues with the custom field value" do
149 should "show only issues with the custom field value" do
150 get '/issues.xml', { :cf_1 => 'MySQL' }
150 get '/issues.xml', { :cf_1 => 'MySQL' }
151
151
152 expected_ids = Issue.visible.all(
152 expected_ids = Issue.visible.all(
153 :include => :custom_values,
153 :include => :custom_values,
154 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
154 :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id)
155
155
156 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
156 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
157 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
157 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
158 end
158 end
159 end
159 end
160 end
160 end
161 end
161 end
162
162
163 context "/index.json" do
163 context "/index.json" do
164 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
164 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
165 end
165 end
166
166
167 context "/index.xml with filter" do
167 context "/index.xml with filter" do
168 should "show only issues with the status_id" do
168 should "show only issues with the status_id" do
169 get '/issues.xml?status_id=5'
169 get '/issues.xml?status_id=5'
170
170
171 expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id)
171 expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id)
172
172
173 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
173 assert_select 'issues > issue > id', :count => expected_ids.count do |ids|
174 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
174 ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) }
175 end
175 end
176 end
176 end
177 end
177 end
178
178
179 context "/index.json with filter" do
179 context "/index.json with filter" do
180 should "show only issues with the status_id" do
180 should "show only issues with the status_id" do
181 get '/issues.json?status_id=5'
181 get '/issues.json?status_id=5'
182
182
183 json = ActiveSupport::JSON.decode(response.body)
183 json = ActiveSupport::JSON.decode(response.body)
184 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
184 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
185 assert_equal 3, status_ids_used.length
185 assert_equal 3, status_ids_used.length
186 assert status_ids_used.all? {|id| id == 5 }
186 assert status_ids_used.all? {|id| id == 5 }
187 end
187 end
188
188
189 end
189 end
190
190
191 # Issue 6 is on a private project
191 # Issue 6 is on a private project
192 context "/issues/6.xml" do
192 context "/issues/6.xml" do
193 should_allow_api_authentication(:get, "/issues/6.xml")
193 should_allow_api_authentication(:get, "/issues/6.xml")
194 end
194 end
195
195
196 context "/issues/6.json" do
196 context "/issues/6.json" do
197 should_allow_api_authentication(:get, "/issues/6.json")
197 should_allow_api_authentication(:get, "/issues/6.json")
198 end
198 end
199
199
200 context "GET /issues/:id" do
200 context "GET /issues/:id" do
201 context "with journals" do
201 context "with journals" do
202 context ".xml" do
202 context ".xml" do
203 should "display journals" do
203 should "display journals" do
204 get '/issues/1.xml?include=journals'
204 get '/issues/1.xml?include=journals'
205
205
206 assert_tag :tag => 'issue',
206 assert_tag :tag => 'issue',
207 :child => {
207 :child => {
208 :tag => 'journals',
208 :tag => 'journals',
209 :attributes => { :type => 'array' },
209 :attributes => { :type => 'array' },
210 :child => {
210 :child => {
211 :tag => 'journal',
211 :tag => 'journal',
212 :attributes => { :id => '1'},
212 :attributes => { :id => '1'},
213 :child => {
213 :child => {
214 :tag => 'details',
214 :tag => 'details',
215 :attributes => { :type => 'array' },
215 :attributes => { :type => 'array' },
216 :child => {
216 :child => {
217 :tag => 'detail',
217 :tag => 'detail',
218 :attributes => { :name => 'status_id' },
218 :attributes => { :name => 'status_id' },
219 :child => {
219 :child => {
220 :tag => 'old_value',
220 :tag => 'old_value',
221 :content => '1',
221 :content => '1',
222 :sibling => {
222 :sibling => {
223 :tag => 'new_value',
223 :tag => 'new_value',
224 :content => '2'
224 :content => '2'
225 }
225 }
226 }
226 }
227 }
227 }
228 }
228 }
229 }
229 }
230 }
230 }
231 end
231 end
232 end
232 end
233 end
233 end
234
234
235 context "with custom fields" do
235 context "with custom fields" do
236 context ".xml" do
236 context ".xml" do
237 should "display custom fields" do
237 should "display custom fields" do
238 get '/issues/3.xml'
238 get '/issues/3.xml'
239
239
240 assert_tag :tag => 'issue',
240 assert_tag :tag => 'issue',
241 :child => {
241 :child => {
242 :tag => 'custom_fields',
242 :tag => 'custom_fields',
243 :attributes => { :type => 'array' },
243 :attributes => { :type => 'array' },
244 :child => {
244 :child => {
245 :tag => 'custom_field',
245 :tag => 'custom_field',
246 :attributes => { :id => '1'},
246 :attributes => { :id => '1'},
247 :child => {
247 :child => {
248 :tag => 'value',
248 :tag => 'value',
249 :content => 'MySQL'
249 :content => 'MySQL'
250 }
250 }
251 }
251 }
252 }
252 }
253
253
254 assert_nothing_raised do
254 assert_nothing_raised do
255 Hash.from_xml(response.body).to_xml
255 Hash.from_xml(response.body).to_xml
256 end
256 end
257 end
257 end
258 end
258 end
259 end
259 end
260
260
261 context "with multi custom fields" do
261 context "with multi custom fields" do
262 setup do
262 setup do
263 field = CustomField.find(1)
263 field = CustomField.find(1)
264 field.update_attribute :multiple, true
264 field.update_attribute :multiple, true
265 issue = Issue.find(3)
265 issue = Issue.find(3)
266 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
266 issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
267 issue.save!
267 issue.save!
268 end
268 end
269
269
270 context ".xml" do
270 context ".xml" do
271 should "display custom fields" do
271 should "display custom fields" do
272 get '/issues/3.xml'
272 get '/issues/3.xml'
273 assert_response :success
273 assert_response :success
274 assert_tag :tag => 'issue',
274 assert_tag :tag => 'issue',
275 :child => {
275 :child => {
276 :tag => 'custom_fields',
276 :tag => 'custom_fields',
277 :attributes => { :type => 'array' },
277 :attributes => { :type => 'array' },
278 :child => {
278 :child => {
279 :tag => 'custom_field',
279 :tag => 'custom_field',
280 :attributes => { :id => '1'},
280 :attributes => { :id => '1'},
281 :child => {
281 :child => {
282 :tag => 'value',
282 :tag => 'value',
283 :attributes => { :type => 'array' },
283 :attributes => { :type => 'array' },
284 :children => { :count => 2 }
284 :children => { :count => 2 }
285 }
285 }
286 }
286 }
287 }
287 }
288
288
289 xml = Hash.from_xml(response.body)
289 xml = Hash.from_xml(response.body)
290 custom_fields = xml['issue']['custom_fields']
290 custom_fields = xml['issue']['custom_fields']
291 assert_kind_of Array, custom_fields
291 assert_kind_of Array, custom_fields
292 field = custom_fields.detect {|f| f['id'] == '1'}
292 field = custom_fields.detect {|f| f['id'] == '1'}
293 assert_kind_of Hash, field
293 assert_kind_of Hash, field
294 assert_equal ['MySQL', 'Oracle'], field['value'].sort
294 assert_equal ['MySQL', 'Oracle'], field['value'].sort
295 end
295 end
296 end
296 end
297
297
298 context ".json" do
298 context ".json" do
299 should "display custom fields" do
299 should "display custom fields" do
300 get '/issues/3.json'
300 get '/issues/3.json'
301 assert_response :success
301 assert_response :success
302 json = ActiveSupport::JSON.decode(response.body)
302 json = ActiveSupport::JSON.decode(response.body)
303 custom_fields = json['issue']['custom_fields']
303 custom_fields = json['issue']['custom_fields']
304 assert_kind_of Array, custom_fields
304 assert_kind_of Array, custom_fields
305 field = custom_fields.detect {|f| f['id'] == 1}
305 field = custom_fields.detect {|f| f['id'] == 1}
306 assert_kind_of Hash, field
306 assert_kind_of Hash, field
307 assert_equal ['MySQL', 'Oracle'], field['value'].sort
307 assert_equal ['MySQL', 'Oracle'], field['value'].sort
308 end
308 end
309 end
309 end
310 end
310 end
311
311
312 context "with empty value for multi custom field" do
312 context "with empty value for multi custom field" do
313 setup do
313 setup do
314 field = CustomField.find(1)
314 field = CustomField.find(1)
315 field.update_attribute :multiple, true
315 field.update_attribute :multiple, true
316 issue = Issue.find(3)
316 issue = Issue.find(3)
317 issue.custom_field_values = {1 => ['']}
317 issue.custom_field_values = {1 => ['']}
318 issue.save!
318 issue.save!
319 end
319 end
320
320
321 context ".xml" do
321 context ".xml" do
322 should "display custom fields" do
322 should "display custom fields" do
323 get '/issues/3.xml'
323 get '/issues/3.xml'
324 assert_response :success
324 assert_response :success
325 assert_tag :tag => 'issue',
325 assert_tag :tag => 'issue',
326 :child => {
326 :child => {
327 :tag => 'custom_fields',
327 :tag => 'custom_fields',
328 :attributes => { :type => 'array' },
328 :attributes => { :type => 'array' },
329 :child => {
329 :child => {
330 :tag => 'custom_field',
330 :tag => 'custom_field',
331 :attributes => { :id => '1'},
331 :attributes => { :id => '1'},
332 :child => {
332 :child => {
333 :tag => 'value',
333 :tag => 'value',
334 :attributes => { :type => 'array' },
334 :attributes => { :type => 'array' },
335 :children => { :count => 0 }
335 :children => { :count => 0 }
336 }
336 }
337 }
337 }
338 }
338 }
339
339
340 xml = Hash.from_xml(response.body)
340 xml = Hash.from_xml(response.body)
341 custom_fields = xml['issue']['custom_fields']
341 custom_fields = xml['issue']['custom_fields']
342 assert_kind_of Array, custom_fields
342 assert_kind_of Array, custom_fields
343 field = custom_fields.detect {|f| f['id'] == '1'}
343 field = custom_fields.detect {|f| f['id'] == '1'}
344 assert_kind_of Hash, field
344 assert_kind_of Hash, field
345 assert_equal [], field['value']
345 assert_equal [], field['value']
346 end
346 end
347 end
347 end
348
348
349 context ".json" do
349 context ".json" do
350 should "display custom fields" do
350 should "display custom fields" do
351 get '/issues/3.json'
351 get '/issues/3.json'
352 assert_response :success
352 assert_response :success
353 json = ActiveSupport::JSON.decode(response.body)
353 json = ActiveSupport::JSON.decode(response.body)
354 custom_fields = json['issue']['custom_fields']
354 custom_fields = json['issue']['custom_fields']
355 assert_kind_of Array, custom_fields
355 assert_kind_of Array, custom_fields
356 field = custom_fields.detect {|f| f['id'] == 1}
356 field = custom_fields.detect {|f| f['id'] == 1}
357 assert_kind_of Hash, field
357 assert_kind_of Hash, field
358 assert_equal [], field['value'].sort
358 assert_equal [], field['value'].sort
359 end
359 end
360 end
360 end
361 end
361 end
362
362
363 context "with attachments" do
363 context "with attachments" do
364 context ".xml" do
364 context ".xml" do
365 should "display attachments" do
365 should "display attachments" do
366 get '/issues/3.xml?include=attachments'
366 get '/issues/3.xml?include=attachments'
367
367
368 assert_tag :tag => 'issue',
368 assert_tag :tag => 'issue',
369 :child => {
369 :child => {
370 :tag => 'attachments',
370 :tag => 'attachments',
371 :children => {:count => 5},
371 :children => {:count => 5},
372 :child => {
372 :child => {
373 :tag => 'attachment',
373 :tag => 'attachment',
374 :child => {
374 :child => {
375 :tag => 'filename',
375 :tag => 'filename',
376 :content => 'source.rb',
376 :content => 'source.rb',
377 :sibling => {
377 :sibling => {
378 :tag => 'content_url',
378 :tag => 'content_url',
379 :content => 'http://www.example.com/attachments/download/4/source.rb'
379 :content => 'http://www.example.com/attachments/download/4/source.rb'
380 }
380 }
381 }
381 }
382 }
382 }
383 }
383 }
384 end
384 end
385 end
385 end
386 end
386 end
387
387
388 context "with subtasks" do
388 context "with subtasks" do
389 setup do
389 setup do
390 @c1 = Issue.create!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => 1)
390 @c1 = Issue.create!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => 1)
391 @c2 = Issue.create!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => 1)
391 @c2 = Issue.create!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => 1)
392 @c3 = Issue.create!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => @c1.id)
392 @c3 = Issue.create!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :author_id => 1, :parent_issue_id => @c1.id)
393 end
393 end
394
394
395 context ".xml" do
395 context ".xml" do
396 should "display children" do
396 should "display children" do
397 get '/issues/1.xml?include=children'
397 get '/issues/1.xml?include=children'
398
398
399 assert_tag :tag => 'issue',
399 assert_tag :tag => 'issue',
400 :child => {
400 :child => {
401 :tag => 'children',
401 :tag => 'children',
402 :children => {:count => 2},
402 :children => {:count => 2},
403 :child => {
403 :child => {
404 :tag => 'issue',
404 :tag => 'issue',
405 :attributes => {:id => @c1.id.to_s},
405 :attributes => {:id => @c1.id.to_s},
406 :child => {
406 :child => {
407 :tag => 'subject',
407 :tag => 'subject',
408 :content => 'child c1',
408 :content => 'child c1',
409 :sibling => {
409 :sibling => {
410 :tag => 'children',
410 :tag => 'children',
411 :children => {:count => 1},
411 :children => {:count => 1},
412 :child => {
412 :child => {
413 :tag => 'issue',
413 :tag => 'issue',
414 :attributes => {:id => @c3.id.to_s}
414 :attributes => {:id => @c3.id.to_s}
415 }
415 }
416 }
416 }
417 }
417 }
418 }
418 }
419 }
419 }
420 end
420 end
421
421
422 context ".json" do
422 context ".json" do
423 should "display children" do
423 should "display children" do
424 get '/issues/1.json?include=children'
424 get '/issues/1.json?include=children'
425
425
426 json = ActiveSupport::JSON.decode(response.body)
426 json = ActiveSupport::JSON.decode(response.body)
427 assert_equal([
427 assert_equal([
428 {
428 {
429 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
429 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
430 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
430 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
431 },
431 },
432 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
432 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
433 ],
433 ],
434 json['issue']['children'])
434 json['issue']['children'])
435 end
435 end
436 end
436 end
437 end
437 end
438 end
438 end
439 end
439 end
440
440
441 context "POST /issues.xml" do
441 context "POST /issues.xml" do
442 should_allow_api_authentication(:post,
442 should_allow_api_authentication(:post,
443 '/issues.xml',
443 '/issues.xml',
444 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
444 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
445 {:success_code => :created})
445 {:success_code => :created})
446
446
447 should "create an issue with the attributes" do
447 should "create an issue with the attributes" do
448 assert_difference('Issue.count') do
448 assert_difference('Issue.count') do
449 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
449 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
450 end
450 end
451
451
452 issue = Issue.first(:order => 'id DESC')
452 issue = Issue.first(:order => 'id DESC')
453 assert_equal 1, issue.project_id
453 assert_equal 1, issue.project_id
454 assert_equal 2, issue.tracker_id
454 assert_equal 2, issue.tracker_id
455 assert_equal 3, issue.status_id
455 assert_equal 3, issue.status_id
456 assert_equal 'API test', issue.subject
456 assert_equal 'API test', issue.subject
457
457
458 assert_response :created
458 assert_response :created
459 assert_equal 'application/xml', @response.content_type
459 assert_equal 'application/xml', @response.content_type
460 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
460 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
461 end
461 end
462 end
462 end
463
463
464 context "POST /issues.xml with failure" do
464 context "POST /issues.xml with failure" do
465 should "have an errors tag" do
465 should "have an errors tag" do
466 assert_no_difference('Issue.count') do
466 assert_no_difference('Issue.count') do
467 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
467 post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith')
468 end
468 end
469
469
470 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
470 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
471 end
471 end
472 end
472 end
473
473
474 context "POST /issues.json" do
474 context "POST /issues.json" do
475 should_allow_api_authentication(:post,
475 should_allow_api_authentication(:post,
476 '/issues.json',
476 '/issues.json',
477 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
477 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
478 {:success_code => :created})
478 {:success_code => :created})
479
479
480 should "create an issue with the attributes" do
480 should "create an issue with the attributes" do
481 assert_difference('Issue.count') do
481 assert_difference('Issue.count') do
482 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
482 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, credentials('jsmith')
483 end
483 end
484
484
485 issue = Issue.first(:order => 'id DESC')
485 issue = Issue.first(:order => 'id DESC')
486 assert_equal 1, issue.project_id
486 assert_equal 1, issue.project_id
487 assert_equal 2, issue.tracker_id
487 assert_equal 2, issue.tracker_id
488 assert_equal 3, issue.status_id
488 assert_equal 3, issue.status_id
489 assert_equal 'API test', issue.subject
489 assert_equal 'API test', issue.subject
490 end
490 end
491
491
492 end
492 end
493
493
494 context "POST /issues.json with failure" do
494 context "POST /issues.json with failure" do
495 should "have an errors element" do
495 should "have an errors element" do
496 assert_no_difference('Issue.count') do
496 assert_no_difference('Issue.count') do
497 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
497 post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith')
498 end
498 end
499
499
500 json = ActiveSupport::JSON.decode(response.body)
500 json = ActiveSupport::JSON.decode(response.body)
501 assert json['errors'].include?("Subject can't be blank")
501 assert json['errors'].include?("Subject can't be blank")
502 end
502 end
503 end
503 end
504
504
505 # Issue 6 is on a private project
505 # Issue 6 is on a private project
506 context "PUT /issues/6.xml" do
506 context "PUT /issues/6.xml" do
507 setup do
507 setup do
508 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
508 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
509 end
509 end
510
510
511 should_allow_api_authentication(:put,
511 should_allow_api_authentication(:put,
512 '/issues/6.xml',
512 '/issues/6.xml',
513 {:issue => {:subject => 'API update', :notes => 'A new note'}},
513 {:issue => {:subject => 'API update', :notes => 'A new note'}},
514 {:success_code => :ok})
514 {:success_code => :ok})
515
515
516 should "not create a new issue" do
516 should "not create a new issue" do
517 assert_no_difference('Issue.count') do
517 assert_no_difference('Issue.count') do
518 put '/issues/6.xml', @parameters, credentials('jsmith')
518 put '/issues/6.xml', @parameters, credentials('jsmith')
519 end
519 end
520 end
520 end
521
521
522 should "create a new journal" do
522 should "create a new journal" do
523 assert_difference('Journal.count') do
523 assert_difference('Journal.count') do
524 put '/issues/6.xml', @parameters, credentials('jsmith')
524 put '/issues/6.xml', @parameters, credentials('jsmith')
525 end
525 end
526 end
526 end
527
527
528 should "add the note to the journal" do
528 should "add the note to the journal" do
529 put '/issues/6.xml', @parameters, credentials('jsmith')
529 put '/issues/6.xml', @parameters, credentials('jsmith')
530
530
531 journal = Journal.last
531 journal = Journal.last
532 assert_equal "A new note", journal.notes
532 assert_equal "A new note", journal.notes
533 end
533 end
534
534
535 should "update the issue" do
535 should "update the issue" do
536 put '/issues/6.xml', @parameters, credentials('jsmith')
536 put '/issues/6.xml', @parameters, credentials('jsmith')
537
537
538 issue = Issue.find(6)
538 issue = Issue.find(6)
539 assert_equal "API update", issue.subject
539 assert_equal "API update", issue.subject
540 end
540 end
541
541
542 end
542 end
543
543
544 context "PUT /issues/3.xml with custom fields" do
544 context "PUT /issues/3.xml with custom fields" do
545 setup do
545 setup do
546 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
546 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
547 end
547 end
548
548
549 should "update custom fields" do
549 should "update custom fields" do
550 assert_no_difference('Issue.count') do
550 assert_no_difference('Issue.count') do
551 put '/issues/3.xml', @parameters, credentials('jsmith')
551 put '/issues/3.xml', @parameters, credentials('jsmith')
552 end
552 end
553
553
554 issue = Issue.find(3)
554 issue = Issue.find(3)
555 assert_equal '150', issue.custom_value_for(2).value
555 assert_equal '150', issue.custom_value_for(2).value
556 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
556 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
557 end
557 end
558 end
558 end
559
559
560 context "PUT /issues/3.xml with multi custom fields" do
560 context "PUT /issues/3.xml with multi custom fields" do
561 setup do
561 setup do
562 field = CustomField.find(1)
562 field = CustomField.find(1)
563 field.update_attribute :multiple, true
563 field.update_attribute :multiple, true
564 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, {'id' => '2', 'value' => '150'}]}}
564 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, {'id' => '2', 'value' => '150'}]}}
565 end
565 end
566
566
567 should "update custom fields" do
567 should "update custom fields" do
568 assert_no_difference('Issue.count') do
568 assert_no_difference('Issue.count') do
569 put '/issues/3.xml', @parameters, credentials('jsmith')
569 put '/issues/3.xml', @parameters, credentials('jsmith')
570 end
570 end
571
571
572 issue = Issue.find(3)
572 issue = Issue.find(3)
573 assert_equal '150', issue.custom_value_for(2).value
573 assert_equal '150', issue.custom_value_for(2).value
574 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
574 assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort
575 end
575 end
576 end
576 end
577
577
578 context "PUT /issues/3.xml with project change" do
578 context "PUT /issues/3.xml with project change" do
579 setup do
579 setup do
580 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
580 @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}}
581 end
581 end
582
582
583 should "update project" do
583 should "update project" do
584 assert_no_difference('Issue.count') do
584 assert_no_difference('Issue.count') do
585 put '/issues/3.xml', @parameters, credentials('jsmith')
585 put '/issues/3.xml', @parameters, credentials('jsmith')
586 end
586 end
587
587
588 issue = Issue.find(3)
588 issue = Issue.find(3)
589 assert_equal 2, issue.project_id
589 assert_equal 2, issue.project_id
590 assert_equal 'Project changed', issue.subject
590 assert_equal 'Project changed', issue.subject
591 end
591 end
592 end
592 end
593
593
594 context "PUT /issues/6.xml with failed update" do
594 context "PUT /issues/6.xml with failed update" do
595 setup do
595 setup do
596 @parameters = {:issue => {:subject => ''}}
596 @parameters = {:issue => {:subject => ''}}
597 end
597 end
598
598
599 should "not create a new issue" do
599 should "not create a new issue" do
600 assert_no_difference('Issue.count') do
600 assert_no_difference('Issue.count') do
601 put '/issues/6.xml', @parameters, credentials('jsmith')
601 put '/issues/6.xml', @parameters, credentials('jsmith')
602 end
602 end
603 end
603 end
604
604
605 should "not create a new journal" do
605 should "not create a new journal" do
606 assert_no_difference('Journal.count') do
606 assert_no_difference('Journal.count') do
607 put '/issues/6.xml', @parameters, credentials('jsmith')
607 put '/issues/6.xml', @parameters, credentials('jsmith')
608 end
608 end
609 end
609 end
610
610
611 should "have an errors tag" do
611 should "have an errors tag" do
612 put '/issues/6.xml', @parameters, credentials('jsmith')
612 put '/issues/6.xml', @parameters, credentials('jsmith')
613
613
614 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
614 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
615 end
615 end
616 end
616 end
617
617
618 context "PUT /issues/6.json" do
618 context "PUT /issues/6.json" do
619 setup do
619 setup do
620 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
620 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
621 end
621 end
622
622
623 should_allow_api_authentication(:put,
623 should_allow_api_authentication(:put,
624 '/issues/6.json',
624 '/issues/6.json',
625 {:issue => {:subject => 'API update', :notes => 'A new note'}},
625 {:issue => {:subject => 'API update', :notes => 'A new note'}},
626 {:success_code => :ok})
626 {:success_code => :ok})
627
627
628 should "not create a new issue" do
628 should "not create a new issue" do
629 assert_no_difference('Issue.count') do
629 assert_no_difference('Issue.count') do
630 put '/issues/6.json', @parameters, credentials('jsmith')
630 put '/issues/6.json', @parameters, credentials('jsmith')
631 end
631 end
632 end
632 end
633
633
634 should "create a new journal" do
634 should "create a new journal" do
635 assert_difference('Journal.count') do
635 assert_difference('Journal.count') do
636 put '/issues/6.json', @parameters, credentials('jsmith')
636 put '/issues/6.json', @parameters, credentials('jsmith')
637 end
637 end
638 end
638 end
639
639
640 should "add the note to the journal" do
640 should "add the note to the journal" do
641 put '/issues/6.json', @parameters, credentials('jsmith')
641 put '/issues/6.json', @parameters, credentials('jsmith')
642
642
643 journal = Journal.last
643 journal = Journal.last
644 assert_equal "A new note", journal.notes
644 assert_equal "A new note", journal.notes
645 end
645 end
646
646
647 should "update the issue" do
647 should "update the issue" do
648 put '/issues/6.json', @parameters, credentials('jsmith')
648 put '/issues/6.json', @parameters, credentials('jsmith')
649
649
650 issue = Issue.find(6)
650 issue = Issue.find(6)
651 assert_equal "API update", issue.subject
651 assert_equal "API update", issue.subject
652 end
652 end
653
653
654 end
654 end
655
655
656 context "PUT /issues/6.json with failed update" do
656 context "PUT /issues/6.json with failed update" do
657 setup do
657 setup do
658 @parameters = {:issue => {:subject => ''}}
658 @parameters = {:issue => {:subject => ''}}
659 end
659 end
660
660
661 should "not create a new issue" do
661 should "not create a new issue" do
662 assert_no_difference('Issue.count') do
662 assert_no_difference('Issue.count') do
663 put '/issues/6.json', @parameters, credentials('jsmith')
663 put '/issues/6.json', @parameters, credentials('jsmith')
664 end
664 end
665 end
665 end
666
666
667 should "not create a new journal" do
667 should "not create a new journal" do
668 assert_no_difference('Journal.count') do
668 assert_no_difference('Journal.count') do
669 put '/issues/6.json', @parameters, credentials('jsmith')
669 put '/issues/6.json', @parameters, credentials('jsmith')
670 end
670 end
671 end
671 end
672
672
673 should "have an errors attribute" do
673 should "have an errors attribute" do
674 put '/issues/6.json', @parameters, credentials('jsmith')
674 put '/issues/6.json', @parameters, credentials('jsmith')
675
675
676 json = ActiveSupport::JSON.decode(response.body)
676 json = ActiveSupport::JSON.decode(response.body)
677 assert json['errors'].include?("Subject can't be blank")
677 assert json['errors'].include?("Subject can't be blank")
678 end
678 end
679 end
679 end
680
680
681 context "DELETE /issues/1.xml" do
681 context "DELETE /issues/1.xml" do
682 should_allow_api_authentication(:delete,
682 should_allow_api_authentication(:delete,
683 '/issues/6.xml',
683 '/issues/6.xml',
684 {},
684 {},
685 {:success_code => :ok})
685 {:success_code => :ok})
686
686
687 should "delete the issue" do
687 should "delete the issue" do
688 assert_difference('Issue.count',-1) do
688 assert_difference('Issue.count',-1) do
689 delete '/issues/6.xml', {}, credentials('jsmith')
689 delete '/issues/6.xml', {}, credentials('jsmith')
690 end
690 end
691
691
692 assert_nil Issue.find_by_id(6)
692 assert_nil Issue.find_by_id(6)
693 end
693 end
694 end
694 end
695
695
696 context "DELETE /issues/1.json" do
696 context "DELETE /issues/1.json" do
697 should_allow_api_authentication(:delete,
697 should_allow_api_authentication(:delete,
698 '/issues/6.json',
698 '/issues/6.json',
699 {},
699 {},
700 {:success_code => :ok})
700 {:success_code => :ok})
701
701
702 should "delete the issue" do
702 should "delete the issue" do
703 assert_difference('Issue.count',-1) do
703 assert_difference('Issue.count',-1) do
704 delete '/issues/6.json', {}, credentials('jsmith')
704 delete '/issues/6.json', {}, credentials('jsmith')
705 end
705 end
706
706
707 assert_nil Issue.find_by_id(6)
707 assert_nil Issue.find_by_id(6)
708 end
708 end
709 end
709 end
710
710
711 def test_create_issue_with_uploaded_file
711 def test_create_issue_with_uploaded_file
712 set_tmp_attachments_directory
712 set_tmp_attachments_directory
713
713
714 # upload the file
714 # upload the file
715 assert_difference 'Attachment.count' do
715 assert_difference 'Attachment.count' do
716 post '/uploads.xml', 'test_create_with_upload', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
716 post '/uploads.xml', 'test_create_with_upload', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
717 assert_response :created
717 assert_response :created
718 end
718 end
719 xml = Hash.from_xml(response.body)
719 xml = Hash.from_xml(response.body)
720 token = xml['upload']['token']
720 token = xml['upload']['token']
721 attachment = Attachment.first(:order => 'id DESC')
721 attachment = Attachment.first(:order => 'id DESC')
722
722
723 # create the issue with the upload's token
723 # create the issue with the upload's token
724 assert_difference 'Issue.count' do
724 assert_difference 'Issue.count' do
725 post '/issues.xml',
725 post '/issues.xml',
726 {:issue => {:project_id => 1, :subject => 'Uploaded file', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
726 {:issue => {:project_id => 1, :subject => 'Uploaded file', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
727 credentials('jsmith')
727 credentials('jsmith')
728 assert_response :created
728 assert_response :created
729 end
729 end
730 issue = Issue.first(:order => 'id DESC')
730 issue = Issue.first(:order => 'id DESC')
731 assert_equal 1, issue.attachments.count
731 assert_equal 1, issue.attachments.count
732 assert_equal attachment, issue.attachments.first
732 assert_equal attachment, issue.attachments.first
733
733
734 attachment.reload
734 attachment.reload
735 assert_equal 'test.txt', attachment.filename
735 assert_equal 'test.txt', attachment.filename
736 assert_equal 'text/plain', attachment.content_type
736 assert_equal 'text/plain', attachment.content_type
737 assert_equal 'test_create_with_upload'.size, attachment.filesize
737 assert_equal 'test_create_with_upload'.size, attachment.filesize
738 assert_equal 2, attachment.author_id
738 assert_equal 2, attachment.author_id
739
739
740 # get the issue with its attachments
740 # get the issue with its attachments
741 get "/issues/#{issue.id}.xml", :include => 'attachments'
741 get "/issues/#{issue.id}.xml", :include => 'attachments'
742 assert_response :success
742 assert_response :success
743 xml = Hash.from_xml(response.body)
743 xml = Hash.from_xml(response.body)
744 attachments = xml['issue']['attachments']
744 attachments = xml['issue']['attachments']
745 assert_kind_of Array, attachments
745 assert_kind_of Array, attachments
746 assert_equal 1, attachments.size
746 assert_equal 1, attachments.size
747 url = attachments.first['content_url']
747 url = attachments.first['content_url']
748 assert_not_nil url
748 assert_not_nil url
749
749
750 # download the attachment
750 # download the attachment
751 get url
751 get url
752 assert_response :success
752 assert_response :success
753 end
753 end
754
754
755 def test_update_issue_with_uploaded_file
755 def test_update_issue_with_uploaded_file
756 set_tmp_attachments_directory
756 set_tmp_attachments_directory
757
757
758 # upload the file
758 # upload the file
759 assert_difference 'Attachment.count' do
759 assert_difference 'Attachment.count' do
760 post '/uploads.xml', 'test_upload_with_upload', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
760 post '/uploads.xml', 'test_upload_with_upload', {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith'))
761 assert_response :created
761 assert_response :created
762 end
762 end
763 xml = Hash.from_xml(response.body)
763 xml = Hash.from_xml(response.body)
764 token = xml['upload']['token']
764 token = xml['upload']['token']
765 attachment = Attachment.first(:order => 'id DESC')
765 attachment = Attachment.first(:order => 'id DESC')
766
766
767 # update the issue with the upload's token
767 # update the issue with the upload's token
768 assert_difference 'Journal.count' do
768 assert_difference 'Journal.count' do
769 put '/issues/1.xml',
769 put '/issues/1.xml',
770 {:issue => {:notes => 'Attachment added', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
770 {:issue => {:notes => 'Attachment added', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}},
771 credentials('jsmith')
771 credentials('jsmith')
772 assert_response :ok
772 assert_response :ok
773 assert_equal '', @response.body
773 end
774 end
774
775
775 issue = Issue.find(1)
776 issue = Issue.find(1)
776 assert_include attachment, issue.attachments
777 assert_include attachment, issue.attachments
777 end
778 end
778 end
779 end
@@ -1,198 +1,200
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::MembershipsTest < ActionController::IntegrationTest
20 class ApiTest::MembershipsTest < ActionController::IntegrationTest
21 fixtures :projects, :users, :roles, :members, :member_roles
21 fixtures :projects, :users, :roles, :members, :member_roles
22
22
23 def setup
23 def setup
24 Setting.rest_api_enabled = '1'
24 Setting.rest_api_enabled = '1'
25 end
25 end
26
26
27 context "/projects/:project_id/memberships" do
27 context "/projects/:project_id/memberships" do
28 context "GET" do
28 context "GET" do
29 context "xml" do
29 context "xml" do
30 should "return memberships" do
30 should "return memberships" do
31 get '/projects/1/memberships.xml', {}, credentials('jsmith')
31 get '/projects/1/memberships.xml', {}, credentials('jsmith')
32
32
33 assert_response :success
33 assert_response :success
34 assert_equal 'application/xml', @response.content_type
34 assert_equal 'application/xml', @response.content_type
35 assert_tag :tag => 'memberships',
35 assert_tag :tag => 'memberships',
36 :attributes => {:type => 'array'},
36 :attributes => {:type => 'array'},
37 :child => {
37 :child => {
38 :tag => 'membership',
38 :tag => 'membership',
39 :child => {
39 :child => {
40 :tag => 'id',
40 :tag => 'id',
41 :content => '2',
41 :content => '2',
42 :sibling => {
42 :sibling => {
43 :tag => 'user',
43 :tag => 'user',
44 :attributes => {:id => '3', :name => 'Dave Lopper'},
44 :attributes => {:id => '3', :name => 'Dave Lopper'},
45 :sibling => {
45 :sibling => {
46 :tag => 'roles',
46 :tag => 'roles',
47 :child => {
47 :child => {
48 :tag => 'role',
48 :tag => 'role',
49 :attributes => {:id => '2', :name => 'Developer'}
49 :attributes => {:id => '2', :name => 'Developer'}
50 }
50 }
51 }
51 }
52 }
52 }
53 }
53 }
54 }
54 }
55 end
55 end
56 end
56 end
57
57
58 context "json" do
58 context "json" do
59 should "return memberships" do
59 should "return memberships" do
60 get '/projects/1/memberships.json', {}, credentials('jsmith')
60 get '/projects/1/memberships.json', {}, credentials('jsmith')
61
61
62 assert_response :success
62 assert_response :success
63 assert_equal 'application/json', @response.content_type
63 assert_equal 'application/json', @response.content_type
64 json = ActiveSupport::JSON.decode(response.body)
64 json = ActiveSupport::JSON.decode(response.body)
65 assert_equal({
65 assert_equal({
66 "memberships" =>
66 "memberships" =>
67 [{"id"=>1,
67 [{"id"=>1,
68 "project" => {"name"=>"eCookbook", "id"=>1},
68 "project" => {"name"=>"eCookbook", "id"=>1},
69 "roles" => [{"name"=>"Manager", "id"=>1}],
69 "roles" => [{"name"=>"Manager", "id"=>1}],
70 "user" => {"name"=>"John Smith", "id"=>2}},
70 "user" => {"name"=>"John Smith", "id"=>2}},
71 {"id"=>2,
71 {"id"=>2,
72 "project" => {"name"=>"eCookbook", "id"=>1},
72 "project" => {"name"=>"eCookbook", "id"=>1},
73 "roles" => [{"name"=>"Developer", "id"=>2}],
73 "roles" => [{"name"=>"Developer", "id"=>2}],
74 "user" => {"name"=>"Dave Lopper", "id"=>3}}],
74 "user" => {"name"=>"Dave Lopper", "id"=>3}}],
75 "limit" => 25,
75 "limit" => 25,
76 "total_count" => 2,
76 "total_count" => 2,
77 "offset" => 0},
77 "offset" => 0},
78 json)
78 json)
79 end
79 end
80 end
80 end
81 end
81 end
82
82
83 context "POST" do
83 context "POST" do
84 context "xml" do
84 context "xml" do
85 should "create membership" do
85 should "create membership" do
86 assert_difference 'Member.count' do
86 assert_difference 'Member.count' do
87 post '/projects/1/memberships.xml', {:membership => {:user_id => 7, :role_ids => [2,3]}}, credentials('jsmith')
87 post '/projects/1/memberships.xml', {:membership => {:user_id => 7, :role_ids => [2,3]}}, credentials('jsmith')
88
88
89 assert_response :created
89 assert_response :created
90 end
90 end
91 end
91 end
92
92
93 should "return errors on failure" do
93 should "return errors on failure" do
94 assert_no_difference 'Member.count' do
94 assert_no_difference 'Member.count' do
95 post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith')
95 post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith')
96
96
97 assert_response :unprocessable_entity
97 assert_response :unprocessable_entity
98 assert_equal 'application/xml', @response.content_type
98 assert_equal 'application/xml', @response.content_type
99 assert_tag 'errors', :child => {:tag => 'error', :content => "Principal can't be blank"}
99 assert_tag 'errors', :child => {:tag => 'error', :content => "Principal can't be blank"}
100 end
100 end
101 end
101 end
102 end
102 end
103 end
103 end
104 end
104 end
105
105
106 context "/memberships/:id" do
106 context "/memberships/:id" do
107 context "GET" do
107 context "GET" do
108 context "xml" do
108 context "xml" do
109 should "return the membership" do
109 should "return the membership" do
110 get '/memberships/2.xml', {}, credentials('jsmith')
110 get '/memberships/2.xml', {}, credentials('jsmith')
111
111
112 assert_response :success
112 assert_response :success
113 assert_equal 'application/xml', @response.content_type
113 assert_equal 'application/xml', @response.content_type
114 assert_tag :tag => 'membership',
114 assert_tag :tag => 'membership',
115 :child => {
115 :child => {
116 :tag => 'id',
116 :tag => 'id',
117 :content => '2',
117 :content => '2',
118 :sibling => {
118 :sibling => {
119 :tag => 'user',
119 :tag => 'user',
120 :attributes => {:id => '3', :name => 'Dave Lopper'},
120 :attributes => {:id => '3', :name => 'Dave Lopper'},
121 :sibling => {
121 :sibling => {
122 :tag => 'roles',
122 :tag => 'roles',
123 :child => {
123 :child => {
124 :tag => 'role',
124 :tag => 'role',
125 :attributes => {:id => '2', :name => 'Developer'}
125 :attributes => {:id => '2', :name => 'Developer'}
126 }
126 }
127 }
127 }
128 }
128 }
129 }
129 }
130 end
130 end
131 end
131 end
132
132
133 context "json" do
133 context "json" do
134 should "return the membership" do
134 should "return the membership" do
135 get '/memberships/2.json', {}, credentials('jsmith')
135 get '/memberships/2.json', {}, credentials('jsmith')
136
136
137 assert_response :success
137 assert_response :success
138 assert_equal 'application/json', @response.content_type
138 assert_equal 'application/json', @response.content_type
139 json = ActiveSupport::JSON.decode(response.body)
139 json = ActiveSupport::JSON.decode(response.body)
140 assert_equal(
140 assert_equal(
141 {"membership" => {
141 {"membership" => {
142 "id" => 2,
142 "id" => 2,
143 "project" => {"name"=>"eCookbook", "id"=>1},
143 "project" => {"name"=>"eCookbook", "id"=>1},
144 "roles" => [{"name"=>"Developer", "id"=>2}],
144 "roles" => [{"name"=>"Developer", "id"=>2}],
145 "user" => {"name"=>"Dave Lopper", "id"=>3}}
145 "user" => {"name"=>"Dave Lopper", "id"=>3}}
146 },
146 },
147 json)
147 json)
148 end
148 end
149 end
149 end
150 end
150 end
151
151
152 context "PUT" do
152 context "PUT" do
153 context "xml" do
153 context "xml" do
154 should "update membership" do
154 should "update membership" do
155 assert_not_equal [1,2], Member.find(2).role_ids.sort
155 assert_not_equal [1,2], Member.find(2).role_ids.sort
156 assert_no_difference 'Member.count' do
156 assert_no_difference 'Member.count' do
157 put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [1,2]}}, credentials('jsmith')
157 put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [1,2]}}, credentials('jsmith')
158
158
159 assert_response :ok
159 assert_response :ok
160 assert_equal '', @response.body
160 end
161 end
161 member = Member.find(2)
162 member = Member.find(2)
162 assert_equal [1,2], member.role_ids.sort
163 assert_equal [1,2], member.role_ids.sort
163 end
164 end
164
165
165 should "return errors on failure" do
166 should "return errors on failure" do
166 put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith')
167 put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith')
167
168
168 assert_response :unprocessable_entity
169 assert_response :unprocessable_entity
169 assert_equal 'application/xml', @response.content_type
170 assert_equal 'application/xml', @response.content_type
170 assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/}
171 assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/}
171 end
172 end
172 end
173 end
173 end
174 end
174
175
175 context "DELETE" do
176 context "DELETE" do
176 context "xml" do
177 context "xml" do
177 should "destroy membership" do
178 should "destroy membership" do
178 assert_difference 'Member.count', -1 do
179 assert_difference 'Member.count', -1 do
179 delete '/memberships/2.xml', {}, credentials('jsmith')
180 delete '/memberships/2.xml', {}, credentials('jsmith')
180
181
181 assert_response :ok
182 assert_response :ok
183 assert_equal '', @response.body
182 end
184 end
183 assert_nil Member.find_by_id(2)
185 assert_nil Member.find_by_id(2)
184 end
186 end
185
187
186 should "respond with 422 on failure" do
188 should "respond with 422 on failure" do
187 assert_no_difference 'Member.count' do
189 assert_no_difference 'Member.count' do
188 # A membership with an inherited role can't be deleted
190 # A membership with an inherited role can't be deleted
189 Member.find(2).member_roles.first.update_attribute :inherited_from, 99
191 Member.find(2).member_roles.first.update_attribute :inherited_from, 99
190 delete '/memberships/2.xml', {}, credentials('jsmith')
192 delete '/memberships/2.xml', {}, credentials('jsmith')
191
193
192 assert_response :unprocessable_entity
194 assert_response :unprocessable_entity
193 end
195 end
194 end
196 end
195 end
197 end
196 end
198 end
197 end
199 end
198 end
200 end
@@ -1,293 +1,297
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
23 :attachments, :custom_fields, :custom_values, :time_entries, :issue_categories
23 :attachments, :custom_fields, :custom_values, :time_entries, :issue_categories
24
24
25 def setup
25 def setup
26 Setting.rest_api_enabled = '1'
26 Setting.rest_api_enabled = '1'
27 set_tmp_attachments_directory
27 set_tmp_attachments_directory
28 end
28 end
29
29
30 context "GET /projects" do
30 context "GET /projects" do
31 context ".xml" do
31 context ".xml" do
32 should "return projects" do
32 should "return projects" do
33 get '/projects.xml'
33 get '/projects.xml'
34 assert_response :success
34 assert_response :success
35 assert_equal 'application/xml', @response.content_type
35 assert_equal 'application/xml', @response.content_type
36
36
37 assert_tag :tag => 'projects',
37 assert_tag :tag => 'projects',
38 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
38 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
39 end
39 end
40 end
40 end
41
41
42 context ".json" do
42 context ".json" do
43 should "return projects" do
43 should "return projects" do
44 get '/projects.json'
44 get '/projects.json'
45 assert_response :success
45 assert_response :success
46 assert_equal 'application/json', @response.content_type
46 assert_equal 'application/json', @response.content_type
47
47
48 json = ActiveSupport::JSON.decode(response.body)
48 json = ActiveSupport::JSON.decode(response.body)
49 assert_kind_of Hash, json
49 assert_kind_of Hash, json
50 assert_kind_of Array, json['projects']
50 assert_kind_of Array, json['projects']
51 assert_kind_of Hash, json['projects'].first
51 assert_kind_of Hash, json['projects'].first
52 assert json['projects'].first.has_key?('id')
52 assert json['projects'].first.has_key?('id')
53 end
53 end
54 end
54 end
55 end
55 end
56
56
57 context "GET /projects/:id" do
57 context "GET /projects/:id" do
58 context ".xml" do
58 context ".xml" do
59 # TODO: A private project is needed because should_allow_api_authentication
59 # TODO: A private project is needed because should_allow_api_authentication
60 # actually tests that authentication is *required*, not just allowed
60 # actually tests that authentication is *required*, not just allowed
61 should_allow_api_authentication(:get, "/projects/2.xml")
61 should_allow_api_authentication(:get, "/projects/2.xml")
62
62
63 should "return requested project" do
63 should "return requested project" do
64 get '/projects/1.xml'
64 get '/projects/1.xml'
65 assert_response :success
65 assert_response :success
66 assert_equal 'application/xml', @response.content_type
66 assert_equal 'application/xml', @response.content_type
67
67
68 assert_tag :tag => 'project',
68 assert_tag :tag => 'project',
69 :child => {:tag => 'id', :content => '1'}
69 :child => {:tag => 'id', :content => '1'}
70 assert_tag :tag => 'custom_field',
70 assert_tag :tag => 'custom_field',
71 :attributes => {:name => 'Development status'}, :content => 'Stable'
71 :attributes => {:name => 'Development status'}, :content => 'Stable'
72
72
73 assert_no_tag 'trackers'
73 assert_no_tag 'trackers'
74 assert_no_tag 'issue_categories'
74 assert_no_tag 'issue_categories'
75 end
75 end
76
76
77 context "with hidden custom fields" do
77 context "with hidden custom fields" do
78 setup do
78 setup do
79 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
79 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
80 end
80 end
81
81
82 should "not display hidden custom fields" do
82 should "not display hidden custom fields" do
83 get '/projects/1.xml'
83 get '/projects/1.xml'
84 assert_response :success
84 assert_response :success
85 assert_equal 'application/xml', @response.content_type
85 assert_equal 'application/xml', @response.content_type
86
86
87 assert_no_tag 'custom_field',
87 assert_no_tag 'custom_field',
88 :attributes => {:name => 'Development status'}
88 :attributes => {:name => 'Development status'}
89 end
89 end
90 end
90 end
91
91
92 should "return categories with include=issue_categories" do
92 should "return categories with include=issue_categories" do
93 get '/projects/1.xml?include=issue_categories'
93 get '/projects/1.xml?include=issue_categories'
94 assert_response :success
94 assert_response :success
95 assert_equal 'application/xml', @response.content_type
95 assert_equal 'application/xml', @response.content_type
96
96
97 assert_tag 'issue_categories',
97 assert_tag 'issue_categories',
98 :attributes => {:type => 'array'},
98 :attributes => {:type => 'array'},
99 :child => {
99 :child => {
100 :tag => 'issue_category',
100 :tag => 'issue_category',
101 :attributes => {
101 :attributes => {
102 :id => '2',
102 :id => '2',
103 :name => 'Recipes'
103 :name => 'Recipes'
104 }
104 }
105 }
105 }
106 end
106 end
107
107
108 should "return trackers with include=trackers" do
108 should "return trackers with include=trackers" do
109 get '/projects/1.xml?include=trackers'
109 get '/projects/1.xml?include=trackers'
110 assert_response :success
110 assert_response :success
111 assert_equal 'application/xml', @response.content_type
111 assert_equal 'application/xml', @response.content_type
112
112
113 assert_tag 'trackers',
113 assert_tag 'trackers',
114 :attributes => {:type => 'array'},
114 :attributes => {:type => 'array'},
115 :child => {
115 :child => {
116 :tag => 'tracker',
116 :tag => 'tracker',
117 :attributes => {
117 :attributes => {
118 :id => '2',
118 :id => '2',
119 :name => 'Feature request'
119 :name => 'Feature request'
120 }
120 }
121 }
121 }
122 end
122 end
123 end
123 end
124
124
125 context ".json" do
125 context ".json" do
126 should_allow_api_authentication(:get, "/projects/2.json")
126 should_allow_api_authentication(:get, "/projects/2.json")
127
127
128 should "return requested project" do
128 should "return requested project" do
129 get '/projects/1.json'
129 get '/projects/1.json'
130
130
131 json = ActiveSupport::JSON.decode(response.body)
131 json = ActiveSupport::JSON.decode(response.body)
132 assert_kind_of Hash, json
132 assert_kind_of Hash, json
133 assert_kind_of Hash, json['project']
133 assert_kind_of Hash, json['project']
134 assert_equal 1, json['project']['id']
134 assert_equal 1, json['project']['id']
135 end
135 end
136 end
136 end
137 end
137 end
138
138
139 context "POST /projects" do
139 context "POST /projects" do
140 context "with valid parameters" do
140 context "with valid parameters" do
141 setup do
141 setup do
142 Setting.default_projects_modules = ['issue_tracking', 'repository']
142 Setting.default_projects_modules = ['issue_tracking', 'repository']
143 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
143 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
144 end
144 end
145
145
146 context ".xml" do
146 context ".xml" do
147 should_allow_api_authentication(:post,
147 should_allow_api_authentication(:post,
148 '/projects.xml',
148 '/projects.xml',
149 {:project => {:name => 'API test', :identifier => 'api-test'}},
149 {:project => {:name => 'API test', :identifier => 'api-test'}},
150 {:success_code => :created})
150 {:success_code => :created})
151
151
152
152
153 should "create a project with the attributes" do
153 should "create a project with the attributes" do
154 assert_difference('Project.count') do
154 assert_difference('Project.count') do
155 post '/projects.xml', @parameters, credentials('admin')
155 post '/projects.xml', @parameters, credentials('admin')
156 end
156 end
157
157
158 project = Project.first(:order => 'id DESC')
158 project = Project.first(:order => 'id DESC')
159 assert_equal 'API test', project.name
159 assert_equal 'API test', project.name
160 assert_equal 'api-test', project.identifier
160 assert_equal 'api-test', project.identifier
161 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort
161 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names.sort
162 assert_equal Tracker.all.size, project.trackers.size
162 assert_equal Tracker.all.size, project.trackers.size
163
163
164 assert_response :created
164 assert_response :created
165 assert_equal 'application/xml', @response.content_type
165 assert_equal 'application/xml', @response.content_type
166 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
166 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
167 end
167 end
168
168
169 should "accept enabled_module_names attribute" do
169 should "accept enabled_module_names attribute" do
170 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
170 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
171
171
172 assert_difference('Project.count') do
172 assert_difference('Project.count') do
173 post '/projects.xml', @parameters, credentials('admin')
173 post '/projects.xml', @parameters, credentials('admin')
174 end
174 end
175
175
176 project = Project.first(:order => 'id DESC')
176 project = Project.first(:order => 'id DESC')
177 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
177 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
178 end
178 end
179
179
180 should "accept tracker_ids attribute" do
180 should "accept tracker_ids attribute" do
181 @parameters[:project].merge!({:tracker_ids => [1, 3]})
181 @parameters[:project].merge!({:tracker_ids => [1, 3]})
182
182
183 assert_difference('Project.count') do
183 assert_difference('Project.count') do
184 post '/projects.xml', @parameters, credentials('admin')
184 post '/projects.xml', @parameters, credentials('admin')
185 end
185 end
186
186
187 project = Project.first(:order => 'id DESC')
187 project = Project.first(:order => 'id DESC')
188 assert_equal [1, 3], project.trackers.map(&:id).sort
188 assert_equal [1, 3], project.trackers.map(&:id).sort
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 context "with invalid parameters" do
193 context "with invalid parameters" do
194 setup do
194 setup do
195 @parameters = {:project => {:name => 'API test'}}
195 @parameters = {:project => {:name => 'API test'}}
196 end
196 end
197
197
198 context ".xml" do
198 context ".xml" do
199 should "return errors" do
199 should "return errors" do
200 assert_no_difference('Project.count') do
200 assert_no_difference('Project.count') do
201 post '/projects.xml', @parameters, credentials('admin')
201 post '/projects.xml', @parameters, credentials('admin')
202 end
202 end
203
203
204 assert_response :unprocessable_entity
204 assert_response :unprocessable_entity
205 assert_equal 'application/xml', @response.content_type
205 assert_equal 'application/xml', @response.content_type
206 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
206 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
207 end
207 end
208 end
208 end
209 end
209 end
210 end
210 end
211
211
212 context "PUT /projects/:id" do
212 context "PUT /projects/:id" do
213 context "with valid parameters" do
213 context "with valid parameters" do
214 setup do
214 setup do
215 @parameters = {:project => {:name => 'API update'}}
215 @parameters = {:project => {:name => 'API update'}}
216 end
216 end
217
217
218 context ".xml" do
218 context ".xml" do
219 should_allow_api_authentication(:put,
219 should_allow_api_authentication(:put,
220 '/projects/2.xml',
220 '/projects/2.xml',
221 {:project => {:name => 'API update'}},
221 {:project => {:name => 'API update'}},
222 {:success_code => :ok})
222 {:success_code => :ok})
223
223
224 should "update the project" do
224 should "update the project" do
225 assert_no_difference 'Project.count' do
225 assert_no_difference 'Project.count' do
226 put '/projects/2.xml', @parameters, credentials('jsmith')
226 put '/projects/2.xml', @parameters, credentials('jsmith')
227 end
227 end
228 assert_response :ok
228 assert_response :ok
229 assert_equal '', @response.body
229 assert_equal 'application/xml', @response.content_type
230 assert_equal 'application/xml', @response.content_type
230 project = Project.find(2)
231 project = Project.find(2)
231 assert_equal 'API update', project.name
232 assert_equal 'API update', project.name
232 end
233 end
233
234
234 should "accept enabled_module_names attribute" do
235 should "accept enabled_module_names attribute" do
235 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
236 @parameters[:project].merge!({:enabled_module_names => ['issue_tracking', 'news', 'time_tracking']})
236
237
237 assert_no_difference 'Project.count' do
238 assert_no_difference 'Project.count' do
238 put '/projects/2.xml', @parameters, credentials('admin')
239 put '/projects/2.xml', @parameters, credentials('admin')
239 end
240 end
240 assert_response :ok
241 assert_response :ok
242 assert_equal '', @response.body
241 project = Project.find(2)
243 project = Project.find(2)
242 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
244 assert_equal ['issue_tracking', 'news', 'time_tracking'], project.enabled_module_names.sort
243 end
245 end
244
246
245 should "accept tracker_ids attribute" do
247 should "accept tracker_ids attribute" do
246 @parameters[:project].merge!({:tracker_ids => [1, 3]})
248 @parameters[:project].merge!({:tracker_ids => [1, 3]})
247
249
248 assert_no_difference 'Project.count' do
250 assert_no_difference 'Project.count' do
249 put '/projects/2.xml', @parameters, credentials('admin')
251 put '/projects/2.xml', @parameters, credentials('admin')
250 end
252 end
251 assert_response :ok
253 assert_response :ok
254 assert_equal '', @response.body
252 project = Project.find(2)
255 project = Project.find(2)
253 assert_equal [1, 3], project.trackers.map(&:id).sort
256 assert_equal [1, 3], project.trackers.map(&:id).sort
254 end
257 end
255 end
258 end
256 end
259 end
257
260
258 context "with invalid parameters" do
261 context "with invalid parameters" do
259 setup do
262 setup do
260 @parameters = {:project => {:name => ''}}
263 @parameters = {:project => {:name => ''}}
261 end
264 end
262
265
263 context ".xml" do
266 context ".xml" do
264 should "return errors" do
267 should "return errors" do
265 assert_no_difference('Project.count') do
268 assert_no_difference('Project.count') do
266 put '/projects/2.xml', @parameters, credentials('admin')
269 put '/projects/2.xml', @parameters, credentials('admin')
267 end
270 end
268
271
269 assert_response :unprocessable_entity
272 assert_response :unprocessable_entity
270 assert_equal 'application/xml', @response.content_type
273 assert_equal 'application/xml', @response.content_type
271 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
274 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
272 end
275 end
273 end
276 end
274 end
277 end
275 end
278 end
276
279
277 context "DELETE /projects/:id" do
280 context "DELETE /projects/:id" do
278 context ".xml" do
281 context ".xml" do
279 should_allow_api_authentication(:delete,
282 should_allow_api_authentication(:delete,
280 '/projects/2.xml',
283 '/projects/2.xml',
281 {},
284 {},
282 {:success_code => :ok})
285 {:success_code => :ok})
283
286
284 should "delete the project" do
287 should "delete the project" do
285 assert_difference('Project.count',-1) do
288 assert_difference('Project.count',-1) do
286 delete '/projects/2.xml', {}, credentials('admin')
289 delete '/projects/2.xml', {}, credentials('admin')
287 end
290 end
288 assert_response :ok
291 assert_response :ok
292 assert_equal '', @response.body
289 assert_nil Project.find_by_id(2)
293 assert_nil Project.find_by_id(2)
290 end
294 end
291 end
295 end
292 end
296 end
293 end
297 end
@@ -1,163 +1,165
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::TimeEntriesTest < ActionController::IntegrationTest
20 class ApiTest::TimeEntriesTest < ActionController::IntegrationTest
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users, :issue_categories,
22 :enumerations, :users, :issue_categories,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :workflows,
28 :workflows,
29 :time_entries
29 :time_entries
30
30
31 def setup
31 def setup
32 Setting.rest_api_enabled = '1'
32 Setting.rest_api_enabled = '1'
33 end
33 end
34
34
35 context "GET /time_entries.xml" do
35 context "GET /time_entries.xml" do
36 should "return time entries" do
36 should "return time entries" do
37 get '/time_entries.xml', {}, credentials('jsmith')
37 get '/time_entries.xml', {}, credentials('jsmith')
38 assert_response :success
38 assert_response :success
39 assert_equal 'application/xml', @response.content_type
39 assert_equal 'application/xml', @response.content_type
40 assert_tag :tag => 'time_entries',
40 assert_tag :tag => 'time_entries',
41 :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}}
41 :child => {:tag => 'time_entry', :child => {:tag => 'id', :content => '2'}}
42 end
42 end
43
43
44 context "with limit" do
44 context "with limit" do
45 should "return limited results" do
45 should "return limited results" do
46 get '/time_entries.xml?limit=2', {}, credentials('jsmith')
46 get '/time_entries.xml?limit=2', {}, credentials('jsmith')
47 assert_response :success
47 assert_response :success
48 assert_equal 'application/xml', @response.content_type
48 assert_equal 'application/xml', @response.content_type
49 assert_tag :tag => 'time_entries',
49 assert_tag :tag => 'time_entries',
50 :children => {:count => 2}
50 :children => {:count => 2}
51 end
51 end
52 end
52 end
53 end
53 end
54
54
55 context "GET /time_entries/2.xml" do
55 context "GET /time_entries/2.xml" do
56 should "return requested time entry" do
56 should "return requested time entry" do
57 get '/time_entries/2.xml', {}, credentials('jsmith')
57 get '/time_entries/2.xml', {}, credentials('jsmith')
58 assert_response :success
58 assert_response :success
59 assert_equal 'application/xml', @response.content_type
59 assert_equal 'application/xml', @response.content_type
60 assert_tag :tag => 'time_entry',
60 assert_tag :tag => 'time_entry',
61 :child => {:tag => 'id', :content => '2'}
61 :child => {:tag => 'id', :content => '2'}
62 end
62 end
63 end
63 end
64
64
65 context "POST /time_entries.xml" do
65 context "POST /time_entries.xml" do
66 context "with issue_id" do
66 context "with issue_id" do
67 should "return create time entry" do
67 should "return create time entry" do
68 assert_difference 'TimeEntry.count' do
68 assert_difference 'TimeEntry.count' do
69 post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
69 post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
70 end
70 end
71 assert_response :created
71 assert_response :created
72 assert_equal 'application/xml', @response.content_type
72 assert_equal 'application/xml', @response.content_type
73
73
74 entry = TimeEntry.first(:order => 'id DESC')
74 entry = TimeEntry.first(:order => 'id DESC')
75 assert_equal 'jsmith', entry.user.login
75 assert_equal 'jsmith', entry.user.login
76 assert_equal Issue.find(1), entry.issue
76 assert_equal Issue.find(1), entry.issue
77 assert_equal Project.find(1), entry.project
77 assert_equal Project.find(1), entry.project
78 assert_equal Date.parse('2010-12-02'), entry.spent_on
78 assert_equal Date.parse('2010-12-02'), entry.spent_on
79 assert_equal 3.5, entry.hours
79 assert_equal 3.5, entry.hours
80 assert_equal TimeEntryActivity.find(11), entry.activity
80 assert_equal TimeEntryActivity.find(11), entry.activity
81 end
81 end
82
82
83 should "accept custom fields" do
83 should "accept custom fields" do
84 field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string')
84 field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string')
85
85
86 assert_difference 'TimeEntry.count' do
86 assert_difference 'TimeEntry.count' do
87 post '/time_entries.xml', {:time_entry => {
87 post '/time_entries.xml', {:time_entry => {
88 :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}]
88 :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}]
89 }}, credentials('jsmith')
89 }}, credentials('jsmith')
90 end
90 end
91 assert_response :created
91 assert_response :created
92 assert_equal 'application/xml', @response.content_type
92 assert_equal 'application/xml', @response.content_type
93
93
94 entry = TimeEntry.first(:order => 'id DESC')
94 entry = TimeEntry.first(:order => 'id DESC')
95 assert_equal 'accepted', entry.custom_field_value(field)
95 assert_equal 'accepted', entry.custom_field_value(field)
96 end
96 end
97 end
97 end
98
98
99 context "with project_id" do
99 context "with project_id" do
100 should "return create time entry" do
100 should "return create time entry" do
101 assert_difference 'TimeEntry.count' do
101 assert_difference 'TimeEntry.count' do
102 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
102 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
103 end
103 end
104 assert_response :created
104 assert_response :created
105 assert_equal 'application/xml', @response.content_type
105 assert_equal 'application/xml', @response.content_type
106
106
107 entry = TimeEntry.first(:order => 'id DESC')
107 entry = TimeEntry.first(:order => 'id DESC')
108 assert_equal 'jsmith', entry.user.login
108 assert_equal 'jsmith', entry.user.login
109 assert_nil entry.issue
109 assert_nil entry.issue
110 assert_equal Project.find(1), entry.project
110 assert_equal Project.find(1), entry.project
111 assert_equal Date.parse('2010-12-02'), entry.spent_on
111 assert_equal Date.parse('2010-12-02'), entry.spent_on
112 assert_equal 3.5, entry.hours
112 assert_equal 3.5, entry.hours
113 assert_equal TimeEntryActivity.find(11), entry.activity
113 assert_equal TimeEntryActivity.find(11), entry.activity
114 end
114 end
115 end
115 end
116
116
117 context "with invalid parameters" do
117 context "with invalid parameters" do
118 should "return errors" do
118 should "return errors" do
119 assert_no_difference 'TimeEntry.count' do
119 assert_no_difference 'TimeEntry.count' do
120 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith')
120 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith')
121 end
121 end
122 assert_response :unprocessable_entity
122 assert_response :unprocessable_entity
123 assert_equal 'application/xml', @response.content_type
123 assert_equal 'application/xml', @response.content_type
124
124
125 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
125 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
126 end
126 end
127 end
127 end
128 end
128 end
129
129
130 context "PUT /time_entries/2.xml" do
130 context "PUT /time_entries/2.xml" do
131 context "with valid parameters" do
131 context "with valid parameters" do
132 should "update time entry" do
132 should "update time entry" do
133 assert_no_difference 'TimeEntry.count' do
133 assert_no_difference 'TimeEntry.count' do
134 put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith')
134 put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith')
135 end
135 end
136 assert_response :ok
136 assert_response :ok
137 assert_equal '', @response.body
137 assert_equal 'API Update', TimeEntry.find(2).comments
138 assert_equal 'API Update', TimeEntry.find(2).comments
138 end
139 end
139 end
140 end
140
141
141 context "with invalid parameters" do
142 context "with invalid parameters" do
142 should "return errors" do
143 should "return errors" do
143 assert_no_difference 'TimeEntry.count' do
144 assert_no_difference 'TimeEntry.count' do
144 put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith')
145 put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith')
145 end
146 end
146 assert_response :unprocessable_entity
147 assert_response :unprocessable_entity
147 assert_equal 'application/xml', @response.content_type
148 assert_equal 'application/xml', @response.content_type
148
149
149 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
150 assert_tag 'errors', :child => {:tag => 'error', :content => "Hours can't be blank"}
150 end
151 end
151 end
152 end
152 end
153 end
153
154
154 context "DELETE /time_entries/2.xml" do
155 context "DELETE /time_entries/2.xml" do
155 should "destroy time entry" do
156 should "destroy time entry" do
156 assert_difference 'TimeEntry.count', -1 do
157 assert_difference 'TimeEntry.count', -1 do
157 delete '/time_entries/2.xml', {}, credentials('jsmith')
158 delete '/time_entries/2.xml', {}, credentials('jsmith')
158 end
159 end
159 assert_response :ok
160 assert_response :ok
161 assert_equal '', @response.body
160 assert_nil TimeEntry.find_by_id(2)
162 assert_nil TimeEntry.find_by_id(2)
161 end
163 end
162 end
164 end
163 end
165 end
@@ -1,343 +1,347
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19 require 'pp'
19 require 'pp'
20 class ApiTest::UsersTest < ActionController::IntegrationTest
20 class ApiTest::UsersTest < ActionController::IntegrationTest
21 fixtures :users
21 fixtures :users
22
22
23 def setup
23 def setup
24 Setting.rest_api_enabled = '1'
24 Setting.rest_api_enabled = '1'
25 end
25 end
26
26
27 context "GET /users" do
27 context "GET /users" do
28 should_allow_api_authentication(:get, "/users.xml")
28 should_allow_api_authentication(:get, "/users.xml")
29 should_allow_api_authentication(:get, "/users.json")
29 should_allow_api_authentication(:get, "/users.json")
30 end
30 end
31
31
32 context "GET /users/2" do
32 context "GET /users/2" do
33 context ".xml" do
33 context ".xml" do
34 should "return requested user" do
34 should "return requested user" do
35 get '/users/2.xml'
35 get '/users/2.xml'
36
36
37 assert_response :success
37 assert_response :success
38 assert_tag :tag => 'user',
38 assert_tag :tag => 'user',
39 :child => {:tag => 'id', :content => '2'}
39 :child => {:tag => 'id', :content => '2'}
40 end
40 end
41
41
42 context "with include=memberships" do
42 context "with include=memberships" do
43 should "include memberships" do
43 should "include memberships" do
44 get '/users/2.xml?include=memberships'
44 get '/users/2.xml?include=memberships'
45
45
46 assert_response :success
46 assert_response :success
47 assert_tag :tag => 'memberships',
47 assert_tag :tag => 'memberships',
48 :parent => {:tag => 'user'},
48 :parent => {:tag => 'user'},
49 :children => {:count => 1}
49 :children => {:count => 1}
50 end
50 end
51 end
51 end
52 end
52 end
53
53
54 context ".json" do
54 context ".json" do
55 should "return requested user" do
55 should "return requested user" do
56 get '/users/2.json'
56 get '/users/2.json'
57
57
58 assert_response :success
58 assert_response :success
59 json = ActiveSupport::JSON.decode(response.body)
59 json = ActiveSupport::JSON.decode(response.body)
60 assert_kind_of Hash, json
60 assert_kind_of Hash, json
61 assert_kind_of Hash, json['user']
61 assert_kind_of Hash, json['user']
62 assert_equal 2, json['user']['id']
62 assert_equal 2, json['user']['id']
63 end
63 end
64
64
65 context "with include=memberships" do
65 context "with include=memberships" do
66 should "include memberships" do
66 should "include memberships" do
67 get '/users/2.json?include=memberships'
67 get '/users/2.json?include=memberships'
68
68
69 assert_response :success
69 assert_response :success
70 json = ActiveSupport::JSON.decode(response.body)
70 json = ActiveSupport::JSON.decode(response.body)
71 assert_kind_of Array, json['user']['memberships']
71 assert_kind_of Array, json['user']['memberships']
72 assert_equal [{
72 assert_equal [{
73 "id"=>1,
73 "id"=>1,
74 "project"=>{"name"=>"eCookbook", "id"=>1},
74 "project"=>{"name"=>"eCookbook", "id"=>1},
75 "roles"=>[{"name"=>"Manager", "id"=>1}]
75 "roles"=>[{"name"=>"Manager", "id"=>1}]
76 }], json['user']['memberships']
76 }], json['user']['memberships']
77 end
77 end
78 end
78 end
79 end
79 end
80 end
80 end
81
81
82 context "GET /users/current" do
82 context "GET /users/current" do
83 context ".xml" do
83 context ".xml" do
84 should "require authentication" do
84 should "require authentication" do
85 get '/users/current.xml'
85 get '/users/current.xml'
86
86
87 assert_response 401
87 assert_response 401
88 end
88 end
89
89
90 should "return current user" do
90 should "return current user" do
91 get '/users/current.xml', {}, credentials('jsmith')
91 get '/users/current.xml', {}, credentials('jsmith')
92
92
93 assert_tag :tag => 'user',
93 assert_tag :tag => 'user',
94 :child => {:tag => 'id', :content => '2'}
94 :child => {:tag => 'id', :content => '2'}
95 end
95 end
96 end
96 end
97 end
97 end
98
98
99 context "POST /users" do
99 context "POST /users" do
100 context "with valid parameters" do
100 context "with valid parameters" do
101 setup do
101 setup do
102 @parameters = {
102 @parameters = {
103 :user => {
103 :user => {
104 :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
104 :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
105 :mail => 'foo@example.net', :password => 'secret',
105 :mail => 'foo@example.net', :password => 'secret',
106 :mail_notification => 'only_assigned'
106 :mail_notification => 'only_assigned'
107 }
107 }
108 }
108 }
109 end
109 end
110
110
111 context ".xml" do
111 context ".xml" do
112 should_allow_api_authentication(:post,
112 should_allow_api_authentication(:post,
113 '/users.xml',
113 '/users.xml',
114 {:user => {
114 {:user => {
115 :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
115 :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
116 :mail => 'foo@example.net', :password => 'secret'
116 :mail => 'foo@example.net', :password => 'secret'
117 }},
117 }},
118 {:success_code => :created})
118 {:success_code => :created})
119
119
120 should "create a user with the attributes" do
120 should "create a user with the attributes" do
121 assert_difference('User.count') do
121 assert_difference('User.count') do
122 post '/users.xml', @parameters, credentials('admin')
122 post '/users.xml', @parameters, credentials('admin')
123 end
123 end
124
124
125 user = User.first(:order => 'id DESC')
125 user = User.first(:order => 'id DESC')
126 assert_equal 'foo', user.login
126 assert_equal 'foo', user.login
127 assert_equal 'Firstname', user.firstname
127 assert_equal 'Firstname', user.firstname
128 assert_equal 'Lastname', user.lastname
128 assert_equal 'Lastname', user.lastname
129 assert_equal 'foo@example.net', user.mail
129 assert_equal 'foo@example.net', user.mail
130 assert_equal 'only_assigned', user.mail_notification
130 assert_equal 'only_assigned', user.mail_notification
131 assert !user.admin?
131 assert !user.admin?
132 assert user.check_password?('secret')
132 assert user.check_password?('secret')
133
133
134 assert_response :created
134 assert_response :created
135 assert_equal 'application/xml', @response.content_type
135 assert_equal 'application/xml', @response.content_type
136 assert_tag 'user', :child => {:tag => 'id', :content => user.id.to_s}
136 assert_tag 'user', :child => {:tag => 'id', :content => user.id.to_s}
137 end
137 end
138 end
138 end
139
139
140 context ".json" do
140 context ".json" do
141 should_allow_api_authentication(:post,
141 should_allow_api_authentication(:post,
142 '/users.json',
142 '/users.json',
143 {:user => {
143 {:user => {
144 :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
144 :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname',
145 :mail => 'foo@example.net'
145 :mail => 'foo@example.net'
146 }},
146 }},
147 {:success_code => :created})
147 {:success_code => :created})
148
148
149 should "create a user with the attributes" do
149 should "create a user with the attributes" do
150 assert_difference('User.count') do
150 assert_difference('User.count') do
151 post '/users.json', @parameters, credentials('admin')
151 post '/users.json', @parameters, credentials('admin')
152 end
152 end
153
153
154 user = User.first(:order => 'id DESC')
154 user = User.first(:order => 'id DESC')
155 assert_equal 'foo', user.login
155 assert_equal 'foo', user.login
156 assert_equal 'Firstname', user.firstname
156 assert_equal 'Firstname', user.firstname
157 assert_equal 'Lastname', user.lastname
157 assert_equal 'Lastname', user.lastname
158 assert_equal 'foo@example.net', user.mail
158 assert_equal 'foo@example.net', user.mail
159 assert !user.admin?
159 assert !user.admin?
160
160
161 assert_response :created
161 assert_response :created
162 assert_equal 'application/json', @response.content_type
162 assert_equal 'application/json', @response.content_type
163 json = ActiveSupport::JSON.decode(response.body)
163 json = ActiveSupport::JSON.decode(response.body)
164 assert_kind_of Hash, json
164 assert_kind_of Hash, json
165 assert_kind_of Hash, json['user']
165 assert_kind_of Hash, json['user']
166 assert_equal user.id, json['user']['id']
166 assert_equal user.id, json['user']['id']
167 end
167 end
168 end
168 end
169 end
169 end
170
170
171 context "with invalid parameters" do
171 context "with invalid parameters" do
172 setup do
172 setup do
173 @parameters = {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}}
173 @parameters = {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}}
174 end
174 end
175
175
176 context ".xml" do
176 context ".xml" do
177 should "return errors" do
177 should "return errors" do
178 assert_no_difference('User.count') do
178 assert_no_difference('User.count') do
179 post '/users.xml', @parameters, credentials('admin')
179 post '/users.xml', @parameters, credentials('admin')
180 end
180 end
181
181
182 assert_response :unprocessable_entity
182 assert_response :unprocessable_entity
183 assert_equal 'application/xml', @response.content_type
183 assert_equal 'application/xml', @response.content_type
184 assert_tag 'errors', :child => {
184 assert_tag 'errors', :child => {
185 :tag => 'error',
185 :tag => 'error',
186 :content => "First name can't be blank"
186 :content => "First name can't be blank"
187 }
187 }
188 end
188 end
189 end
189 end
190
190
191 context ".json" do
191 context ".json" do
192 should "return errors" do
192 should "return errors" do
193 assert_no_difference('User.count') do
193 assert_no_difference('User.count') do
194 post '/users.json', @parameters, credentials('admin')
194 post '/users.json', @parameters, credentials('admin')
195 end
195 end
196
196
197 assert_response :unprocessable_entity
197 assert_response :unprocessable_entity
198 assert_equal 'application/json', @response.content_type
198 assert_equal 'application/json', @response.content_type
199 json = ActiveSupport::JSON.decode(response.body)
199 json = ActiveSupport::JSON.decode(response.body)
200 assert_kind_of Hash, json
200 assert_kind_of Hash, json
201 assert json.has_key?('errors')
201 assert json.has_key?('errors')
202 assert_kind_of Array, json['errors']
202 assert_kind_of Array, json['errors']
203 end
203 end
204 end
204 end
205 end
205 end
206 end
206 end
207
207
208 context "PUT /users/2" do
208 context "PUT /users/2" do
209 context "with valid parameters" do
209 context "with valid parameters" do
210 setup do
210 setup do
211 @parameters = {
211 @parameters = {
212 :user => {
212 :user => {
213 :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
213 :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
214 :mail => 'jsmith@somenet.foo'
214 :mail => 'jsmith@somenet.foo'
215 }
215 }
216 }
216 }
217 end
217 end
218
218
219 context ".xml" do
219 context ".xml" do
220 should_allow_api_authentication(:put,
220 should_allow_api_authentication(:put,
221 '/users/2.xml',
221 '/users/2.xml',
222 {:user => {
222 {:user => {
223 :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
223 :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
224 :mail => 'jsmith@somenet.foo'
224 :mail => 'jsmith@somenet.foo'
225 }},
225 }},
226 {:success_code => :ok})
226 {:success_code => :ok})
227
227
228 should "update user with the attributes" do
228 should "update user with the attributes" do
229 assert_no_difference('User.count') do
229 assert_no_difference('User.count') do
230 put '/users/2.xml', @parameters, credentials('admin')
230 put '/users/2.xml', @parameters, credentials('admin')
231 end
231 end
232
232
233 user = User.find(2)
233 user = User.find(2)
234 assert_equal 'jsmith', user.login
234 assert_equal 'jsmith', user.login
235 assert_equal 'John', user.firstname
235 assert_equal 'John', user.firstname
236 assert_equal 'Renamed', user.lastname
236 assert_equal 'Renamed', user.lastname
237 assert_equal 'jsmith@somenet.foo', user.mail
237 assert_equal 'jsmith@somenet.foo', user.mail
238 assert !user.admin?
238 assert !user.admin?
239
239
240 assert_response :ok
240 assert_response :ok
241 assert_equal '', @response.body
241 end
242 end
242 end
243 end
243
244
244 context ".json" do
245 context ".json" do
245 should_allow_api_authentication(:put,
246 should_allow_api_authentication(:put,
246 '/users/2.json',
247 '/users/2.json',
247 {:user => {
248 {:user => {
248 :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
249 :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed',
249 :mail => 'jsmith@somenet.foo'
250 :mail => 'jsmith@somenet.foo'
250 }},
251 }},
251 {:success_code => :ok})
252 {:success_code => :ok})
252
253
253 should "update user with the attributes" do
254 should "update user with the attributes" do
254 assert_no_difference('User.count') do
255 assert_no_difference('User.count') do
255 put '/users/2.json', @parameters, credentials('admin')
256 put '/users/2.json', @parameters, credentials('admin')
256 end
257 end
257
258
258 user = User.find(2)
259 user = User.find(2)
259 assert_equal 'jsmith', user.login
260 assert_equal 'jsmith', user.login
260 assert_equal 'John', user.firstname
261 assert_equal 'John', user.firstname
261 assert_equal 'Renamed', user.lastname
262 assert_equal 'Renamed', user.lastname
262 assert_equal 'jsmith@somenet.foo', user.mail
263 assert_equal 'jsmith@somenet.foo', user.mail
263 assert !user.admin?
264 assert !user.admin?
264
265
265 assert_response :ok
266 assert_response :ok
267 assert_equal '', @response.body
266 end
268 end
267 end
269 end
268 end
270 end
269
271
270 context "with invalid parameters" do
272 context "with invalid parameters" do
271 setup do
273 setup do
272 @parameters = {
274 @parameters = {
273 :user => {
275 :user => {
274 :login => 'jsmith', :firstname => '', :lastname => 'Lastname',
276 :login => 'jsmith', :firstname => '', :lastname => 'Lastname',
275 :mail => 'foo'
277 :mail => 'foo'
276 }
278 }
277 }
279 }
278 end
280 end
279
281
280 context ".xml" do
282 context ".xml" do
281 should "return errors" do
283 should "return errors" do
282 assert_no_difference('User.count') do
284 assert_no_difference('User.count') do
283 put '/users/2.xml', @parameters, credentials('admin')
285 put '/users/2.xml', @parameters, credentials('admin')
284 end
286 end
285
287
286 assert_response :unprocessable_entity
288 assert_response :unprocessable_entity
287 assert_equal 'application/xml', @response.content_type
289 assert_equal 'application/xml', @response.content_type
288 assert_tag 'errors', :child => {
290 assert_tag 'errors', :child => {
289 :tag => 'error',
291 :tag => 'error',
290 :content => "First name can't be blank"
292 :content => "First name can't be blank"
291 }
293 }
292 end
294 end
293 end
295 end
294
296
295 context ".json" do
297 context ".json" do
296 should "return errors" do
298 should "return errors" do
297 assert_no_difference('User.count') do
299 assert_no_difference('User.count') do
298 put '/users/2.json', @parameters, credentials('admin')
300 put '/users/2.json', @parameters, credentials('admin')
299 end
301 end
300
302
301 assert_response :unprocessable_entity
303 assert_response :unprocessable_entity
302 assert_equal 'application/json', @response.content_type
304 assert_equal 'application/json', @response.content_type
303 json = ActiveSupport::JSON.decode(response.body)
305 json = ActiveSupport::JSON.decode(response.body)
304 assert_kind_of Hash, json
306 assert_kind_of Hash, json
305 assert json.has_key?('errors')
307 assert json.has_key?('errors')
306 assert_kind_of Array, json['errors']
308 assert_kind_of Array, json['errors']
307 end
309 end
308 end
310 end
309 end
311 end
310 end
312 end
311
313
312 context "DELETE /users/2" do
314 context "DELETE /users/2" do
313 context ".xml" do
315 context ".xml" do
314 should_allow_api_authentication(:delete,
316 should_allow_api_authentication(:delete,
315 '/users/2.xml',
317 '/users/2.xml',
316 {},
318 {},
317 {:success_code => :ok})
319 {:success_code => :ok})
318
320
319 should "delete user" do
321 should "delete user" do
320 assert_difference('User.count', -1) do
322 assert_difference('User.count', -1) do
321 delete '/users/2.xml', {}, credentials('admin')
323 delete '/users/2.xml', {}, credentials('admin')
322 end
324 end
323
325
324 assert_response :ok
326 assert_response :ok
327 assert_equal '', @response.body
325 end
328 end
326 end
329 end
327
330
328 context ".json" do
331 context ".json" do
329 should_allow_api_authentication(:delete,
332 should_allow_api_authentication(:delete,
330 '/users/2.xml',
333 '/users/2.xml',
331 {},
334 {},
332 {:success_code => :ok})
335 {:success_code => :ok})
333
336
334 should "delete user" do
337 should "delete user" do
335 assert_difference('User.count', -1) do
338 assert_difference('User.count', -1) do
336 delete '/users/2.json', {}, credentials('admin')
339 delete '/users/2.json', {}, credentials('admin')
337 end
340 end
338
341
339 assert_response :ok
342 assert_response :ok
343 assert_equal '', @response.body
340 end
344 end
341 end
345 end
342 end
346 end
343 end
347 end
@@ -1,138 +1,140
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApiTest::VersionsTest < ActionController::IntegrationTest
20 class ApiTest::VersionsTest < ActionController::IntegrationTest
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :enumerations, :users, :issue_categories,
22 :enumerations, :users, :issue_categories,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :workflows,
28 :workflows,
29 :versions
29 :versions
30
30
31 def setup
31 def setup
32 Setting.rest_api_enabled = '1'
32 Setting.rest_api_enabled = '1'
33 end
33 end
34
34
35 context "/projects/:project_id/versions" do
35 context "/projects/:project_id/versions" do
36 context "GET" do
36 context "GET" do
37 should "return project versions" do
37 should "return project versions" do
38 get '/projects/1/versions.xml'
38 get '/projects/1/versions.xml'
39
39
40 assert_response :success
40 assert_response :success
41 assert_equal 'application/xml', @response.content_type
41 assert_equal 'application/xml', @response.content_type
42 assert_tag :tag => 'versions',
42 assert_tag :tag => 'versions',
43 :attributes => {:type => 'array'},
43 :attributes => {:type => 'array'},
44 :child => {
44 :child => {
45 :tag => 'version',
45 :tag => 'version',
46 :child => {
46 :child => {
47 :tag => 'id',
47 :tag => 'id',
48 :content => '2',
48 :content => '2',
49 :sibling => {
49 :sibling => {
50 :tag => 'name',
50 :tag => 'name',
51 :content => '1.0'
51 :content => '1.0'
52 }
52 }
53 }
53 }
54 }
54 }
55 end
55 end
56 end
56 end
57
57
58 context "POST" do
58 context "POST" do
59 should "create the version" do
59 should "create the version" do
60 assert_difference 'Version.count' do
60 assert_difference 'Version.count' do
61 post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith')
61 post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith')
62 end
62 end
63
63
64 version = Version.first(:order => 'id DESC')
64 version = Version.first(:order => 'id DESC')
65 assert_equal 'API test', version.name
65 assert_equal 'API test', version.name
66
66
67 assert_response :created
67 assert_response :created
68 assert_equal 'application/xml', @response.content_type
68 assert_equal 'application/xml', @response.content_type
69 assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
69 assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
70 end
70 end
71
71
72 should "create the version with due date" do
72 should "create the version with due date" do
73 assert_difference 'Version.count' do
73 assert_difference 'Version.count' do
74 post '/projects/1/versions.xml', {:version => {:name => 'API test', :due_date => '2012-01-24'}}, credentials('jsmith')
74 post '/projects/1/versions.xml', {:version => {:name => 'API test', :due_date => '2012-01-24'}}, credentials('jsmith')
75 end
75 end
76
76
77 version = Version.first(:order => 'id DESC')
77 version = Version.first(:order => 'id DESC')
78 assert_equal 'API test', version.name
78 assert_equal 'API test', version.name
79 assert_equal Date.parse('2012-01-24'), version.due_date
79 assert_equal Date.parse('2012-01-24'), version.due_date
80
80
81 assert_response :created
81 assert_response :created
82 assert_equal 'application/xml', @response.content_type
82 assert_equal 'application/xml', @response.content_type
83 assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
83 assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s}
84 end
84 end
85
85
86 context "with failure" do
86 context "with failure" do
87 should "return the errors" do
87 should "return the errors" do
88 assert_no_difference('Version.count') do
88 assert_no_difference('Version.count') do
89 post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith')
89 post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith')
90 end
90 end
91
91
92 assert_response :unprocessable_entity
92 assert_response :unprocessable_entity
93 assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"}
93 assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"}
94 end
94 end
95 end
95 end
96 end
96 end
97 end
97 end
98
98
99 context "/versions/:id" do
99 context "/versions/:id" do
100 context "GET" do
100 context "GET" do
101 should "return the version" do
101 should "return the version" do
102 get '/versions/2.xml'
102 get '/versions/2.xml'
103
103
104 assert_response :success
104 assert_response :success
105 assert_equal 'application/xml', @response.content_type
105 assert_equal 'application/xml', @response.content_type
106 assert_tag 'version',
106 assert_tag 'version',
107 :child => {
107 :child => {
108 :tag => 'id',
108 :tag => 'id',
109 :content => '2',
109 :content => '2',
110 :sibling => {
110 :sibling => {
111 :tag => 'name',
111 :tag => 'name',
112 :content => '1.0'
112 :content => '1.0'
113 }
113 }
114 }
114 }
115 end
115 end
116 end
116 end
117
117
118 context "PUT" do
118 context "PUT" do
119 should "update the version" do
119 should "update the version" do
120 put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith')
120 put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith')
121
121
122 assert_response :ok
122 assert_response :ok
123 assert_equal '', @response.body
123 assert_equal 'API update', Version.find(2).name
124 assert_equal 'API update', Version.find(2).name
124 end
125 end
125 end
126 end
126
127
127 context "DELETE" do
128 context "DELETE" do
128 should "destroy the version" do
129 should "destroy the version" do
129 assert_difference 'Version.count', -1 do
130 assert_difference 'Version.count', -1 do
130 delete '/versions/3.xml', {}, credentials('jsmith')
131 delete '/versions/3.xml', {}, credentials('jsmith')
131 end
132 end
132
133
133 assert_response :ok
134 assert_response :ok
135 assert_equal '', @response.body
134 assert_nil Version.find_by_id(3)
136 assert_nil Version.find_by_id(3)
135 end
137 end
136 end
138 end
137 end
139 end
138 end
140 end
General Comments 0
You need to be logged in to leave comments. Login now