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