##// END OF EJS Templates
Don't pass conditions to #delete_all....
Jean-Philippe Lang -
r15293:57afa5345eea
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,678 +1,678
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 include Redmine::Pagination
26 26 include Redmine::Hook::Helper
27 27 include RoutesHelper
28 28 helper :routes
29 29
30 30 class_attribute :accept_api_auth_actions
31 31 class_attribute :accept_rss_auth_actions
32 32 class_attribute :model_object
33 33
34 34 layout 'base'
35 35
36 36 protect_from_forgery
37 37
38 38 def verify_authenticity_token
39 39 unless api_request?
40 40 super
41 41 end
42 42 end
43 43
44 44 def handle_unverified_request
45 45 unless api_request?
46 46 super
47 47 cookies.delete(autologin_cookie_name)
48 48 self.logged_user = nil
49 49 set_localization
50 50 render_error :status => 422, :message => "Invalid form authenticity token."
51 51 end
52 52 end
53 53
54 54 before_action :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
55 55
56 56 rescue_from ::Unauthorized, :with => :deny_access
57 57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
58 58
59 59 include Redmine::Search::Controller
60 60 include Redmine::MenuManager::MenuController
61 61 helper Redmine::MenuManager::MenuHelper
62 62
63 63 include Redmine::SudoMode::Controller
64 64
65 65 def session_expiration
66 66 if session[:user_id] && Rails.application.config.redmine_verify_sessions != false
67 67 if session_expired? && !try_to_autologin
68 68 set_localization(User.active.find_by_id(session[:user_id]))
69 69 self.logged_user = nil
70 70 flash[:error] = l(:error_session_expired)
71 71 require_login
72 72 end
73 73 end
74 74 end
75 75
76 76 def session_expired?
77 77 ! User.verify_session_token(session[:user_id], session[:tk])
78 78 end
79 79
80 80 def start_user_session(user)
81 81 session[:user_id] = user.id
82 82 session[:tk] = user.generate_session_token
83 83 if user.must_change_password?
84 84 session[:pwd] = '1'
85 85 end
86 86 end
87 87
88 88 def user_setup
89 89 # Check the settings cache for each request
90 90 Setting.check_cache
91 91 # Find the current user
92 92 User.current = find_current_user
93 93 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
94 94 end
95 95
96 96 # Returns the current user or nil if no user is logged in
97 97 # and starts a session if needed
98 98 def find_current_user
99 99 user = nil
100 100 unless api_request?
101 101 if session[:user_id]
102 102 # existing session
103 103 user = (User.active.find(session[:user_id]) rescue nil)
104 104 elsif autologin_user = try_to_autologin
105 105 user = autologin_user
106 106 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
107 107 # RSS key authentication does not start a session
108 108 user = User.find_by_rss_key(params[:key])
109 109 end
110 110 end
111 111 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
112 112 if (key = api_key_from_request)
113 113 # Use API key
114 114 user = User.find_by_api_key(key)
115 115 elsif request.authorization.to_s =~ /\ABasic /i
116 116 # HTTP Basic, either username/password or API key/random
117 117 authenticate_with_http_basic do |username, password|
118 118 user = User.try_to_login(username, password) || User.find_by_api_key(username)
119 119 end
120 120 if user && user.must_change_password?
121 121 render_error :message => 'You must change your password', :status => 403
122 122 return
123 123 end
124 124 end
125 125 # Switch user if requested by an admin user
126 126 if user && user.admin? && (username = api_switch_user_from_request)
127 127 su = User.find_by_login(username)
128 128 if su && su.active?
129 129 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
130 130 user = su
131 131 else
132 132 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
133 133 end
134 134 end
135 135 end
136 136 # store current ip address in user object ephemerally
137 137 user.remote_ip = request.remote_ip if user
138 138 user
139 139 end
140 140
141 141 def autologin_cookie_name
142 142 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
143 143 end
144 144
145 145 def try_to_autologin
146 146 if cookies[autologin_cookie_name] && Setting.autologin?
147 147 # auto-login feature starts a new session
148 148 user = User.try_to_autologin(cookies[autologin_cookie_name])
149 149 if user
150 150 reset_session
151 151 start_user_session(user)
152 152 end
153 153 user
154 154 end
155 155 end
156 156
157 157 # Sets the logged in user
158 158 def logged_user=(user)
159 159 reset_session
160 160 if user && user.is_a?(User)
161 161 User.current = user
162 162 start_user_session(user)
163 163 else
164 164 User.current = User.anonymous
165 165 end
166 166 end
167 167
168 168 # Logs out current user
169 169 def logout_user
170 170 if User.current.logged?
171 171 cookies.delete(autologin_cookie_name)
172 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
173 Token.delete_all(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]])
172 Token.where(["user_id = ? AND action = ?", User.current.id, 'autologin']).delete_all
173 Token.where(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]]).delete_all
174 174 self.logged_user = nil
175 175 end
176 176 end
177 177
178 178 # check if login is globally required to access the application
179 179 def check_if_login_required
180 180 # no check needed if user is already logged in
181 181 return true if User.current.logged?
182 182 require_login if Setting.login_required?
183 183 end
184 184
185 185 def check_password_change
186 186 if session[:pwd]
187 187 if User.current.must_change_password?
188 188 flash[:error] = l(:error_password_expired)
189 189 redirect_to my_password_path
190 190 else
191 191 session.delete(:pwd)
192 192 end
193 193 end
194 194 end
195 195
196 196 def set_localization(user=User.current)
197 197 lang = nil
198 198 if user && user.logged?
199 199 lang = find_language(user.language)
200 200 end
201 201 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
202 202 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
203 203 if !accept_lang.blank?
204 204 accept_lang = accept_lang.downcase
205 205 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
206 206 end
207 207 end
208 208 lang ||= Setting.default_language
209 209 set_language_if_valid(lang)
210 210 end
211 211
212 212 def require_login
213 213 if !User.current.logged?
214 214 # Extract only the basic url parameters on non-GET requests
215 215 if request.get?
216 216 url = request.original_url
217 217 else
218 218 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
219 219 end
220 220 respond_to do |format|
221 221 format.html {
222 222 if request.xhr?
223 223 head :unauthorized
224 224 else
225 225 redirect_to signin_path(:back_url => url)
226 226 end
227 227 }
228 228 format.any(:atom, :pdf, :csv) {
229 229 redirect_to signin_path(:back_url => url)
230 230 }
231 231 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
232 232 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
233 233 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
234 234 format.any { head :unauthorized }
235 235 end
236 236 return false
237 237 end
238 238 true
239 239 end
240 240
241 241 def require_admin
242 242 return unless require_login
243 243 if !User.current.admin?
244 244 render_403
245 245 return false
246 246 end
247 247 true
248 248 end
249 249
250 250 def deny_access
251 251 User.current.logged? ? render_403 : require_login
252 252 end
253 253
254 254 # Authorize the user for the requested action
255 255 def authorize(ctrl = params[:controller], action = params[:action], global = false)
256 256 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
257 257 if allowed
258 258 true
259 259 else
260 260 if @project && @project.archived?
261 261 render_403 :message => :notice_not_authorized_archived_project
262 262 else
263 263 deny_access
264 264 end
265 265 end
266 266 end
267 267
268 268 # Authorize the user for the requested action outside a project
269 269 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
270 270 authorize(ctrl, action, global)
271 271 end
272 272
273 273 # Find project of id params[:id]
274 274 def find_project
275 275 @project = Project.find(params[:id])
276 276 rescue ActiveRecord::RecordNotFound
277 277 render_404
278 278 end
279 279
280 280 # Find project of id params[:project_id]
281 281 def find_project_by_project_id
282 282 @project = Project.find(params[:project_id])
283 283 rescue ActiveRecord::RecordNotFound
284 284 render_404
285 285 end
286 286
287 287 # Find a project based on params[:project_id]
288 288 # TODO: some subclasses override this, see about merging their logic
289 289 def find_optional_project
290 290 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
291 291 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
292 292 allowed ? true : deny_access
293 293 rescue ActiveRecord::RecordNotFound
294 294 render_404
295 295 end
296 296
297 297 # Finds and sets @project based on @object.project
298 298 def find_project_from_association
299 299 render_404 unless @object.present?
300 300
301 301 @project = @object.project
302 302 end
303 303
304 304 def find_model_object
305 305 model = self.class.model_object
306 306 if model
307 307 @object = model.find(params[:id])
308 308 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
309 309 end
310 310 rescue ActiveRecord::RecordNotFound
311 311 render_404
312 312 end
313 313
314 314 def self.model_object(model)
315 315 self.model_object = model
316 316 end
317 317
318 318 # Find the issue whose id is the :id parameter
319 319 # Raises a Unauthorized exception if the issue is not visible
320 320 def find_issue
321 321 # Issue.visible.find(...) can not be used to redirect user to the login form
322 322 # if the issue actually exists but requires authentication
323 323 @issue = Issue.find(params[:id])
324 324 raise Unauthorized unless @issue.visible?
325 325 @project = @issue.project
326 326 rescue ActiveRecord::RecordNotFound
327 327 render_404
328 328 end
329 329
330 330 # Find issues with a single :id param or :ids array param
331 331 # Raises a Unauthorized exception if one of the issues is not visible
332 332 def find_issues
333 333 @issues = Issue.
334 334 where(:id => (params[:id] || params[:ids])).
335 335 preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to, {:custom_values => :custom_field}).
336 336 to_a
337 337 raise ActiveRecord::RecordNotFound if @issues.empty?
338 338 raise Unauthorized unless @issues.all?(&:visible?)
339 339 @projects = @issues.collect(&:project).compact.uniq
340 340 @project = @projects.first if @projects.size == 1
341 341 rescue ActiveRecord::RecordNotFound
342 342 render_404
343 343 end
344 344
345 345 def find_attachments
346 346 if (attachments = params[:attachments]).present?
347 347 att = attachments.values.collect do |attachment|
348 348 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
349 349 end
350 350 att.compact!
351 351 end
352 352 @attachments = att || []
353 353 end
354 354
355 355 def parse_params_for_bulk_update(params)
356 356 attributes = (params || {}).reject {|k,v| v.blank?}
357 357 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
358 358 if custom = attributes[:custom_field_values]
359 359 custom.reject! {|k,v| v.blank?}
360 360 custom.keys.each do |k|
361 361 if custom[k].is_a?(Array)
362 362 custom[k] << '' if custom[k].delete('__none__')
363 363 else
364 364 custom[k] = '' if custom[k] == '__none__'
365 365 end
366 366 end
367 367 end
368 368 attributes
369 369 end
370 370
371 371 # make sure that the user is a member of the project (or admin) if project is private
372 372 # used as a before_action for actions that do not require any particular permission on the project
373 373 def check_project_privacy
374 374 if @project && !@project.archived?
375 375 if @project.visible?
376 376 true
377 377 else
378 378 deny_access
379 379 end
380 380 else
381 381 @project = nil
382 382 render_404
383 383 false
384 384 end
385 385 end
386 386
387 387 def back_url
388 388 url = params[:back_url]
389 389 if url.nil? && referer = request.env['HTTP_REFERER']
390 390 url = CGI.unescape(referer.to_s)
391 391 end
392 392 url
393 393 end
394 394
395 395 def redirect_back_or_default(default, options={})
396 396 back_url = params[:back_url].to_s
397 397 if back_url.present? && valid_url = validate_back_url(back_url)
398 398 redirect_to(valid_url)
399 399 return
400 400 elsif options[:referer]
401 401 redirect_to_referer_or default
402 402 return
403 403 end
404 404 redirect_to default
405 405 false
406 406 end
407 407
408 408 # Returns a validated URL string if back_url is a valid url for redirection,
409 409 # otherwise false
410 410 def validate_back_url(back_url)
411 411 if CGI.unescape(back_url).include?('..')
412 412 return false
413 413 end
414 414
415 415 begin
416 416 uri = URI.parse(back_url)
417 417 rescue URI::InvalidURIError
418 418 return false
419 419 end
420 420
421 421 [:scheme, :host, :port].each do |component|
422 422 if uri.send(component).present? && uri.send(component) != request.send(component)
423 423 return false
424 424 end
425 425 uri.send(:"#{component}=", nil)
426 426 end
427 427 # Always ignore basic user:password in the URL
428 428 uri.userinfo = nil
429 429
430 430 path = uri.to_s
431 431 # Ensure that the remaining URL starts with a slash, followed by a
432 432 # non-slash character or the end
433 433 if path !~ %r{\A/([^/]|\z)}
434 434 return false
435 435 end
436 436
437 437 if path.match(%r{/(login|account/register)})
438 438 return false
439 439 end
440 440
441 441 if relative_url_root.present? && !path.starts_with?(relative_url_root)
442 442 return false
443 443 end
444 444
445 445 return path
446 446 end
447 447 private :validate_back_url
448 448
449 449 def valid_back_url?(back_url)
450 450 !!validate_back_url(back_url)
451 451 end
452 452 private :valid_back_url?
453 453
454 454 # Redirects to the request referer if present, redirects to args or call block otherwise.
455 455 def redirect_to_referer_or(*args, &block)
456 456 redirect_to :back
457 457 rescue ::ActionController::RedirectBackError
458 458 if args.any?
459 459 redirect_to *args
460 460 elsif block_given?
461 461 block.call
462 462 else
463 463 raise "#redirect_to_referer_or takes arguments or a block"
464 464 end
465 465 end
466 466
467 467 def render_403(options={})
468 468 @project = nil
469 469 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
470 470 return false
471 471 end
472 472
473 473 def render_404(options={})
474 474 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
475 475 return false
476 476 end
477 477
478 478 # Renders an error response
479 479 def render_error(arg)
480 480 arg = {:message => arg} unless arg.is_a?(Hash)
481 481
482 482 @message = arg[:message]
483 483 @message = l(@message) if @message.is_a?(Symbol)
484 484 @status = arg[:status] || 500
485 485
486 486 respond_to do |format|
487 487 format.html {
488 488 render :template => 'common/error', :layout => use_layout, :status => @status
489 489 }
490 490 format.any { head @status }
491 491 end
492 492 end
493 493
494 494 # Handler for ActionView::MissingTemplate exception
495 495 def missing_template
496 496 logger.warn "Missing template, responding with 404"
497 497 @project = nil
498 498 render_404
499 499 end
500 500
501 501 # Filter for actions that provide an API response
502 502 # but have no HTML representation for non admin users
503 503 def require_admin_or_api_request
504 504 return true if api_request?
505 505 if User.current.admin?
506 506 true
507 507 elsif User.current.logged?
508 508 render_error(:status => 406)
509 509 else
510 510 deny_access
511 511 end
512 512 end
513 513
514 514 # Picks which layout to use based on the request
515 515 #
516 516 # @return [boolean, string] name of the layout to use or false for no layout
517 517 def use_layout
518 518 request.xhr? ? false : 'base'
519 519 end
520 520
521 521 def render_feed(items, options={})
522 522 @items = (items || []).to_a
523 523 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
524 524 @items = @items.slice(0, Setting.feeds_limit.to_i)
525 525 @title = options[:title] || Setting.app_title
526 526 render :template => "common/feed", :formats => [:atom], :layout => false,
527 527 :content_type => 'application/atom+xml'
528 528 end
529 529
530 530 def self.accept_rss_auth(*actions)
531 531 if actions.any?
532 532 self.accept_rss_auth_actions = actions
533 533 else
534 534 self.accept_rss_auth_actions || []
535 535 end
536 536 end
537 537
538 538 def accept_rss_auth?(action=action_name)
539 539 self.class.accept_rss_auth.include?(action.to_sym)
540 540 end
541 541
542 542 def self.accept_api_auth(*actions)
543 543 if actions.any?
544 544 self.accept_api_auth_actions = actions
545 545 else
546 546 self.accept_api_auth_actions || []
547 547 end
548 548 end
549 549
550 550 def accept_api_auth?(action=action_name)
551 551 self.class.accept_api_auth.include?(action.to_sym)
552 552 end
553 553
554 554 # Returns the number of objects that should be displayed
555 555 # on the paginated list
556 556 def per_page_option
557 557 per_page = nil
558 558 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
559 559 per_page = params[:per_page].to_s.to_i
560 560 session[:per_page] = per_page
561 561 elsif session[:per_page]
562 562 per_page = session[:per_page]
563 563 else
564 564 per_page = Setting.per_page_options_array.first || 25
565 565 end
566 566 per_page
567 567 end
568 568
569 569 # Returns offset and limit used to retrieve objects
570 570 # for an API response based on offset, limit and page parameters
571 571 def api_offset_and_limit(options=params)
572 572 if options[:offset].present?
573 573 offset = options[:offset].to_i
574 574 if offset < 0
575 575 offset = 0
576 576 end
577 577 end
578 578 limit = options[:limit].to_i
579 579 if limit < 1
580 580 limit = 25
581 581 elsif limit > 100
582 582 limit = 100
583 583 end
584 584 if offset.nil? && options[:page].present?
585 585 offset = (options[:page].to_i - 1) * limit
586 586 offset = 0 if offset < 0
587 587 end
588 588 offset ||= 0
589 589
590 590 [offset, limit]
591 591 end
592 592
593 593 # qvalues http header parser
594 594 # code taken from webrick
595 595 def parse_qvalues(value)
596 596 tmp = []
597 597 if value
598 598 parts = value.split(/,\s*/)
599 599 parts.each {|part|
600 600 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
601 601 val = m[1]
602 602 q = (m[2] or 1).to_f
603 603 tmp.push([val, q])
604 604 end
605 605 }
606 606 tmp = tmp.sort_by{|val, q| -q}
607 607 tmp.collect!{|val, q| val}
608 608 end
609 609 return tmp
610 610 rescue
611 611 nil
612 612 end
613 613
614 614 # Returns a string that can be used as filename value in Content-Disposition header
615 615 def filename_for_content_disposition(name)
616 616 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident|Edge)} ? ERB::Util.url_encode(name) : name
617 617 end
618 618
619 619 def api_request?
620 620 %w(xml json).include? params[:format]
621 621 end
622 622
623 623 # Returns the API key present in the request
624 624 def api_key_from_request
625 625 if params[:key].present?
626 626 params[:key].to_s
627 627 elsif request.headers["X-Redmine-API-Key"].present?
628 628 request.headers["X-Redmine-API-Key"].to_s
629 629 end
630 630 end
631 631
632 632 # Returns the API 'switch user' value if present
633 633 def api_switch_user_from_request
634 634 request.headers["X-Redmine-Switch-User"].to_s.presence
635 635 end
636 636
637 637 # Renders a warning flash if obj has unsaved attachments
638 638 def render_attachment_warning_if_needed(obj)
639 639 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
640 640 end
641 641
642 642 # Rescues an invalid query statement. Just in case...
643 643 def query_statement_invalid(exception)
644 644 logger.error "Query::StatementInvalid: #{exception.message}" if logger
645 645 session.delete(:query)
646 646 sort_clear if respond_to?(:sort_clear)
647 647 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
648 648 end
649 649
650 650 # Renders a 200 response for successfull updates or deletions via the API
651 651 def render_api_ok
652 652 render_api_head :ok
653 653 end
654 654
655 655 # Renders a head API response
656 656 def render_api_head(status)
657 657 # #head would return a response body with one space
658 658 render :text => '', :status => status, :layout => nil
659 659 end
660 660
661 661 # Renders API response on validation failure
662 662 # for an object or an array of objects
663 663 def render_validation_errors(objects)
664 664 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
665 665 render_api_errors(messages)
666 666 end
667 667
668 668 def render_api_errors(*messages)
669 669 @error_messages = messages.flatten
670 670 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
671 671 end
672 672
673 673 # Overrides #_include_layout? so that #render with no arguments
674 674 # doesn't use the layout for api requests
675 675 def _include_layout?(*args)
676 676 api_request? ? false : super
677 677 end
678 678 end
@@ -1,113 +1,113
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 IssueStatus < ActiveRecord::Base
19 19 before_destroy :check_integrity
20 20 has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id"
21 21 has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id"
22 22 acts_as_positioned
23 23
24 24 after_update :handle_is_closed_change
25 25 before_destroy :delete_workflow_rules
26 26
27 27 validates_presence_of :name
28 28 validates_uniqueness_of :name
29 29 validates_length_of :name, :maximum => 30
30 30 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
31 31 attr_protected :id
32 32
33 33 scope :sorted, lambda { order(:position) }
34 34 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
35 35
36 36 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
37 37 def self.update_issue_done_ratios
38 38 if Issue.use_status_for_done_ratio?
39 39 IssueStatus.where("default_done_ratio >= 0").each do |status|
40 40 Issue.where({:status_id => status.id}).update_all({:done_ratio => status.default_done_ratio})
41 41 end
42 42 end
43 43
44 44 return Issue.use_status_for_done_ratio?
45 45 end
46 46
47 47 # Returns an array of all statuses the given role can switch to
48 48 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
49 49 self.class.new_statuses_allowed(self, roles, tracker, author, assignee)
50 50 end
51 51 alias :find_new_statuses_allowed_to :new_statuses_allowed_to
52 52
53 53 def self.new_statuses_allowed(status, roles, tracker, author=false, assignee=false)
54 54 if roles.present? && tracker
55 55 status_id = status.try(:id) || 0
56 56
57 57 scope = IssueStatus.
58 58 joins(:workflow_transitions_as_new_status).
59 59 where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id})
60 60
61 61 unless author && assignee
62 62 if author || assignee
63 63 scope = scope.where("author = ? OR assignee = ?", author, assignee)
64 64 else
65 65 scope = scope.where("author = ? AND assignee = ?", false, false)
66 66 end
67 67 end
68 68
69 69 scope.distinct.to_a.sort
70 70 else
71 71 []
72 72 end
73 73 end
74 74
75 75 def <=>(status)
76 76 position <=> status.position
77 77 end
78 78
79 79 def to_s; name end
80 80
81 81 private
82 82
83 83 # Updates issues closed_on attribute when an existing status is set as closed.
84 84 def handle_is_closed_change
85 85 if is_closed_changed? && is_closed == true
86 86 # First we update issues that have a journal for when the current status was set,
87 87 # a subselect is used to update all issues with a single query
88 88 subselect = "SELECT MAX(j.created_on) FROM #{Journal.table_name} j" +
89 89 " JOIN #{JournalDetail.table_name} d ON d.journal_id = j.id" +
90 90 " WHERE j.journalized_type = 'Issue' AND j.journalized_id = #{Issue.table_name}.id" +
91 91 " AND d.property = 'attr' AND d.prop_key = 'status_id' AND d.value = :status_id"
92 92 Issue.where(:status_id => id, :closed_on => nil).
93 93 update_all(["closed_on = (#{subselect})", {:status_id => id.to_s}])
94 94
95 95 # Then we update issues that don't have a journal which means the
96 96 # current status was set on creation
97 97 Issue.where(:status_id => id, :closed_on => nil).update_all("closed_on = created_on")
98 98 end
99 99 end
100 100
101 101 def check_integrity
102 102 if Issue.where(:status_id => id).any?
103 103 raise "This status is used by some issues"
104 104 elsif Tracker.where(:default_status_id => id).any?
105 105 raise "This status is used as the default status by some trackers"
106 106 end
107 107 end
108 108
109 109 # Deletes associated workflows
110 110 def delete_workflow_rules
111 WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
111 WorkflowRule.where(["old_status_id = :id OR new_status_id = :id", {:id => id}]).delete_all
112 112 end
113 113 end
@@ -1,1066 +1,1066
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 include Redmine::NestedSet::ProjectNestedSet
21 21
22 22 # Project statuses
23 23 STATUS_ACTIVE = 1
24 24 STATUS_CLOSED = 5
25 25 STATUS_ARCHIVED = 9
26 26
27 27 # Maximum length for project identifiers
28 28 IDENTIFIER_MAX_LENGTH = 100
29 29
30 30 # Specific overridden Activities
31 31 has_many :time_entry_activities
32 32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
33 33 # Memberships of active users only
34 34 has_many :members,
35 35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
36 36 has_many :enabled_modules, :dependent => :delete_all
37 37 has_and_belongs_to_many :trackers, lambda {order(:position)}
38 38 has_many :issues, :dependent => :destroy
39 39 has_many :issue_changes, :through => :issues, :source => :journals
40 40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 41 belongs_to :default_version, :class_name => 'Version'
42 42 has_many :time_entries, :dependent => :destroy
43 43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 44 has_many :documents, :dependent => :destroy
45 45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
46 46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
47 47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
48 48 has_one :repository, lambda {where(["is_default = ?", true])}
49 49 has_many :repositories, :dependent => :destroy
50 50 has_many :changesets, :through => :repository
51 51 has_one :wiki, :dependent => :destroy
52 52 # Custom field for the project issues
53 53 has_and_belongs_to_many :issue_custom_fields,
54 54 lambda {order("#{CustomField.table_name}.position")},
55 55 :class_name => 'IssueCustomField',
56 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 57 :association_foreign_key => 'custom_field_id'
58 58
59 59 acts_as_attachable :view_permission => :view_files,
60 60 :edit_permission => :manage_files,
61 61 :delete_permission => :manage_files
62 62
63 63 acts_as_customizable
64 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
65 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 67 :author => nil
68 68
69 69 attr_protected :status
70 70
71 71 validates_presence_of :name, :identifier
72 72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
73 73 validates_length_of :name, :maximum => 255
74 74 validates_length_of :homepage, :maximum => 255
75 75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 76 # downcase letters, digits, dashes but not digits only
77 77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 78 # reserved words
79 79 validates_exclusion_of :identifier, :in => %w( new )
80 80 validate :validate_parent
81 81
82 82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
84 84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
85 85 before_destroy :delete_all_members
86 86
87 87 scope :has_module, lambda {|mod|
88 88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 89 }
90 90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 92 scope :all_public, lambda { where(:is_public => true) }
93 93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 94 scope :allowed_to, lambda {|*args|
95 95 user = User.current
96 96 permission = nil
97 97 if args.first.is_a?(Symbol)
98 98 permission = args.shift
99 99 else
100 100 user = args.shift
101 101 permission = args.shift
102 102 end
103 103 where(Project.allowed_to_condition(user, permission, *args))
104 104 }
105 105 scope :like, lambda {|arg|
106 106 if arg.blank?
107 107 where(nil)
108 108 else
109 109 pattern = "%#{arg.to_s.strip.downcase}%"
110 110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 111 end
112 112 }
113 113 scope :sorted, lambda {order(:lft)}
114 114 scope :having_trackers, lambda {
115 115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
116 116 }
117 117
118 118 def initialize(attributes=nil, *args)
119 119 super
120 120
121 121 initialized = (attributes || {}).stringify_keys
122 122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
123 123 self.identifier = Project.next_identifier
124 124 end
125 125 if !initialized.key?('is_public')
126 126 self.is_public = Setting.default_projects_public?
127 127 end
128 128 if !initialized.key?('enabled_module_names')
129 129 self.enabled_module_names = Setting.default_projects_modules
130 130 end
131 131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
132 132 default = Setting.default_projects_tracker_ids
133 133 if default.is_a?(Array)
134 134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
135 135 else
136 136 self.trackers = Tracker.sorted.to_a
137 137 end
138 138 end
139 139 end
140 140
141 141 def identifier=(identifier)
142 142 super unless identifier_frozen?
143 143 end
144 144
145 145 def identifier_frozen?
146 146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
147 147 end
148 148
149 149 # returns latest created projects
150 150 # non public projects will be returned only if user is a member of those
151 151 def self.latest(user=nil, count=5)
152 152 visible(user).limit(count).
153 153 order(:created_on => :desc).
154 154 where("#{table_name}.created_on >= ?", 30.days.ago).
155 155 to_a
156 156 end
157 157
158 158 # Returns true if the project is visible to +user+ or to the current user.
159 159 def visible?(user=User.current)
160 160 user.allowed_to?(:view_project, self)
161 161 end
162 162
163 163 # Returns a SQL conditions string used to find all projects visible by the specified user.
164 164 #
165 165 # Examples:
166 166 # Project.visible_condition(admin) => "projects.status = 1"
167 167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
168 168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
169 169 def self.visible_condition(user, options={})
170 170 allowed_to_condition(user, :view_project, options)
171 171 end
172 172
173 173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
174 174 #
175 175 # Valid options:
176 176 # * :project => limit the condition to project
177 177 # * :with_subprojects => limit the condition to project and its subprojects
178 178 # * :member => limit the condition to the user projects
179 179 def self.allowed_to_condition(user, permission, options={})
180 180 perm = Redmine::AccessControl.permission(permission)
181 181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
182 182 if perm && perm.project_module
183 183 # If the permission belongs to a project module, make sure the module is enabled
184 184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
185 185 end
186 186 if project = options[:project]
187 187 project_statement = project.project_condition(options[:with_subprojects])
188 188 base_statement = "(#{project_statement}) AND (#{base_statement})"
189 189 end
190 190
191 191 if user.admin?
192 192 base_statement
193 193 else
194 194 statement_by_role = {}
195 195 unless options[:member]
196 196 role = user.builtin_role
197 197 if role.allowed_to?(permission)
198 198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
199 199 if user.id
200 200 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
201 201 end
202 202 statement_by_role[role] = s
203 203 end
204 204 end
205 205 user.projects_by_role.each do |role, projects|
206 206 if role.allowed_to?(permission) && projects.any?
207 207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
208 208 end
209 209 end
210 210 if statement_by_role.empty?
211 211 "1=0"
212 212 else
213 213 if block_given?
214 214 statement_by_role.each do |role, statement|
215 215 if s = yield(role, user)
216 216 statement_by_role[role] = "(#{statement} AND (#{s}))"
217 217 end
218 218 end
219 219 end
220 220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
221 221 end
222 222 end
223 223 end
224 224
225 225 def override_roles(role)
226 226 @override_members ||= memberships.
227 227 joins(:principal).
228 228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
229 229
230 230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
231 231 member = @override_members.detect {|m| m.principal.is_a? group_class}
232 232 member ? member.roles.to_a : [role]
233 233 end
234 234
235 235 def principals
236 236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
237 237 end
238 238
239 239 def users
240 240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).distinct
241 241 end
242 242
243 243 # Returns the Systemwide and project specific activities
244 244 def activities(include_inactive=false)
245 245 t = TimeEntryActivity.table_name
246 246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
247 247
248 248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
249 249 if overridden_activity_ids.any?
250 250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
251 251 end
252 252 unless include_inactive
253 253 scope = scope.active
254 254 end
255 255 scope
256 256 end
257 257
258 258 # Will create a new Project specific Activity or update an existing one
259 259 #
260 260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
261 261 # does not successfully save.
262 262 def update_or_create_time_entry_activity(id, activity_hash)
263 263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
264 264 self.create_time_entry_activity_if_needed(activity_hash)
265 265 else
266 266 activity = project.time_entry_activities.find_by_id(id.to_i)
267 267 activity.update_attributes(activity_hash) if activity
268 268 end
269 269 end
270 270
271 271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
272 272 #
273 273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
274 274 # does not successfully save.
275 275 def create_time_entry_activity_if_needed(activity)
276 276 if activity['parent_id']
277 277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
278 278 activity['name'] = parent_activity.name
279 279 activity['position'] = parent_activity.position
280 280 if Enumeration.overriding_change?(activity, parent_activity)
281 281 project_activity = self.time_entry_activities.create(activity)
282 282 if project_activity.new_record?
283 283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
284 284 else
285 285 self.time_entries.
286 286 where(:activity_id => parent_activity.id).
287 287 update_all(:activity_id => project_activity.id)
288 288 end
289 289 end
290 290 end
291 291 end
292 292
293 293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
294 294 #
295 295 # Examples:
296 296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
297 297 # project.project_condition(false) => "projects.id = 1"
298 298 def project_condition(with_subprojects)
299 299 cond = "#{Project.table_name}.id = #{id}"
300 300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
301 301 cond
302 302 end
303 303
304 304 def self.find(*args)
305 305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
306 306 project = find_by_identifier(*args)
307 307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
308 308 project
309 309 else
310 310 super
311 311 end
312 312 end
313 313
314 314 def self.find_by_param(*args)
315 315 self.find(*args)
316 316 end
317 317
318 318 alias :base_reload :reload
319 319 def reload(*args)
320 320 @principals = nil
321 321 @users = nil
322 322 @shared_versions = nil
323 323 @rolled_up_versions = nil
324 324 @rolled_up_trackers = nil
325 325 @all_issue_custom_fields = nil
326 326 @all_time_entry_custom_fields = nil
327 327 @to_param = nil
328 328 @allowed_parents = nil
329 329 @allowed_permissions = nil
330 330 @actions_allowed = nil
331 331 @start_date = nil
332 332 @due_date = nil
333 333 @override_members = nil
334 334 @assignable_users = nil
335 335 base_reload(*args)
336 336 end
337 337
338 338 def to_param
339 339 if new_record?
340 340 nil
341 341 else
342 342 # id is used for projects with a numeric identifier (compatibility)
343 343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
344 344 end
345 345 end
346 346
347 347 def active?
348 348 self.status == STATUS_ACTIVE
349 349 end
350 350
351 351 def archived?
352 352 self.status == STATUS_ARCHIVED
353 353 end
354 354
355 355 # Archives the project and its descendants
356 356 def archive
357 357 # Check that there is no issue of a non descendant project that is assigned
358 358 # to one of the project or descendant versions
359 359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
360 360
361 361 if version_ids.any? &&
362 362 Issue.
363 363 includes(:project).
364 364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
365 365 where(:fixed_version_id => version_ids).
366 366 exists?
367 367 return false
368 368 end
369 369 Project.transaction do
370 370 archive!
371 371 end
372 372 true
373 373 end
374 374
375 375 # Unarchives the project
376 376 # All its ancestors must be active
377 377 def unarchive
378 378 return false if ancestors.detect {|a| !a.active?}
379 379 update_attribute :status, STATUS_ACTIVE
380 380 end
381 381
382 382 def close
383 383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
384 384 end
385 385
386 386 def reopen
387 387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
388 388 end
389 389
390 390 # Returns an array of projects the project can be moved to
391 391 # by the current user
392 392 def allowed_parents(user=User.current)
393 393 return @allowed_parents if @allowed_parents
394 394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
395 395 @allowed_parents = @allowed_parents - self_and_descendants
396 396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
397 397 @allowed_parents << nil
398 398 end
399 399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
400 400 @allowed_parents << parent
401 401 end
402 402 @allowed_parents
403 403 end
404 404
405 405 # Sets the parent of the project with authorization check
406 406 def set_allowed_parent!(p)
407 407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
408 408 p = p.id if p.is_a?(Project)
409 409 send :safe_attributes, {:project_id => p}
410 410 save
411 411 end
412 412
413 413 # Sets the parent of the project and saves the project
414 414 # Argument can be either a Project, a String, a Fixnum or nil
415 415 def set_parent!(p)
416 416 if p.is_a?(Project)
417 417 self.parent = p
418 418 else
419 419 self.parent_id = p
420 420 end
421 421 save
422 422 end
423 423
424 424 # Returns a scope of the trackers used by the project and its active sub projects
425 425 def rolled_up_trackers(include_subprojects=true)
426 426 if include_subprojects
427 427 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
428 428 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
429 429 else
430 430 rolled_up_trackers_base_scope.
431 431 where(:projects => {:id => id})
432 432 end
433 433 end
434 434
435 435 def rolled_up_trackers_base_scope
436 436 Tracker.
437 437 joins(projects: :enabled_modules).
438 438 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
439 439 where(:enabled_modules => {:name => 'issue_tracking'}).
440 440 distinct.
441 441 sorted
442 442 end
443 443
444 444 # Closes open and locked project versions that are completed
445 445 def close_completed_versions
446 446 Version.transaction do
447 447 versions.where(:status => %w(open locked)).each do |version|
448 448 if version.completed?
449 449 version.update_attribute(:status, 'closed')
450 450 end
451 451 end
452 452 end
453 453 end
454 454
455 455 # Returns a scope of the Versions on subprojects
456 456 def rolled_up_versions
457 457 @rolled_up_versions ||=
458 458 Version.
459 459 joins(:project).
460 460 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
461 461 end
462 462
463 463 # Returns a scope of the Versions used by the project
464 464 def shared_versions
465 465 if new_record?
466 466 Version.
467 467 joins(:project).
468 468 preload(:project).
469 469 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
470 470 else
471 471 @shared_versions ||= begin
472 472 r = root? ? self : root
473 473 Version.
474 474 joins(:project).
475 475 preload(:project).
476 476 where("#{Project.table_name}.id = #{id}" +
477 477 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
478 478 " #{Version.table_name}.sharing = 'system'" +
479 479 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
480 480 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
481 481 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
482 482 "))")
483 483 end
484 484 end
485 485 end
486 486
487 487 # Returns a hash of project users grouped by role
488 488 def users_by_role
489 489 members.includes(:user, :roles).inject({}) do |h, m|
490 490 m.roles.each do |r|
491 491 h[r] ||= []
492 492 h[r] << m.user
493 493 end
494 494 h
495 495 end
496 496 end
497 497
498 498 # Adds user as a project member with the default role
499 499 # Used for when a non-admin user creates a project
500 500 def add_default_member(user)
501 501 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
502 502 member = Member.new(:project => self, :principal => user, :roles => [role])
503 503 self.members << member
504 504 member
505 505 end
506 506
507 507 # Deletes all project's members
508 508 def delete_all_members
509 509 me, mr = Member.table_name, MemberRole.table_name
510 510 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
511 Member.delete_all(['project_id = ?', id])
511 Member.where(:project_id => id).delete_all
512 512 end
513 513
514 514 # Return a Principal scope of users/groups issues can be assigned to
515 515 def assignable_users(tracker=nil)
516 516 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
517 517
518 518 types = ['User']
519 519 types << 'Group' if Setting.issue_group_assignment?
520 520
521 521 scope = Principal.
522 522 active.
523 523 joins(:members => :roles).
524 524 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
525 525 distinct.
526 526 sorted
527 527
528 528 if tracker
529 529 # Rejects users that cannot the view the tracker
530 530 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
531 531 scope = scope.where(:roles => {:id => roles.map(&:id)})
532 532 end
533 533
534 534 @assignable_users ||= {}
535 535 @assignable_users[tracker] = scope
536 536 end
537 537
538 538 # Returns the mail addresses of users that should be always notified on project events
539 539 def recipients
540 540 notified_users.collect {|user| user.mail}
541 541 end
542 542
543 543 # Returns the users that should be notified on project events
544 544 def notified_users
545 545 # TODO: User part should be extracted to User#notify_about?
546 546 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
547 547 end
548 548
549 549 # Returns a scope of all custom fields enabled for project issues
550 550 # (explicitly associated custom fields and custom fields enabled for all projects)
551 551 def all_issue_custom_fields
552 552 if new_record?
553 553 @all_issue_custom_fields ||= IssueCustomField.
554 554 sorted.
555 555 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
556 556 else
557 557 @all_issue_custom_fields ||= IssueCustomField.
558 558 sorted.
559 559 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
560 560 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
561 561 " WHERE cfp.project_id = ?)", true, id)
562 562 end
563 563 end
564 564
565 565 def project
566 566 self
567 567 end
568 568
569 569 def <=>(project)
570 570 name.casecmp(project.name)
571 571 end
572 572
573 573 def to_s
574 574 name
575 575 end
576 576
577 577 # Returns a short description of the projects (first lines)
578 578 def short_description(length = 255)
579 579 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
580 580 end
581 581
582 582 def css_classes
583 583 s = 'project'
584 584 s << ' root' if root?
585 585 s << ' child' if child?
586 586 s << (leaf? ? ' leaf' : ' parent')
587 587 unless active?
588 588 if archived?
589 589 s << ' archived'
590 590 else
591 591 s << ' closed'
592 592 end
593 593 end
594 594 s
595 595 end
596 596
597 597 # The earliest start date of a project, based on it's issues and versions
598 598 def start_date
599 599 @start_date ||= [
600 600 issues.minimum('start_date'),
601 601 shared_versions.minimum('effective_date'),
602 602 Issue.fixed_version(shared_versions).minimum('start_date')
603 603 ].compact.min
604 604 end
605 605
606 606 # The latest due date of an issue or version
607 607 def due_date
608 608 @due_date ||= [
609 609 issues.maximum('due_date'),
610 610 shared_versions.maximum('effective_date'),
611 611 Issue.fixed_version(shared_versions).maximum('due_date')
612 612 ].compact.max
613 613 end
614 614
615 615 def overdue?
616 616 active? && !due_date.nil? && (due_date < User.current.today)
617 617 end
618 618
619 619 # Returns the percent completed for this project, based on the
620 620 # progress on it's versions.
621 621 def completed_percent(options={:include_subprojects => false})
622 622 if options.delete(:include_subprojects)
623 623 total = self_and_descendants.collect(&:completed_percent).sum
624 624
625 625 total / self_and_descendants.count
626 626 else
627 627 if versions.count > 0
628 628 total = versions.collect(&:completed_percent).sum
629 629
630 630 total / versions.count
631 631 else
632 632 100
633 633 end
634 634 end
635 635 end
636 636
637 637 # Return true if this project allows to do the specified action.
638 638 # action can be:
639 639 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
640 640 # * a permission Symbol (eg. :edit_project)
641 641 def allows_to?(action)
642 642 if archived?
643 643 # No action allowed on archived projects
644 644 return false
645 645 end
646 646 unless active? || Redmine::AccessControl.read_action?(action)
647 647 # No write action allowed on closed projects
648 648 return false
649 649 end
650 650 # No action allowed on disabled modules
651 651 if action.is_a? Hash
652 652 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
653 653 else
654 654 allowed_permissions.include? action
655 655 end
656 656 end
657 657
658 658 # Return the enabled module with the given name
659 659 # or nil if the module is not enabled for the project
660 660 def enabled_module(name)
661 661 name = name.to_s
662 662 enabled_modules.detect {|m| m.name == name}
663 663 end
664 664
665 665 # Return true if the module with the given name is enabled
666 666 def module_enabled?(name)
667 667 enabled_module(name).present?
668 668 end
669 669
670 670 def enabled_module_names=(module_names)
671 671 if module_names && module_names.is_a?(Array)
672 672 module_names = module_names.collect(&:to_s).reject(&:blank?)
673 673 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
674 674 else
675 675 enabled_modules.clear
676 676 end
677 677 end
678 678
679 679 # Returns an array of the enabled modules names
680 680 def enabled_module_names
681 681 enabled_modules.collect(&:name)
682 682 end
683 683
684 684 # Enable a specific module
685 685 #
686 686 # Examples:
687 687 # project.enable_module!(:issue_tracking)
688 688 # project.enable_module!("issue_tracking")
689 689 def enable_module!(name)
690 690 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
691 691 end
692 692
693 693 # Disable a module if it exists
694 694 #
695 695 # Examples:
696 696 # project.disable_module!(:issue_tracking)
697 697 # project.disable_module!("issue_tracking")
698 698 # project.disable_module!(project.enabled_modules.first)
699 699 def disable_module!(target)
700 700 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
701 701 target.destroy unless target.blank?
702 702 end
703 703
704 704 safe_attributes 'name',
705 705 'description',
706 706 'homepage',
707 707 'is_public',
708 708 'identifier',
709 709 'custom_field_values',
710 710 'custom_fields',
711 711 'tracker_ids',
712 712 'issue_custom_field_ids',
713 713 'parent_id',
714 714 'default_version_id'
715 715
716 716 safe_attributes 'enabled_module_names',
717 717 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
718 718
719 719 safe_attributes 'inherit_members',
720 720 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
721 721
722 722 def safe_attributes=(attrs, user=User.current)
723 723 return unless attrs.is_a?(Hash)
724 724 attrs = attrs.deep_dup
725 725
726 726 @unallowed_parent_id = nil
727 727 if new_record? || attrs.key?('parent_id')
728 728 parent_id_param = attrs['parent_id'].to_s
729 729 if new_record? || parent_id_param != parent_id.to_s
730 730 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
731 731 unless allowed_parents(user).include?(p)
732 732 attrs.delete('parent_id')
733 733 @unallowed_parent_id = true
734 734 end
735 735 end
736 736 end
737 737
738 738 super(attrs, user)
739 739 end
740 740
741 741 # Returns an auto-generated project identifier based on the last identifier used
742 742 def self.next_identifier
743 743 p = Project.order('id DESC').first
744 744 p.nil? ? nil : p.identifier.to_s.succ
745 745 end
746 746
747 747 # Copies and saves the Project instance based on the +project+.
748 748 # Duplicates the source project's:
749 749 # * Wiki
750 750 # * Versions
751 751 # * Categories
752 752 # * Issues
753 753 # * Members
754 754 # * Queries
755 755 #
756 756 # Accepts an +options+ argument to specify what to copy
757 757 #
758 758 # Examples:
759 759 # project.copy(1) # => copies everything
760 760 # project.copy(1, :only => 'members') # => copies members only
761 761 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
762 762 def copy(project, options={})
763 763 project = project.is_a?(Project) ? project : Project.find(project)
764 764
765 765 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
766 766 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
767 767
768 768 Project.transaction do
769 769 if save
770 770 reload
771 771 to_be_copied.each do |name|
772 772 send "copy_#{name}", project
773 773 end
774 774 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
775 775 save
776 776 else
777 777 false
778 778 end
779 779 end
780 780 end
781 781
782 782 def member_principals
783 783 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
784 784 memberships.active
785 785 end
786 786
787 787 # Returns a new unsaved Project instance with attributes copied from +project+
788 788 def self.copy_from(project)
789 789 project = project.is_a?(Project) ? project : Project.find(project)
790 790 # clear unique attributes
791 791 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
792 792 copy = Project.new(attributes)
793 793 copy.enabled_module_names = project.enabled_module_names
794 794 copy.trackers = project.trackers
795 795 copy.custom_values = project.custom_values.collect {|v| v.clone}
796 796 copy.issue_custom_fields = project.issue_custom_fields
797 797 copy
798 798 end
799 799
800 800 # Yields the given block for each project with its level in the tree
801 801 def self.project_tree(projects, &block)
802 802 ancestors = []
803 803 projects.sort_by(&:lft).each do |project|
804 804 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
805 805 ancestors.pop
806 806 end
807 807 yield project, ancestors.size
808 808 ancestors << project
809 809 end
810 810 end
811 811
812 812 private
813 813
814 814 def update_inherited_members
815 815 if parent
816 816 if inherit_members? && !inherit_members_was
817 817 remove_inherited_member_roles
818 818 add_inherited_member_roles
819 819 elsif !inherit_members? && inherit_members_was
820 820 remove_inherited_member_roles
821 821 end
822 822 end
823 823 end
824 824
825 825 def remove_inherited_member_roles
826 826 member_roles = memberships.map(&:member_roles).flatten
827 827 member_role_ids = member_roles.map(&:id)
828 828 member_roles.each do |member_role|
829 829 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
830 830 member_role.destroy
831 831 end
832 832 end
833 833 end
834 834
835 835 def add_inherited_member_roles
836 836 if inherit_members? && parent
837 837 parent.memberships.each do |parent_member|
838 838 member = Member.find_or_new(self.id, parent_member.user_id)
839 839 parent_member.member_roles.each do |parent_member_role|
840 840 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
841 841 end
842 842 member.save!
843 843 end
844 844 memberships.reset
845 845 end
846 846 end
847 847
848 848 def update_versions_from_hierarchy_change
849 849 Issue.update_versions_from_hierarchy_change(self)
850 850 end
851 851
852 852 def validate_parent
853 853 if @unallowed_parent_id
854 854 errors.add(:parent_id, :invalid)
855 855 elsif parent_id_changed?
856 856 unless parent.nil? || (parent.active? && move_possible?(parent))
857 857 errors.add(:parent_id, :invalid)
858 858 end
859 859 end
860 860 end
861 861
862 862 # Copies wiki from +project+
863 863 def copy_wiki(project)
864 864 # Check that the source project has a wiki first
865 865 unless project.wiki.nil?
866 866 wiki = self.wiki || Wiki.new
867 867 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
868 868 wiki_pages_map = {}
869 869 project.wiki.pages.each do |page|
870 870 # Skip pages without content
871 871 next if page.content.nil?
872 872 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
873 873 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
874 874 new_wiki_page.content = new_wiki_content
875 875 wiki.pages << new_wiki_page
876 876 wiki_pages_map[page.id] = new_wiki_page
877 877 end
878 878
879 879 self.wiki = wiki
880 880 wiki.save
881 881 # Reproduce page hierarchy
882 882 project.wiki.pages.each do |page|
883 883 if page.parent_id && wiki_pages_map[page.id]
884 884 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
885 885 wiki_pages_map[page.id].save
886 886 end
887 887 end
888 888 end
889 889 end
890 890
891 891 # Copies versions from +project+
892 892 def copy_versions(project)
893 893 project.versions.each do |version|
894 894 new_version = Version.new
895 895 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
896 896 self.versions << new_version
897 897 end
898 898 end
899 899
900 900 # Copies issue categories from +project+
901 901 def copy_issue_categories(project)
902 902 project.issue_categories.each do |issue_category|
903 903 new_issue_category = IssueCategory.new
904 904 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
905 905 self.issue_categories << new_issue_category
906 906 end
907 907 end
908 908
909 909 # Copies issues from +project+
910 910 def copy_issues(project)
911 911 # Stores the source issue id as a key and the copied issues as the
912 912 # value. Used to map the two together for issue relations.
913 913 issues_map = {}
914 914
915 915 # Store status and reopen locked/closed versions
916 916 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
917 917 version_statuses.each do |version, status|
918 918 version.update_attribute :status, 'open'
919 919 end
920 920
921 921 # Get issues sorted by root_id, lft so that parent issues
922 922 # get copied before their children
923 923 project.issues.reorder('root_id, lft').each do |issue|
924 924 new_issue = Issue.new
925 925 new_issue.copy_from(issue, :subtasks => false, :link => false)
926 926 new_issue.project = self
927 927 # Changing project resets the custom field values
928 928 # TODO: handle this in Issue#project=
929 929 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
930 930 # Reassign fixed_versions by name, since names are unique per project
931 931 if issue.fixed_version && issue.fixed_version.project == project
932 932 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
933 933 end
934 934 # Reassign version custom field values
935 935 new_issue.custom_field_values.each do |custom_value|
936 936 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
937 937 versions = Version.where(:id => custom_value.value).to_a
938 938 new_value = versions.map do |version|
939 939 if version.project == project
940 940 self.versions.detect {|v| v.name == version.name}.try(:id)
941 941 else
942 942 version.id
943 943 end
944 944 end
945 945 new_value.compact!
946 946 new_value = new_value.first unless custom_value.custom_field.multiple?
947 947 custom_value.value = new_value
948 948 end
949 949 end
950 950 # Reassign the category by name, since names are unique per project
951 951 if issue.category
952 952 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
953 953 end
954 954 # Parent issue
955 955 if issue.parent_id
956 956 if copied_parent = issues_map[issue.parent_id]
957 957 new_issue.parent_issue_id = copied_parent.id
958 958 end
959 959 end
960 960
961 961 self.issues << new_issue
962 962 if new_issue.new_record?
963 963 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
964 964 else
965 965 issues_map[issue.id] = new_issue unless new_issue.new_record?
966 966 end
967 967 end
968 968
969 969 # Restore locked/closed version statuses
970 970 version_statuses.each do |version, status|
971 971 version.update_attribute :status, status
972 972 end
973 973
974 974 # Relations after in case issues related each other
975 975 project.issues.each do |issue|
976 976 new_issue = issues_map[issue.id]
977 977 unless new_issue
978 978 # Issue was not copied
979 979 next
980 980 end
981 981
982 982 # Relations
983 983 issue.relations_from.each do |source_relation|
984 984 new_issue_relation = IssueRelation.new
985 985 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
986 986 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
987 987 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
988 988 new_issue_relation.issue_to = source_relation.issue_to
989 989 end
990 990 new_issue.relations_from << new_issue_relation
991 991 end
992 992
993 993 issue.relations_to.each do |source_relation|
994 994 new_issue_relation = IssueRelation.new
995 995 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
996 996 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
997 997 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
998 998 new_issue_relation.issue_from = source_relation.issue_from
999 999 end
1000 1000 new_issue.relations_to << new_issue_relation
1001 1001 end
1002 1002 end
1003 1003 end
1004 1004
1005 1005 # Copies members from +project+
1006 1006 def copy_members(project)
1007 1007 # Copy users first, then groups to handle members with inherited and given roles
1008 1008 members_to_copy = []
1009 1009 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
1010 1010 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1011 1011
1012 1012 members_to_copy.each do |member|
1013 1013 new_member = Member.new
1014 1014 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1015 1015 # only copy non inherited roles
1016 1016 # inherited roles will be added when copying the group membership
1017 1017 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1018 1018 next if role_ids.empty?
1019 1019 new_member.role_ids = role_ids
1020 1020 new_member.project = self
1021 1021 self.members << new_member
1022 1022 end
1023 1023 end
1024 1024
1025 1025 # Copies queries from +project+
1026 1026 def copy_queries(project)
1027 1027 project.queries.each do |query|
1028 1028 new_query = IssueQuery.new
1029 1029 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1030 1030 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1031 1031 new_query.project = self
1032 1032 new_query.user_id = query.user_id
1033 1033 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1034 1034 self.queries << new_query
1035 1035 end
1036 1036 end
1037 1037
1038 1038 # Copies boards from +project+
1039 1039 def copy_boards(project)
1040 1040 project.boards.each do |board|
1041 1041 new_board = Board.new
1042 1042 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1043 1043 new_board.project = self
1044 1044 self.boards << new_board
1045 1045 end
1046 1046 end
1047 1047
1048 1048 def allowed_permissions
1049 1049 @allowed_permissions ||= begin
1050 1050 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1051 1051 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1052 1052 end
1053 1053 end
1054 1054
1055 1055 def allowed_actions
1056 1056 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1057 1057 end
1058 1058
1059 1059 # Archives subprojects recursively
1060 1060 def archive!
1061 1061 children.each do |subproject|
1062 1062 subproject.send :archive!
1063 1063 end
1064 1064 update_attribute :status, STATUS_ARCHIVED
1065 1065 end
1066 1066 end
@@ -1,15 +1,15
1 1 class SetTopicAuthorsAsWatchers < ActiveRecord::Migration
2 2 def self.up
3 3 # Sets active users who created/replied a topic as watchers of the topic
4 4 # so that the new watch functionality at topic level doesn't affect notifications behaviour
5 5 Message.connection.execute("INSERT INTO #{Watcher.table_name} (watchable_type, watchable_id, user_id)" +
6 6 " SELECT DISTINCT 'Message', COALESCE(m.parent_id, m.id), m.author_id" +
7 7 " FROM #{Message.table_name} m, #{User.table_name} u" +
8 8 " WHERE m.author_id = u.id AND u.status = 1")
9 9 end
10 10
11 11 def self.down
12 12 # Removes all message watchers
13 Watcher.delete_all("watchable_type = 'Message'")
13 Watcher.where("watchable_type = 'Message'").delete_all
14 14 end
15 15 end
@@ -1,12 +1,12
1 1 class EnableCalendarAndGanttModulesWhereAppropriate < ActiveRecord::Migration
2 2 def self.up
3 3 EnabledModule.where(:name => 'issue_tracking').each do |e|
4 4 EnabledModule.create(:name => 'calendar', :project_id => e.project_id)
5 5 EnabledModule.create(:name => 'gantt', :project_id => e.project_id)
6 6 end
7 7 end
8 8
9 9 def self.down
10 EnabledModule.delete_all("name = 'calendar' OR name = 'gantt'")
10 EnabledModule.where("name = 'calendar' OR name = 'gantt'").delete_all
11 11 end
12 12 end
@@ -1,22 +1,22
1 1 class AddUniqueIndexOnMembers < ActiveRecord::Migration
2 2 def self.up
3 3 # Clean and reassign MemberRole rows if needed
4 MemberRole.delete_all("member_id NOT IN (SELECT id FROM #{Member.table_name})")
4 MemberRole.where("member_id NOT IN (SELECT id FROM #{Member.table_name})").delete_all
5 5 MemberRole.update_all("member_id =" +
6 6 " (SELECT min(m2.id) FROM #{Member.table_name} m1, #{Member.table_name} m2" +
7 7 " WHERE m1.user_id = m2.user_id AND m1.project_id = m2.project_id" +
8 8 " AND m1.id = #{MemberRole.table_name}.member_id)")
9 9 # Remove duplicates
10 10 Member.connection.select_values("SELECT m.id FROM #{Member.table_name} m" +
11 11 " WHERE m.id > (SELECT min(m1.id) FROM #{Member.table_name} m1 WHERE m1.user_id = m.user_id AND m1.project_id = m.project_id)").each do |i|
12 Member.delete_all(["id = ?", i])
12 Member.where(["id = ?", i]).delete_all
13 13 end
14 14
15 15 # Then add a unique index
16 16 add_index :members, [:user_id, :project_id], :unique => true
17 17 end
18 18
19 19 def self.down
20 20 remove_index :members, [:user_id, :project_id]
21 21 end
22 22 end
@@ -1,16 +1,16
1 1 class AddUniqueIndexToIssueRelations < ActiveRecord::Migration
2 2 def self.up
3 3
4 4 # Remove duplicates
5 5 IssueRelation.connection.select_values("SELECT r.id FROM #{IssueRelation.table_name} r" +
6 6 " WHERE r.id > (SELECT min(r1.id) FROM #{IssueRelation.table_name} r1 WHERE r1.issue_from_id = r.issue_from_id AND r1.issue_to_id = r.issue_to_id)").each do |i|
7 IssueRelation.delete_all(["id = ?", i])
7 IssueRelation.where(["id = ?", i]).delete_all
8 8 end
9 9
10 10 add_index :issue_relations, [:issue_from_id, :issue_to_id], :unique => true
11 11 end
12 12
13 13 def self.down
14 14 remove_index :issue_relations, :column => [:issue_from_id, :issue_to_id]
15 15 end
16 16 end
@@ -1,516 +1,516
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 desc 'Mantis migration script'
19 19
20 20 require 'active_record'
21 21 require 'pp'
22 22
23 23 namespace :redmine do
24 24 task :migrate_from_mantis => :environment do
25 25
26 26 module MantisMigrate
27 27
28 28 new_status = IssueStatus.find_by_position(1)
29 29 assigned_status = IssueStatus.find_by_position(2)
30 30 resolved_status = IssueStatus.find_by_position(3)
31 31 feedback_status = IssueStatus.find_by_position(4)
32 32 closed_status = IssueStatus.where(:is_closed => true).first
33 33 STATUS_MAPPING = {10 => new_status, # new
34 34 20 => feedback_status, # feedback
35 35 30 => new_status, # acknowledged
36 36 40 => new_status, # confirmed
37 37 50 => assigned_status, # assigned
38 38 80 => resolved_status, # resolved
39 39 90 => closed_status # closed
40 40 }
41 41
42 42 priorities = IssuePriority.all
43 43 DEFAULT_PRIORITY = priorities[2]
44 44 PRIORITY_MAPPING = {10 => priorities[1], # none
45 45 20 => priorities[1], # low
46 46 30 => priorities[2], # normal
47 47 40 => priorities[3], # high
48 48 50 => priorities[4], # urgent
49 49 60 => priorities[5] # immediate
50 50 }
51 51
52 52 TRACKER_BUG = Tracker.find_by_position(1)
53 53 TRACKER_FEATURE = Tracker.find_by_position(2)
54 54
55 55 roles = Role.where(:builtin => 0).order('position ASC').all
56 56 manager_role = roles[0]
57 57 developer_role = roles[1]
58 58 DEFAULT_ROLE = roles.last
59 59 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
60 60 25 => DEFAULT_ROLE, # reporter
61 61 40 => DEFAULT_ROLE, # updater
62 62 55 => developer_role, # developer
63 63 70 => manager_role, # manager
64 64 90 => manager_role # administrator
65 65 }
66 66
67 67 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
68 68 1 => 'int', # Numeric
69 69 2 => 'int', # Float
70 70 3 => 'list', # Enumeration
71 71 4 => 'string', # Email
72 72 5 => 'bool', # Checkbox
73 73 6 => 'list', # List
74 74 7 => 'list', # Multiselection list
75 75 8 => 'date', # Date
76 76 }
77 77
78 78 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
79 79 2 => IssueRelation::TYPE_RELATES, # parent of
80 80 3 => IssueRelation::TYPE_RELATES, # child of
81 81 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
82 82 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
83 83 }
84 84
85 85 class MantisUser < ActiveRecord::Base
86 86 self.table_name = :mantis_user_table
87 87
88 88 def firstname
89 89 @firstname = realname.blank? ? username : realname.split.first[0..29]
90 90 @firstname
91 91 end
92 92
93 93 def lastname
94 94 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
95 95 @lastname = '-' if @lastname.blank?
96 96 @lastname
97 97 end
98 98
99 99 def email
100 100 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
101 101 !User.find_by_mail(read_attribute(:email))
102 102 @email = read_attribute(:email)
103 103 else
104 104 @email = "#{username}@foo.bar"
105 105 end
106 106 end
107 107
108 108 def username
109 109 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
110 110 end
111 111 end
112 112
113 113 class MantisProject < ActiveRecord::Base
114 114 self.table_name = :mantis_project_table
115 115 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
116 116 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
117 117 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
118 118 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
119 119
120 120 def identifier
121 121 read_attribute(:name).downcase.gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
122 122 end
123 123 end
124 124
125 125 class MantisVersion < ActiveRecord::Base
126 126 self.table_name = :mantis_project_version_table
127 127
128 128 def version
129 129 read_attribute(:version)[0..29]
130 130 end
131 131
132 132 def description
133 133 read_attribute(:description)[0..254]
134 134 end
135 135 end
136 136
137 137 class MantisCategory < ActiveRecord::Base
138 138 self.table_name = :mantis_project_category_table
139 139 end
140 140
141 141 class MantisProjectUser < ActiveRecord::Base
142 142 self.table_name = :mantis_project_user_list_table
143 143 end
144 144
145 145 class MantisBug < ActiveRecord::Base
146 146 self.table_name = :mantis_bug_table
147 147 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
148 148 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
149 149 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
150 150 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
151 151 end
152 152
153 153 class MantisBugText < ActiveRecord::Base
154 154 self.table_name = :mantis_bug_text_table
155 155
156 156 # Adds Mantis steps_to_reproduce and additional_information fields
157 157 # to description if any
158 158 def full_description
159 159 full_description = description
160 160 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
161 161 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
162 162 full_description
163 163 end
164 164 end
165 165
166 166 class MantisBugNote < ActiveRecord::Base
167 167 self.table_name = :mantis_bugnote_table
168 168 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
169 169 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
170 170 end
171 171
172 172 class MantisBugNoteText < ActiveRecord::Base
173 173 self.table_name = :mantis_bugnote_text_table
174 174 end
175 175
176 176 class MantisBugFile < ActiveRecord::Base
177 177 self.table_name = :mantis_bug_file_table
178 178
179 179 def size
180 180 filesize
181 181 end
182 182
183 183 def original_filename
184 184 MantisMigrate.encode(filename)
185 185 end
186 186
187 187 def content_type
188 188 file_type
189 189 end
190 190
191 191 def read(*args)
192 192 if @read_finished
193 193 nil
194 194 else
195 195 @read_finished = true
196 196 content
197 197 end
198 198 end
199 199 end
200 200
201 201 class MantisBugRelationship < ActiveRecord::Base
202 202 self.table_name = :mantis_bug_relationship_table
203 203 end
204 204
205 205 class MantisBugMonitor < ActiveRecord::Base
206 206 self.table_name = :mantis_bug_monitor_table
207 207 end
208 208
209 209 class MantisNews < ActiveRecord::Base
210 210 self.table_name = :mantis_news_table
211 211 end
212 212
213 213 class MantisCustomField < ActiveRecord::Base
214 214 self.table_name = :mantis_custom_field_table
215 215 set_inheritance_column :none
216 216 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
217 217 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
218 218
219 219 def format
220 220 read_attribute :type
221 221 end
222 222
223 223 def name
224 224 read_attribute(:name)[0..29]
225 225 end
226 226 end
227 227
228 228 class MantisCustomFieldProject < ActiveRecord::Base
229 229 self.table_name = :mantis_custom_field_project_table
230 230 end
231 231
232 232 class MantisCustomFieldString < ActiveRecord::Base
233 233 self.table_name = :mantis_custom_field_string_table
234 234 end
235 235
236 236 def self.migrate
237 237
238 238 # Users
239 239 print "Migrating users"
240 User.delete_all "login <> 'admin'"
240 User.where("login <> 'admin'").delete_all
241 241 users_map = {}
242 242 users_migrated = 0
243 243 MantisUser.all.each do |user|
244 244 u = User.new :firstname => encode(user.firstname),
245 245 :lastname => encode(user.lastname),
246 246 :mail => user.email,
247 247 :last_login_on => user.last_visit
248 248 u.login = user.username
249 249 u.password = 'mantis'
250 250 u.status = User::STATUS_LOCKED if user.enabled != 1
251 251 u.admin = true if user.access_level == 90
252 252 next unless u.save!
253 253 users_migrated += 1
254 254 users_map[user.id] = u.id
255 255 print '.'
256 256 end
257 257 puts
258 258
259 259 # Projects
260 260 print "Migrating projects"
261 261 Project.destroy_all
262 262 projects_map = {}
263 263 versions_map = {}
264 264 categories_map = {}
265 265 MantisProject.all.each do |project|
266 266 p = Project.new :name => encode(project.name),
267 267 :description => encode(project.description)
268 268 p.identifier = project.identifier
269 269 next unless p.save
270 270 projects_map[project.id] = p.id
271 271 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
272 272 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
273 273 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
274 274 print '.'
275 275
276 276 # Project members
277 277 project.members.each do |member|
278 278 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
279 279 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
280 280 m.project = p
281 281 m.save
282 282 end
283 283
284 284 # Project versions
285 285 project.versions.each do |version|
286 286 v = Version.new :name => encode(version.version),
287 287 :description => encode(version.description),
288 288 :effective_date => (version.date_order ? version.date_order.to_date : nil)
289 289 v.project = p
290 290 v.save
291 291 versions_map[version.id] = v.id
292 292 end
293 293
294 294 # Project categories
295 295 project.categories.each do |category|
296 296 g = IssueCategory.new :name => category.category[0,30]
297 297 g.project = p
298 298 g.save
299 299 categories_map[category.category] = g.id
300 300 end
301 301 end
302 302 puts
303 303
304 304 # Bugs
305 305 print "Migrating bugs"
306 306 Issue.destroy_all
307 307 issues_map = {}
308 308 keep_bug_ids = (Issue.count == 0)
309 309 MantisBug.find_each(:batch_size => 200) do |bug|
310 310 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
311 311 i = Issue.new :project_id => projects_map[bug.project_id],
312 312 :subject => encode(bug.summary),
313 313 :description => encode(bug.bug_text.full_description),
314 314 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
315 315 :created_on => bug.date_submitted,
316 316 :updated_on => bug.last_updated
317 317 i.author = User.find_by_id(users_map[bug.reporter_id])
318 318 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
319 319 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
320 320 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
321 321 i.status = STATUS_MAPPING[bug.status] || i.status
322 322 i.id = bug.id if keep_bug_ids
323 323 next unless i.save
324 324 issues_map[bug.id] = i.id
325 325 print '.'
326 326 STDOUT.flush
327 327
328 328 # Assignee
329 329 # Redmine checks that the assignee is a project member
330 330 if (bug.handler_id && users_map[bug.handler_id])
331 331 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
332 332 i.save(:validate => false)
333 333 end
334 334
335 335 # Bug notes
336 336 bug.bug_notes.each do |note|
337 337 next unless users_map[note.reporter_id]
338 338 n = Journal.new :notes => encode(note.bug_note_text.note),
339 339 :created_on => note.date_submitted
340 340 n.user = User.find_by_id(users_map[note.reporter_id])
341 341 n.journalized = i
342 342 n.save
343 343 end
344 344
345 345 # Bug files
346 346 bug.bug_files.each do |file|
347 347 a = Attachment.new :created_on => file.date_added
348 348 a.file = file
349 349 a.author = User.first
350 350 a.container = i
351 351 a.save
352 352 end
353 353
354 354 # Bug monitors
355 355 bug.bug_monitors.each do |monitor|
356 356 next unless users_map[monitor.user_id]
357 357 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
358 358 end
359 359 end
360 360
361 361 # update issue id sequence if needed (postgresql)
362 362 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
363 363 puts
364 364
365 365 # Bug relationships
366 366 print "Migrating bug relations"
367 367 MantisBugRelationship.all.each do |relation|
368 368 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
369 369 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
370 370 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
371 371 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
372 372 pp r unless r.save
373 373 print '.'
374 374 STDOUT.flush
375 375 end
376 376 puts
377 377
378 378 # News
379 379 print "Migrating news"
380 380 News.destroy_all
381 381 MantisNews.where('project_id > 0').all.each do |news|
382 382 next unless projects_map[news.project_id]
383 383 n = News.new :project_id => projects_map[news.project_id],
384 384 :title => encode(news.headline[0..59]),
385 385 :description => encode(news.body),
386 386 :created_on => news.date_posted
387 387 n.author = User.find_by_id(users_map[news.poster_id])
388 388 n.save
389 389 print '.'
390 390 STDOUT.flush
391 391 end
392 392 puts
393 393
394 394 # Custom fields
395 395 print "Migrating custom fields"
396 396 IssueCustomField.destroy_all
397 397 MantisCustomField.all.each do |field|
398 398 f = IssueCustomField.new :name => field.name[0..29],
399 399 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
400 400 :min_length => field.length_min,
401 401 :max_length => field.length_max,
402 402 :regexp => field.valid_regexp,
403 403 :possible_values => field.possible_values.split('|'),
404 404 :is_required => field.require_report?
405 405 next unless f.save
406 406 print '.'
407 407 STDOUT.flush
408 408 # Trackers association
409 409 f.trackers = Tracker.all
410 410
411 411 # Projects association
412 412 field.projects.each do |project|
413 413 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
414 414 end
415 415
416 416 # Values
417 417 field.values.each do |value|
418 418 v = CustomValue.new :custom_field_id => f.id,
419 419 :value => value.value
420 420 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
421 421 v.save
422 422 end unless f.new_record?
423 423 end
424 424 puts
425 425
426 426 puts
427 427 puts "Users: #{users_migrated}/#{MantisUser.count}"
428 428 puts "Projects: #{Project.count}/#{MantisProject.count}"
429 429 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
430 430 puts "Versions: #{Version.count}/#{MantisVersion.count}"
431 431 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
432 432 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
433 433 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
434 434 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
435 435 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
436 436 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
437 437 puts "News: #{News.count}/#{MantisNews.count}"
438 438 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
439 439 end
440 440
441 441 def self.encoding(charset)
442 442 @charset = charset
443 443 end
444 444
445 445 def self.establish_connection(params)
446 446 constants.each do |const|
447 447 klass = const_get(const)
448 448 next unless klass.respond_to? 'establish_connection'
449 449 klass.establish_connection params
450 450 end
451 451 end
452 452
453 453 def self.encode(text)
454 454 text.to_s.force_encoding(@charset).encode('UTF-8')
455 455 end
456 456 end
457 457
458 458 puts
459 459 if Redmine::DefaultData::Loader.no_data?
460 460 puts "Redmine configuration need to be loaded before importing data."
461 461 puts "Please, run this first:"
462 462 puts
463 463 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
464 464 exit
465 465 end
466 466
467 467 puts "WARNING: Your Redmine data will be deleted during this process."
468 468 print "Are you sure you want to continue ? [y/N] "
469 469 STDOUT.flush
470 470 break unless STDIN.gets.match(/^y$/i)
471 471
472 472 # Default Mantis database settings
473 473 db_params = {:adapter => 'mysql2',
474 474 :database => 'bugtracker',
475 475 :host => 'localhost',
476 476 :username => 'root',
477 477 :password => '' }
478 478
479 479 puts
480 480 puts "Please enter settings for your Mantis database"
481 481 [:adapter, :host, :database, :username, :password].each do |param|
482 482 print "#{param} [#{db_params[param]}]: "
483 483 value = STDIN.gets.chomp!
484 484 db_params[param] = value unless value.blank?
485 485 end
486 486
487 487 while true
488 488 print "encoding [UTF-8]: "
489 489 STDOUT.flush
490 490 encoding = STDIN.gets.chomp!
491 491 encoding = 'UTF-8' if encoding.blank?
492 492 break if MantisMigrate.encoding encoding
493 493 puts "Invalid encoding!"
494 494 end
495 495 puts
496 496
497 497 # Make sure bugs can refer bugs in other projects
498 498 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
499 499
500 500 old_notified_events = Setting.notified_events
501 501 old_password_min_length = Setting.password_min_length
502 502 begin
503 503 # Turn off email notifications temporarily
504 504 Setting.notified_events = []
505 505 Setting.password_min_length = 4
506 506 # Run the migration
507 507 MantisMigrate.establish_connection db_params
508 508 MantisMigrate.migrate
509 509 ensure
510 510 # Restore previous settings
511 511 Setting.notified_events = old_notified_events
512 512 Setting.password_min_length = old_password_min_length
513 513 end
514 514
515 515 end
516 516 end
@@ -1,168 +1,168
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 AdminControllerTest < Redmine::ControllerTest
21 21 fixtures :projects, :users, :email_addresses, :roles
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 1 # admin
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_select 'div.nodata', 0
31 31 end
32 32
33 33 def test_index_with_no_configuration_data
34 34 delete_configuration_data
35 35 get :index
36 36 assert_select 'div.nodata'
37 37 end
38 38
39 39 def test_projects
40 40 get :projects
41 41 assert_response :success
42 42 assert_template 'projects'
43 43 assert_not_nil assigns(:projects)
44 44 # active projects only
45 45 assert_nil assigns(:projects).detect {|u| !u.active?}
46 46 end
47 47
48 48 def test_projects_with_status_filter
49 49 get :projects, :status => 1
50 50 assert_response :success
51 51 assert_template 'projects'
52 52 assert_not_nil assigns(:projects)
53 53 # active projects only
54 54 assert_nil assigns(:projects).detect {|u| !u.active?}
55 55 end
56 56
57 57 def test_projects_with_name_filter
58 58 get :projects, :name => 'store', :status => ''
59 59 assert_response :success
60 60 assert_template 'projects'
61 61 projects = assigns(:projects)
62 62 assert_not_nil projects
63 63 assert_equal 1, projects.size
64 64 assert_equal 'OnlineStore', projects.first.name
65 65 end
66 66
67 67 def test_load_default_configuration_data
68 68 delete_configuration_data
69 69 post :default_configuration, :lang => 'fr'
70 70 assert_response :redirect
71 71 assert_nil flash[:error]
72 72 assert IssueStatus.find_by_name('Nouveau')
73 73 end
74 74
75 75 def test_load_default_configuration_data_should_rescue_error
76 76 delete_configuration_data
77 77 Redmine::DefaultData::Loader.stubs(:load).raises(Exception.new("Something went wrong"))
78 78 post :default_configuration, :lang => 'fr'
79 79 assert_response :redirect
80 80 assert_not_nil flash[:error]
81 81 assert_match /Something went wrong/, flash[:error]
82 82 end
83 83
84 84 def test_test_email
85 85 user = User.find(1)
86 86 user.pref.no_self_notified = '1'
87 87 user.pref.save!
88 88 ActionMailer::Base.deliveries.clear
89 89
90 90 post :test_email
91 91 assert_redirected_to '/settings?tab=notifications'
92 92 mail = ActionMailer::Base.deliveries.last
93 93 assert_not_nil mail
94 94 user = User.find(1)
95 95 assert_equal [user.mail], mail.bcc
96 96 end
97 97
98 98 def test_test_email_failure_should_display_the_error
99 99 Mailer.stubs(:test_email).raises(Exception, 'Some error message')
100 100 post :test_email
101 101 assert_redirected_to '/settings?tab=notifications'
102 102 assert_match /Some error message/, flash[:error]
103 103 end
104 104
105 105 def test_no_plugins
106 106 Redmine::Plugin.stubs(:registered_plugins).returns({})
107 107
108 108 get :plugins
109 109 assert_response :success
110 110 assert_template 'plugins'
111 111 assert_equal [], assigns(:plugins)
112 112 end
113 113
114 114 def test_plugins
115 115 # Register a few plugins
116 116 Redmine::Plugin.register :foo do
117 117 name 'Foo plugin'
118 118 author 'John Smith'
119 119 description 'This is a test plugin'
120 120 version '0.0.1'
121 121 settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'foo/settings'
122 122 end
123 123 Redmine::Plugin.register :bar do
124 124 end
125 125
126 126 get :plugins
127 127 assert_response :success
128 128 assert_template 'plugins'
129 129
130 130 assert_select 'tr#plugin-foo' do
131 131 assert_select 'td span.name', :text => 'Foo plugin'
132 132 assert_select 'td.configure a[href="/settings/plugin/foo"]'
133 133 end
134 134 assert_select 'tr#plugin-bar' do
135 135 assert_select 'td span.name', :text => 'Bar'
136 136 assert_select 'td.configure a', 0
137 137 end
138 138 end
139 139
140 140 def test_info
141 141 get :info
142 142 assert_response :success
143 143 assert_template 'info'
144 144 end
145 145
146 146 def test_admin_menu_plugin_extension
147 147 Redmine::MenuManager.map :admin_menu do |menu|
148 148 menu.push :test_admin_menu_plugin_extension, '/foo/bar', :caption => 'Test'
149 149 end
150 150
151 151 get :index
152 152 assert_response :success
153 153 assert_select 'div#admin-menu a[href="/foo/bar"]', :text => 'Test'
154 154
155 155 Redmine::MenuManager.map :admin_menu do |menu|
156 156 menu.delete :test_admin_menu_plugin_extension
157 157 end
158 158 end
159 159
160 160 private
161 161
162 162 def delete_configuration_data
163 Role.delete_all('builtin = 0')
163 Role.where('builtin = 0').delete_all
164 164 Tracker.delete_all
165 165 IssueStatus.delete_all
166 166 Enumeration.delete_all
167 167 end
168 168 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,49 +1,49
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 DefaultDataTest < ActiveSupport::TestCase
21 21 include Redmine::I18n
22 22 fixtures :roles
23 23
24 24 def test_no_data
25 25 assert !Redmine::DefaultData::Loader::no_data?
26 Role.delete_all("builtin = 0")
26 Role.where("builtin = 0").delete_all
27 27 Tracker.delete_all
28 28 IssueStatus.delete_all
29 29 Enumeration.delete_all
30 30 assert Redmine::DefaultData::Loader::no_data?
31 31 end
32 32
33 33 def test_load
34 34 valid_languages.each do |lang|
35 35 begin
36 Role.delete_all("builtin = 0")
36 Role.where("builtin = 0").delete_all
37 37 Tracker.delete_all
38 38 IssueStatus.delete_all
39 39 Enumeration.delete_all
40 40 assert Redmine::DefaultData::Loader::load(lang)
41 41 assert_not_nil DocumentCategory.first
42 42 assert_not_nil IssuePriority.first
43 43 assert_not_nil TimeEntryActivity.first
44 44 rescue ActiveRecord::RecordInvalid => e
45 45 assert false, ":#{lang} default data is invalid (#{e.message})."
46 46 end
47 47 end
48 48 end
49 49 end
@@ -1,90 +1,90
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 NewsTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, :enabled_modules, :news
22 22
23 23 def valid_news
24 24 { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.first }
25 25 end
26 26
27 27 def setup
28 28 end
29 29
30 30 def test_create_should_send_email_notification
31 31 ActionMailer::Base.deliveries.clear
32 32 news = Project.find(1).news.new(valid_news)
33 33
34 34 with_settings :notified_events => %w(news_added) do
35 35 assert news.save
36 36 end
37 37 assert_equal 1, ActionMailer::Base.deliveries.size
38 38 end
39 39
40 40 def test_should_include_news_for_projects_with_news_enabled
41 41 project = projects(:projects_001)
42 42 assert project.enabled_modules.any?{ |em| em.name == 'news' }
43 43
44 44 # News.latest should return news from projects_001
45 45 assert News.latest.any? { |news| news.project == project }
46 46 end
47 47
48 48 def test_should_not_include_news_for_projects_with_news_disabled
49 EnabledModule.delete_all(["project_id = ? AND name = ?", 2, 'news'])
49 EnabledModule.where(["project_id = ? AND name = ?", 2, 'news']).delete_all
50 50 project = Project.find(2)
51 51
52 52 # Add a piece of news to the project
53 53 news = project.news.create(valid_news)
54 54
55 55 # News.latest should not return that new piece of news
56 56 assert News.latest.include?(news) == false
57 57 end
58 58
59 59 def test_should_only_include_news_from_projects_visibly_to_the_user
60 60 assert News.latest(User.anonymous).all? { |news| news.project.is_public? }
61 61 end
62 62
63 63 def test_should_limit_the_amount_of_returned_news
64 64 # Make sure we have a bunch of news stories
65 65 10.times { projects(:projects_001).news.create(valid_news) }
66 66 assert_equal 2, News.latest(users(:users_002), 2).size
67 67 assert_equal 6, News.latest(users(:users_002), 6).size
68 68 end
69 69
70 70 def test_should_return_5_news_stories_by_default
71 71 # Make sure we have a bunch of news stories
72 72 10.times { projects(:projects_001).news.create(valid_news) }
73 73 assert_equal 5, News.latest(users(:users_004)).size
74 74 end
75 75
76 76 def test_attachments_should_be_visible
77 77 assert News.find(1).attachments_visible?(User.anonymous)
78 78 end
79 79
80 80 def test_attachments_should_be_deletable_with_manage_news_permission
81 81 manager = User.find(2)
82 82 assert News.find(1).attachments_deletable?(manager)
83 83 end
84 84
85 85 def test_attachments_should_not_be_deletable_without_manage_news_permission
86 86 manager = User.find(2)
87 87 Role.find_by_name('Manager').remove_permission!(:manage_news)
88 88 assert !News.find(1).attachments_deletable?(manager)
89 89 end
90 90 end
@@ -1,1237 +1,1237
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 UserTest < ActiveSupport::TestCase
21 21 fixtures :users, :email_addresses, :members, :projects, :roles, :member_roles, :auth_sources,
22 22 :trackers, :issue_statuses,
23 23 :projects_trackers,
24 24 :watchers,
25 25 :issue_categories, :enumerations, :issues,
26 26 :journals, :journal_details,
27 27 :groups_users,
28 28 :enabled_modules,
29 29 :tokens
30 30
31 31 include Redmine::I18n
32 32
33 33 def setup
34 34 @admin = User.find(1)
35 35 @jsmith = User.find(2)
36 36 @dlopper = User.find(3)
37 37 end
38 38
39 39 def test_sorted_scope_should_sort_user_by_display_name
40 40 # Use .active to ignore anonymous with localized display name
41 41 assert_equal User.active.map(&:name).map(&:downcase).sort,
42 42 User.active.sorted.map(&:name).map(&:downcase)
43 43 end
44 44
45 45 def test_generate
46 46 User.generate!(:firstname => 'Testing connection')
47 47 User.generate!(:firstname => 'Testing connection')
48 48 assert_equal 2, User.where(:firstname => 'Testing connection').count
49 49 end
50 50
51 51 def test_truth
52 52 assert_kind_of User, @jsmith
53 53 end
54 54
55 55 def test_should_validate_status
56 56 user = User.new
57 57 user.status = 0
58 58
59 59 assert !user.save
60 60 assert_include I18n.translate('activerecord.errors.messages.invalid'), user.errors[:status]
61 61 end
62 62
63 63 def test_mail_should_be_stripped
64 64 u = User.new
65 65 u.mail = " foo@bar.com "
66 66 assert_equal "foo@bar.com", u.mail
67 67 end
68 68
69 69 def test_should_create_email_address
70 70 u = User.new(:firstname => "new", :lastname => "user")
71 71 u.login = "create_email_address"
72 72 u.mail = "defaultemail@somenet.foo"
73 73 assert u.save
74 74 u.reload
75 75 assert u.email_address
76 76 assert_equal "defaultemail@somenet.foo", u.email_address.address
77 77 assert_equal true, u.email_address.is_default
78 78 assert_equal true, u.email_address.notify
79 79 end
80 80
81 81 def test_should_not_create_user_without_mail
82 82 set_language_if_valid 'en'
83 83 u = User.new(:firstname => "new", :lastname => "user")
84 84 u.login = "user_without_mail"
85 85 assert !u.save
86 86 assert_equal ["Email #{I18n.translate('activerecord.errors.messages.blank')}"], u.errors.full_messages
87 87 end
88 88
89 89 def test_should_not_create_user_with_blank_mail
90 90 set_language_if_valid 'en'
91 91 u = User.new(:firstname => "new", :lastname => "user")
92 92 u.login = "user_with_blank_mail"
93 93 u.mail = ''
94 94 assert !u.save
95 95 assert_equal ["Email #{I18n.translate('activerecord.errors.messages.blank')}"], u.errors.full_messages
96 96 end
97 97
98 98 def test_should_not_update_user_with_blank_mail
99 99 set_language_if_valid 'en'
100 100 u = User.find(2)
101 101 u.mail = ''
102 102 assert !u.save
103 103 assert_equal ["Email #{I18n.translate('activerecord.errors.messages.blank')}"], u.errors.full_messages
104 104 end
105 105
106 106 def test_login_length_validation
107 107 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
108 108 user.login = "x" * (User::LOGIN_LENGTH_LIMIT+1)
109 109 assert !user.valid?
110 110
111 111 user.login = "x" * (User::LOGIN_LENGTH_LIMIT)
112 112 assert user.valid?
113 113 assert user.save
114 114 end
115 115
116 116 def test_generate_password_should_respect_minimum_password_length
117 117 with_settings :password_min_length => 15 do
118 118 user = User.generate!(:generate_password => true)
119 119 assert user.password.length >= 15
120 120 end
121 121 end
122 122
123 123 def test_generate_password_should_not_generate_password_with_less_than_10_characters
124 124 with_settings :password_min_length => 4 do
125 125 user = User.generate!(:generate_password => true)
126 126 assert user.password.length >= 10
127 127 end
128 128 end
129 129
130 130 def test_generate_password_on_create_should_set_password
131 131 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
132 132 user.login = "newuser"
133 133 user.generate_password = true
134 134 assert user.save
135 135
136 136 password = user.password
137 137 assert user.check_password?(password)
138 138 end
139 139
140 140 def test_generate_password_on_update_should_update_password
141 141 user = User.find(2)
142 142 hash = user.hashed_password
143 143 user.generate_password = true
144 144 assert user.save
145 145
146 146 password = user.password
147 147 assert user.check_password?(password)
148 148 assert_not_equal hash, user.reload.hashed_password
149 149 end
150 150
151 151 def test_create
152 152 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
153 153
154 154 user.login = "jsmith"
155 155 user.password, user.password_confirmation = "password", "password"
156 156 # login uniqueness
157 157 assert !user.save
158 158 assert_equal 1, user.errors.count
159 159
160 160 user.login = "newuser"
161 161 user.password, user.password_confirmation = "password", "pass"
162 162 # password confirmation
163 163 assert !user.save
164 164 assert_equal 1, user.errors.count
165 165
166 166 user.password, user.password_confirmation = "password", "password"
167 167 assert user.save
168 168 end
169 169
170 170 def test_user_before_create_should_set_the_mail_notification_to_the_default_setting
171 171 @user1 = User.generate!
172 172 assert_equal 'only_my_events', @user1.mail_notification
173 173 with_settings :default_notification_option => 'all' do
174 174 @user2 = User.generate!
175 175 assert_equal 'all', @user2.mail_notification
176 176 end
177 177 end
178 178
179 179 def test_user_login_should_be_case_insensitive
180 180 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
181 181 u.login = 'newuser'
182 182 u.password, u.password_confirmation = "password", "password"
183 183 assert u.save
184 184 u = User.new(:firstname => "Similar", :lastname => "User",
185 185 :mail => "similaruser@somenet.foo")
186 186 u.login = 'NewUser'
187 187 u.password, u.password_confirmation = "password", "password"
188 188 assert !u.save
189 189 assert_include I18n.translate('activerecord.errors.messages.taken'), u.errors[:login]
190 190 end
191 191
192 192 def test_mail_uniqueness_should_not_be_case_sensitive
193 193 set_language_if_valid 'en'
194 194 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
195 195 u.login = 'newuser1'
196 196 u.password, u.password_confirmation = "password", "password"
197 197 assert u.save
198 198
199 199 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
200 200 u.login = 'newuser2'
201 201 u.password, u.password_confirmation = "password", "password"
202 202 assert !u.save
203 203 assert_include "Email #{I18n.translate('activerecord.errors.messages.taken')}", u.errors.full_messages
204 204 end
205 205
206 206 def test_update
207 207 assert_equal "admin", @admin.login
208 208 @admin.login = "john"
209 209 assert @admin.save, @admin.errors.full_messages.join("; ")
210 210 @admin.reload
211 211 assert_equal "john", @admin.login
212 212 end
213 213
214 214 def test_update_should_not_fail_for_legacy_user_with_different_case_logins
215 215 u1 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser1@somenet.foo")
216 216 u1.login = 'newuser1'
217 217 assert u1.save
218 218
219 219 u2 = User.new(:firstname => "new", :lastname => "user", :mail => "newuser2@somenet.foo")
220 220 u2.login = 'newuser1'
221 221 assert u2.save(:validate => false)
222 222
223 223 user = User.find(u2.id)
224 224 user.firstname = "firstname"
225 225 assert user.save, "Save failed"
226 226 end
227 227
228 228 def test_destroy_should_delete_members_and_roles
229 229 members = Member.where(:user_id => 2)
230 230 ms = members.count
231 231 rs = members.collect(&:roles).flatten.size
232 232 assert ms > 0
233 233 assert rs > 0
234 234 assert_difference 'Member.count', - ms do
235 235 assert_difference 'MemberRole.count', - rs do
236 236 User.find(2).destroy
237 237 end
238 238 end
239 239 assert_nil User.find_by_id(2)
240 240 assert_equal 0, Member.where(:user_id => 2).count
241 241 end
242 242
243 243 def test_destroy_should_update_attachments
244 244 attachment = Attachment.create!(:container => Project.find(1),
245 245 :file => uploaded_test_file("testfile.txt", "text/plain"),
246 246 :author_id => 2)
247 247
248 248 User.find(2).destroy
249 249 assert_nil User.find_by_id(2)
250 250 assert_equal User.anonymous, attachment.reload.author
251 251 end
252 252
253 253 def test_destroy_should_update_comments
254 254 comment = Comment.create!(
255 255 :commented => News.create!(:project_id => 1,
256 256 :author_id => 1, :title => 'foo', :description => 'foo'),
257 257 :author => User.find(2),
258 258 :comments => 'foo'
259 259 )
260 260
261 261 User.find(2).destroy
262 262 assert_nil User.find_by_id(2)
263 263 assert_equal User.anonymous, comment.reload.author
264 264 end
265 265
266 266 def test_destroy_should_update_issues
267 267 issue = Issue.create!(:project_id => 1, :author_id => 2,
268 268 :tracker_id => 1, :subject => 'foo')
269 269
270 270 User.find(2).destroy
271 271 assert_nil User.find_by_id(2)
272 272 assert_equal User.anonymous, issue.reload.author
273 273 end
274 274
275 275 def test_destroy_should_unassign_issues
276 276 issue = Issue.create!(:project_id => 1, :author_id => 1,
277 277 :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
278 278
279 279 User.find(2).destroy
280 280 assert_nil User.find_by_id(2)
281 281 assert_nil issue.reload.assigned_to
282 282 end
283 283
284 284 def test_destroy_should_update_journals
285 285 issue = Issue.create!(:project_id => 1, :author_id => 2,
286 286 :tracker_id => 1, :subject => 'foo')
287 287 issue.init_journal(User.find(2), "update")
288 288 issue.save!
289 289
290 290 User.find(2).destroy
291 291 assert_nil User.find_by_id(2)
292 292 assert_equal User.anonymous, issue.journals.first.reload.user
293 293 end
294 294
295 295 def test_destroy_should_update_journal_details_old_value
296 296 issue = Issue.create!(:project_id => 1, :author_id => 1,
297 297 :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
298 298 issue.init_journal(User.find(1), "update")
299 299 issue.assigned_to_id = nil
300 300 assert_difference 'JournalDetail.count' do
301 301 issue.save!
302 302 end
303 303 journal_detail = JournalDetail.order('id DESC').first
304 304 assert_equal '2', journal_detail.old_value
305 305
306 306 User.find(2).destroy
307 307 assert_nil User.find_by_id(2)
308 308 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
309 309 end
310 310
311 311 def test_destroy_should_update_journal_details_value
312 312 issue = Issue.create!(:project_id => 1, :author_id => 1,
313 313 :tracker_id => 1, :subject => 'foo')
314 314 issue.init_journal(User.find(1), "update")
315 315 issue.assigned_to_id = 2
316 316 assert_difference 'JournalDetail.count' do
317 317 issue.save!
318 318 end
319 319 journal_detail = JournalDetail.order('id DESC').first
320 320 assert_equal '2', journal_detail.value
321 321
322 322 User.find(2).destroy
323 323 assert_nil User.find_by_id(2)
324 324 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
325 325 end
326 326
327 327 def test_destroy_should_update_messages
328 328 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
329 329 message = Message.create!(:board_id => board.id, :author_id => 2,
330 330 :subject => 'foo', :content => 'foo')
331 331 User.find(2).destroy
332 332 assert_nil User.find_by_id(2)
333 333 assert_equal User.anonymous, message.reload.author
334 334 end
335 335
336 336 def test_destroy_should_update_news
337 337 news = News.create!(:project_id => 1, :author_id => 2,
338 338 :title => 'foo', :description => 'foo')
339 339 User.find(2).destroy
340 340 assert_nil User.find_by_id(2)
341 341 assert_equal User.anonymous, news.reload.author
342 342 end
343 343
344 344 def test_destroy_should_delete_private_queries
345 345 query = Query.new(:name => 'foo', :visibility => Query::VISIBILITY_PRIVATE)
346 346 query.project_id = 1
347 347 query.user_id = 2
348 348 query.save!
349 349
350 350 User.find(2).destroy
351 351 assert_nil User.find_by_id(2)
352 352 assert_nil Query.find_by_id(query.id)
353 353 end
354 354
355 355 def test_destroy_should_update_public_queries
356 356 query = Query.new(:name => 'foo', :visibility => Query::VISIBILITY_PUBLIC)
357 357 query.project_id = 1
358 358 query.user_id = 2
359 359 query.save!
360 360
361 361 User.find(2).destroy
362 362 assert_nil User.find_by_id(2)
363 363 assert_equal User.anonymous, query.reload.user
364 364 end
365 365
366 366 def test_destroy_should_update_time_entries
367 367 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today,
368 368 :activity => TimeEntryActivity.create!(:name => 'foo'))
369 369 entry.project_id = 1
370 370 entry.user_id = 2
371 371 entry.save!
372 372
373 373 User.find(2).destroy
374 374 assert_nil User.find_by_id(2)
375 375 assert_equal User.anonymous, entry.reload.user
376 376 end
377 377
378 378 def test_destroy_should_delete_tokens
379 379 token = Token.create!(:user_id => 2, :value => 'foo')
380 380
381 381 User.find(2).destroy
382 382 assert_nil User.find_by_id(2)
383 383 assert_nil Token.find_by_id(token.id)
384 384 end
385 385
386 386 def test_destroy_should_delete_watchers
387 387 issue = Issue.create!(:project_id => 1, :author_id => 1,
388 388 :tracker_id => 1, :subject => 'foo')
389 389 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
390 390
391 391 User.find(2).destroy
392 392 assert_nil User.find_by_id(2)
393 393 assert_nil Watcher.find_by_id(watcher.id)
394 394 end
395 395
396 396 def test_destroy_should_update_wiki_contents
397 397 wiki_content = WikiContent.create!(
398 398 :text => 'foo',
399 399 :author_id => 2,
400 400 :page => WikiPage.create!(:title => 'Foo',
401 401 :wiki => Wiki.create!(:project_id => 3,
402 402 :start_page => 'Start'))
403 403 )
404 404 wiki_content.text = 'bar'
405 405 assert_difference 'WikiContent::Version.count' do
406 406 wiki_content.save!
407 407 end
408 408
409 409 User.find(2).destroy
410 410 assert_nil User.find_by_id(2)
411 411 assert_equal User.anonymous, wiki_content.reload.author
412 412 wiki_content.versions.each do |version|
413 413 assert_equal User.anonymous, version.reload.author
414 414 end
415 415 end
416 416
417 417 def test_destroy_should_nullify_issue_categories
418 418 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
419 419
420 420 User.find(2).destroy
421 421 assert_nil User.find_by_id(2)
422 422 assert_nil category.reload.assigned_to_id
423 423 end
424 424
425 425 def test_destroy_should_nullify_changesets
426 426 changeset = Changeset.create!(
427 427 :repository => Repository::Subversion.create!(
428 428 :project_id => 1,
429 429 :url => 'file:///tmp',
430 430 :identifier => 'tmp'
431 431 ),
432 432 :revision => '12',
433 433 :committed_on => Time.now,
434 434 :committer => 'jsmith'
435 435 )
436 436 assert_equal 2, changeset.user_id
437 437
438 438 User.find(2).destroy
439 439 assert_nil User.find_by_id(2)
440 440 assert_nil changeset.reload.user_id
441 441 end
442 442
443 443 def test_anonymous_user_should_not_be_destroyable
444 444 assert_no_difference 'User.count' do
445 445 assert_equal false, User.anonymous.destroy
446 446 end
447 447 end
448 448
449 449 def test_password_change_should_destroy_tokens
450 450 recovery_token = Token.create!(:user_id => 2, :action => 'recovery')
451 451 autologin_token = Token.create!(:user_id => 2, :action => 'autologin')
452 452
453 453 user = User.find(2)
454 454 user.password, user.password_confirmation = "a new password", "a new password"
455 455 assert user.save
456 456
457 457 assert_nil Token.find_by_id(recovery_token.id)
458 458 assert_nil Token.find_by_id(autologin_token.id)
459 459 end
460 460
461 461 def test_mail_change_should_destroy_tokens
462 462 recovery_token = Token.create!(:user_id => 2, :action => 'recovery')
463 463 autologin_token = Token.create!(:user_id => 2, :action => 'autologin')
464 464
465 465 user = User.find(2)
466 466 user.mail = "user@somwehere.com"
467 467 assert user.save
468 468
469 469 assert_nil Token.find_by_id(recovery_token.id)
470 470 assert_equal autologin_token, Token.find_by_id(autologin_token.id)
471 471 end
472 472
473 473 def test_change_on_other_fields_should_not_destroy_tokens
474 474 recovery_token = Token.create!(:user_id => 2, :action => 'recovery')
475 475 autologin_token = Token.create!(:user_id => 2, :action => 'autologin')
476 476
477 477 user = User.find(2)
478 478 user.firstname = "Bobby"
479 479 assert user.save
480 480
481 481 assert_equal recovery_token, Token.find_by_id(recovery_token.id)
482 482 assert_equal autologin_token, Token.find_by_id(autologin_token.id)
483 483 end
484 484
485 485 def test_validate_login_presence
486 486 @admin.login = ""
487 487 assert !@admin.save
488 488 assert_equal 1, @admin.errors.count
489 489 end
490 490
491 491 def test_validate_mail_notification_inclusion
492 492 u = User.new
493 493 u.mail_notification = 'foo'
494 494 u.save
495 495 assert_not_equal [], u.errors[:mail_notification]
496 496 end
497 497
498 498 def test_password
499 499 user = User.try_to_login("admin", "admin")
500 500 assert_kind_of User, user
501 501 assert_equal "admin", user.login
502 502 user.password = "hello123"
503 503 assert user.save
504 504
505 505 user = User.try_to_login("admin", "hello123")
506 506 assert_kind_of User, user
507 507 assert_equal "admin", user.login
508 508 end
509 509
510 510 def test_validate_password_length
511 511 with_settings :password_min_length => '100' do
512 512 user = User.new(:firstname => "new100",
513 513 :lastname => "user100", :mail => "newuser100@somenet.foo")
514 514 user.login = "newuser100"
515 515 user.password, user.password_confirmation = "password100", "password100"
516 516 assert !user.save
517 517 assert_equal 1, user.errors.count
518 518 end
519 519 end
520 520
521 521 def test_name_format
522 522 assert_equal 'John S.', @jsmith.name(:firstname_lastinitial)
523 523 assert_equal 'Smith, John', @jsmith.name(:lastname_comma_firstname)
524 524 assert_equal 'J. Smith', @jsmith.name(:firstinitial_lastname)
525 525 assert_equal 'J.-P. Lang', User.new(:firstname => 'Jean-Philippe', :lastname => 'Lang').name(:firstinitial_lastname)
526 526 end
527 527
528 528 def test_name_should_use_setting_as_default_format
529 529 with_settings :user_format => :firstname_lastname do
530 530 assert_equal 'John Smith', @jsmith.reload.name
531 531 end
532 532 with_settings :user_format => :username do
533 533 assert_equal 'jsmith', @jsmith.reload.name
534 534 end
535 535 with_settings :user_format => :lastname do
536 536 assert_equal 'Smith', @jsmith.reload.name
537 537 end
538 538 end
539 539
540 540 def test_today_should_return_the_day_according_to_user_time_zone
541 541 preference = User.find(1).pref
542 542 date = Date.new(2012, 05, 15)
543 543 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
544 544 Date.stubs(:today).returns(date)
545 545 Time.stubs(:now).returns(time)
546 546
547 547 preference.update_attribute :time_zone, 'Baku' # UTC+4
548 548 assert_equal '2012-05-16', User.find(1).today.to_s
549 549
550 550 preference.update_attribute :time_zone, 'La Paz' # UTC-4
551 551 assert_equal '2012-05-15', User.find(1).today.to_s
552 552
553 553 preference.update_attribute :time_zone, ''
554 554 assert_equal '2012-05-15', User.find(1).today.to_s
555 555 end
556 556
557 557 def test_time_to_date_should_return_the_date_according_to_user_time_zone
558 558 preference = User.find(1).pref
559 559 time = Time.gm(2012, 05, 15, 23, 30).utc # 2012-05-15 23:30 UTC
560 560
561 561 preference.update_attribute :time_zone, 'Baku' # UTC+4
562 562 assert_equal '2012-05-16', User.find(1).time_to_date(time).to_s
563 563
564 564 preference.update_attribute :time_zone, 'La Paz' # UTC-4
565 565 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
566 566
567 567 preference.update_attribute :time_zone, ''
568 568 assert_equal '2012-05-15', User.find(1).time_to_date(time).to_s
569 569 end
570 570
571 571 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
572 572 with_settings :user_format => 'lastname_comma_firstname' do
573 573 assert_equal ['users.lastname', 'users.firstname', 'users.id'],
574 574 User.fields_for_order_statement
575 575 end
576 576 end
577 577
578 578 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
579 579 with_settings :user_format => 'lastname_firstname' do
580 580 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'],
581 581 User.fields_for_order_statement('authors')
582 582 end
583 583 end
584 584
585 585 def test_fields_for_order_statement_with_blank_format_should_return_default
586 586 with_settings :user_format => '' do
587 587 assert_equal ['users.firstname', 'users.lastname', 'users.id'],
588 588 User.fields_for_order_statement
589 589 end
590 590 end
591 591
592 592 def test_fields_for_order_statement_with_invalid_format_should_return_default
593 593 with_settings :user_format => 'foo' do
594 594 assert_equal ['users.firstname', 'users.lastname', 'users.id'],
595 595 User.fields_for_order_statement
596 596 end
597 597 end
598 598
599 599 test ".try_to_login with good credentials should return the user" do
600 600 user = User.try_to_login("admin", "admin")
601 601 assert_kind_of User, user
602 602 assert_equal "admin", user.login
603 603 end
604 604
605 605 test ".try_to_login with wrong credentials should return nil" do
606 606 assert_nil User.try_to_login("admin", "foo")
607 607 end
608 608
609 609 def test_try_to_login_with_locked_user_should_return_nil
610 610 @jsmith.status = User::STATUS_LOCKED
611 611 @jsmith.save!
612 612
613 613 user = User.try_to_login("jsmith", "jsmith")
614 614 assert_equal nil, user
615 615 end
616 616
617 617 def test_try_to_login_with_locked_user_and_not_active_only_should_return_user
618 618 @jsmith.status = User::STATUS_LOCKED
619 619 @jsmith.save!
620 620
621 621 user = User.try_to_login("jsmith", "jsmith", false)
622 622 assert_equal @jsmith, user
623 623 end
624 624
625 625 test ".try_to_login should fall-back to case-insensitive if user login is not found as-typed" do
626 626 user = User.try_to_login("AdMin", "admin")
627 627 assert_kind_of User, user
628 628 assert_equal "admin", user.login
629 629 end
630 630
631 631 test ".try_to_login should select the exact matching user first" do
632 632 case_sensitive_user = User.generate! do |user|
633 633 user.password = "admin123"
634 634 end
635 635 # bypass validations to make it appear like existing data
636 636 case_sensitive_user.update_attribute(:login, 'ADMIN')
637 637
638 638 user = User.try_to_login("ADMIN", "admin123")
639 639 assert_kind_of User, user
640 640 assert_equal "ADMIN", user.login
641 641 end
642 642
643 643 if ldap_configured?
644 644 test "#try_to_login using LDAP with failed connection to the LDAP server" do
645 645 auth_source = AuthSourceLdap.find(1)
646 646 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
647 647
648 648 assert_equal nil, User.try_to_login('edavis', 'wrong')
649 649 end
650 650
651 651 test "#try_to_login using LDAP" do
652 652 assert_equal nil, User.try_to_login('edavis', 'wrong')
653 653 end
654 654
655 655 test "#try_to_login using LDAP binding with user's account" do
656 656 auth_source = AuthSourceLdap.find(1)
657 657 auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
658 658 auth_source.account_password = ''
659 659 auth_source.save!
660 660
661 661 ldap_user = User.new(:mail => 'example1@redmine.org', :firstname => 'LDAP', :lastname => 'user', :auth_source_id => 1)
662 662 ldap_user.login = 'example1'
663 663 ldap_user.save!
664 664
665 665 assert_equal ldap_user, User.try_to_login('example1', '123456')
666 666 assert_nil User.try_to_login('example1', '11111')
667 667 end
668 668
669 669 test "#try_to_login using LDAP on the fly registration" do
670 670 AuthSourceLdap.find(1).update_attribute :onthefly_register, true
671 671
672 672 assert_difference('User.count') do
673 673 assert User.try_to_login('edavis', '123456')
674 674 end
675 675
676 676 assert_no_difference('User.count') do
677 677 assert User.try_to_login('edavis', '123456')
678 678 end
679 679
680 680 assert_nil User.try_to_login('example1', '11111')
681 681 end
682 682
683 683 test "#try_to_login using LDAP on the fly registration and binding with user's account" do
684 684 auth_source = AuthSourceLdap.find(1)
685 685 auth_source.update_attribute :onthefly_register, true
686 686 auth_source = AuthSourceLdap.find(1)
687 687 auth_source.account = "uid=$login,ou=Person,dc=redmine,dc=org"
688 688 auth_source.account_password = ''
689 689 auth_source.save!
690 690
691 691 assert_difference('User.count') do
692 692 assert User.try_to_login('example1', '123456')
693 693 end
694 694
695 695 assert_no_difference('User.count') do
696 696 assert User.try_to_login('example1', '123456')
697 697 end
698 698
699 699 assert_nil User.try_to_login('example1', '11111')
700 700 end
701 701
702 702 else
703 703 puts "Skipping LDAP tests."
704 704 end
705 705
706 706 def test_create_anonymous
707 707 AnonymousUser.delete_all
708 708 anon = User.anonymous
709 709 assert !anon.new_record?
710 710 assert_kind_of AnonymousUser, anon
711 711 end
712 712
713 713 def test_ensure_single_anonymous_user
714 714 AnonymousUser.delete_all
715 715 anon1 = User.anonymous
716 716 assert !anon1.new_record?
717 717 assert_kind_of AnonymousUser, anon1
718 718 anon2 = AnonymousUser.create(
719 719 :lastname => 'Anonymous', :firstname => '',
720 720 :login => '', :status => 0)
721 721 assert_equal 1, anon2.errors.count
722 722 end
723 723
724 724 def test_rss_key
725 725 assert_nil @jsmith.rss_token
726 726 key = @jsmith.rss_key
727 727 assert_equal 40, key.length
728 728
729 729 @jsmith.reload
730 730 assert_equal key, @jsmith.rss_key
731 731 end
732 732
733 733 def test_rss_key_should_not_be_generated_twice
734 734 assert_difference 'Token.count', 1 do
735 735 key1 = @jsmith.rss_key
736 736 key2 = @jsmith.rss_key
737 737 assert_equal key1, key2
738 738 end
739 739 end
740 740
741 741 def test_api_key_should_not_be_generated_twice
742 742 assert_difference 'Token.count', 1 do
743 743 key1 = @jsmith.api_key
744 744 key2 = @jsmith.api_key
745 745 assert_equal key1, key2
746 746 end
747 747 end
748 748
749 749 test "#api_key should generate a new one if the user doesn't have one" do
750 750 user = User.generate!(:api_token => nil)
751 751 assert_nil user.api_token
752 752
753 753 key = user.api_key
754 754 assert_equal 40, key.length
755 755 user.reload
756 756 assert_equal key, user.api_key
757 757 end
758 758
759 759 test "#api_key should return the existing api token value" do
760 760 user = User.generate!
761 761 token = Token.create!(:action => 'api')
762 762 user.api_token = token
763 763 assert user.save
764 764
765 765 assert_equal token.value, user.api_key
766 766 end
767 767
768 768 test "#find_by_api_key should return nil if no matching key is found" do
769 769 assert_nil User.find_by_api_key('zzzzzzzzz')
770 770 end
771 771
772 772 test "#find_by_api_key should return nil if the key is found for an inactive user" do
773 773 user = User.generate!
774 774 user.status = User::STATUS_LOCKED
775 775 token = Token.create!(:action => 'api')
776 776 user.api_token = token
777 777 user.save
778 778
779 779 assert_nil User.find_by_api_key(token.value)
780 780 end
781 781
782 782 test "#find_by_api_key should return the user if the key is found for an active user" do
783 783 user = User.generate!
784 784 token = Token.create!(:action => 'api')
785 785 user.api_token = token
786 786 user.save
787 787
788 788 assert_equal user, User.find_by_api_key(token.value)
789 789 end
790 790
791 791 def test_default_admin_account_changed_should_return_false_if_account_was_not_changed
792 792 user = User.find_by_login("admin")
793 793 user.password = "admin"
794 794 assert user.save(:validate => false)
795 795
796 796 assert_equal false, User.default_admin_account_changed?
797 797 end
798 798
799 799 def test_default_admin_account_changed_should_return_true_if_password_was_changed
800 800 user = User.find_by_login("admin")
801 801 user.password = "newpassword"
802 802 user.save!
803 803
804 804 assert_equal true, User.default_admin_account_changed?
805 805 end
806 806
807 807 def test_default_admin_account_changed_should_return_true_if_account_is_disabled
808 808 user = User.find_by_login("admin")
809 809 user.password = "admin"
810 810 user.status = User::STATUS_LOCKED
811 811 assert user.save(:validate => false)
812 812
813 813 assert_equal true, User.default_admin_account_changed?
814 814 end
815 815
816 816 def test_default_admin_account_changed_should_return_true_if_account_does_not_exist
817 817 user = User.find_by_login("admin")
818 818 user.destroy
819 819
820 820 assert_equal true, User.default_admin_account_changed?
821 821 end
822 822
823 823 def test_membership_with_project_should_return_membership
824 824 project = Project.find(1)
825 825
826 826 membership = @jsmith.membership(project)
827 827 assert_kind_of Member, membership
828 828 assert_equal @jsmith, membership.user
829 829 assert_equal project, membership.project
830 830 end
831 831
832 832 def test_membership_with_project_id_should_return_membership
833 833 project = Project.find(1)
834 834
835 835 membership = @jsmith.membership(1)
836 836 assert_kind_of Member, membership
837 837 assert_equal @jsmith, membership.user
838 838 assert_equal project, membership.project
839 839 end
840 840
841 841 def test_membership_for_non_member_should_return_nil
842 842 project = Project.find(1)
843 843
844 844 user = User.generate!
845 845 membership = user.membership(1)
846 846 assert_nil membership
847 847 end
848 848
849 849 def test_roles_for_project_with_member_on_public_project_should_return_roles_and_non_member
850 850 roles = @jsmith.roles_for_project(Project.find(1))
851 851 assert_kind_of Role, roles.first
852 852 assert_equal ["Manager"], roles.map(&:name)
853 853 end
854 854
855 855 def test_roles_for_project_with_member_on_private_project_should_return_roles
856 856 Project.find(1).update_attribute :is_public, false
857 857
858 858 roles = @jsmith.roles_for_project(Project.find(1))
859 859 assert_kind_of Role, roles.first
860 860 assert_equal ["Manager"], roles.map(&:name)
861 861 end
862 862
863 863 def test_roles_for_project_with_non_member_with_public_project_should_return_non_member
864 864 set_language_if_valid 'en'
865 865 roles = User.find(8).roles_for_project(Project.find(1))
866 866 assert_equal ["Non member"], roles.map(&:name)
867 867 end
868 868
869 869 def test_roles_for_project_with_non_member_with_public_project_and_override_should_return_override_roles
870 870 project = Project.find(1)
871 871 Member.create!(:project => project, :principal => Group.non_member, :role_ids => [1, 2])
872 872 roles = User.find(8).roles_for_project(project)
873 873 assert_equal ["Developer", "Manager"], roles.map(&:name).sort
874 874 end
875 875
876 876 def test_roles_for_project_with_non_member_with_private_project_should_return_no_roles
877 877 Project.find(1).update_attribute :is_public, false
878 878
879 879 roles = User.find(8).roles_for_project(Project.find(1))
880 880 assert_equal [], roles.map(&:name)
881 881 end
882 882
883 883 def test_roles_for_project_with_non_member_with_private_project_and_override_should_return_no_roles
884 884 project = Project.find(1)
885 885 project.update_attribute :is_public, false
886 886 Member.create!(:project => project, :principal => Group.non_member, :role_ids => [1, 2])
887 887 roles = User.find(8).roles_for_project(project)
888 888 assert_equal [], roles.map(&:name).sort
889 889 end
890 890
891 891 def test_roles_for_project_with_anonymous_with_public_project_should_return_anonymous
892 892 set_language_if_valid 'en'
893 893 roles = User.anonymous.roles_for_project(Project.find(1))
894 894 assert_equal ["Anonymous"], roles.map(&:name)
895 895 end
896 896
897 897 def test_roles_for_project_with_anonymous_with_public_project_and_override_should_return_override_roles
898 898 project = Project.find(1)
899 899 Member.create!(:project => project, :principal => Group.anonymous, :role_ids => [1, 2])
900 900 roles = User.anonymous.roles_for_project(project)
901 901 assert_equal ["Developer", "Manager"], roles.map(&:name).sort
902 902 end
903 903
904 904 def test_roles_for_project_with_anonymous_with_private_project_should_return_no_roles
905 905 Project.find(1).update_attribute :is_public, false
906 906
907 907 roles = User.anonymous.roles_for_project(Project.find(1))
908 908 assert_equal [], roles.map(&:name)
909 909 end
910 910
911 911 def test_roles_for_project_with_anonymous_with_private_project_and_override_should_return_no_roles
912 912 project = Project.find(1)
913 913 project.update_attribute :is_public, false
914 914 Member.create!(:project => project, :principal => Group.anonymous, :role_ids => [1, 2])
915 915 roles = User.anonymous.roles_for_project(project)
916 916 assert_equal [], roles.map(&:name).sort
917 917 end
918 918
919 919 def test_roles_for_project_should_be_unique
920 920 m = Member.new(:user_id => 1, :project_id => 1)
921 921 m.member_roles.build(:role_id => 1)
922 922 m.member_roles.build(:role_id => 1)
923 923 m.save!
924 924
925 925 user = User.find(1)
926 926 project = Project.find(1)
927 927 assert_equal 1, user.roles_for_project(project).size
928 928 assert_equal [1], user.roles_for_project(project).map(&:id)
929 929 end
930 930
931 931 def test_projects_by_role_for_user_with_role
932 932 user = User.find(2)
933 933 assert_kind_of Hash, user.projects_by_role
934 934 assert_equal 2, user.projects_by_role.size
935 935 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
936 936 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
937 937 end
938 938
939 939 def test_accessing_projects_by_role_with_no_projects_should_return_an_empty_array
940 940 user = User.find(2)
941 941 assert_equal [], user.projects_by_role[Role.find(3)]
942 942 # should not update the hash
943 943 assert_nil user.projects_by_role.values.detect(&:blank?)
944 944 end
945 945
946 946 def test_projects_by_role_for_user_with_no_role
947 947 user = User.generate!
948 948 assert_equal({}, user.projects_by_role)
949 949 end
950 950
951 951 def test_projects_by_role_for_anonymous
952 952 assert_equal({}, User.anonymous.projects_by_role)
953 953 end
954 954
955 955 def test_valid_notification_options
956 956 # without memberships
957 957 assert_equal 5, User.find(7).valid_notification_options.size
958 958 # with memberships
959 959 assert_equal 6, User.find(2).valid_notification_options.size
960 960 end
961 961
962 962 def test_valid_notification_options_class_method
963 963 assert_equal 5, User.valid_notification_options.size
964 964 assert_equal 5, User.valid_notification_options(User.find(7)).size
965 965 assert_equal 6, User.valid_notification_options(User.find(2)).size
966 966 end
967 967
968 968 def test_notified_project_ids_setter_should_coerce_to_unique_integer_array
969 969 @jsmith.notified_project_ids = ["1", "123", "2u", "wrong", "12", 6, 12, -35, ""]
970 970 assert_equal [1, 123, 2, 12, 6], @jsmith.notified_projects_ids
971 971 end
972 972
973 973 def test_mail_notification_all
974 974 @jsmith.mail_notification = 'all'
975 975 @jsmith.notified_project_ids = []
976 976 @jsmith.save
977 977 @jsmith.reload
978 978 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
979 979 end
980 980
981 981 def test_mail_notification_selected
982 982 @jsmith.mail_notification = 'selected'
983 983 @jsmith.notified_project_ids = [1]
984 984 @jsmith.save
985 985 @jsmith.reload
986 986 assert Project.find(1).recipients.include?(@jsmith.mail)
987 987 end
988 988
989 989 def test_mail_notification_only_my_events
990 990 @jsmith.mail_notification = 'only_my_events'
991 991 @jsmith.notified_project_ids = []
992 992 @jsmith.save
993 993 @jsmith.reload
994 994 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
995 995 end
996 996
997 997 def test_comments_sorting_preference
998 998 assert !@jsmith.wants_comments_in_reverse_order?
999 999 @jsmith.pref.comments_sorting = 'asc'
1000 1000 assert !@jsmith.wants_comments_in_reverse_order?
1001 1001 @jsmith.pref.comments_sorting = 'desc'
1002 1002 assert @jsmith.wants_comments_in_reverse_order?
1003 1003 end
1004 1004
1005 1005 def test_find_by_mail_should_be_case_insensitive
1006 1006 u = User.find_by_mail('JSmith@somenet.foo')
1007 1007 assert_not_nil u
1008 1008 assert_equal 'jsmith@somenet.foo', u.mail
1009 1009 end
1010 1010
1011 1011 def test_random_password
1012 1012 u = User.new
1013 1013 u.random_password
1014 1014 assert !u.password.blank?
1015 1015 assert !u.password_confirmation.blank?
1016 1016 end
1017 1017
1018 1018 test "#change_password_allowed? should be allowed if no auth source is set" do
1019 1019 user = User.generate!
1020 1020 assert user.change_password_allowed?
1021 1021 end
1022 1022
1023 1023 test "#change_password_allowed? should delegate to the auth source" do
1024 1024 user = User.generate!
1025 1025
1026 1026 allowed_auth_source = AuthSource.generate!
1027 1027 def allowed_auth_source.allow_password_changes?; true; end
1028 1028
1029 1029 denied_auth_source = AuthSource.generate!
1030 1030 def denied_auth_source.allow_password_changes?; false; end
1031 1031
1032 1032 assert user.change_password_allowed?
1033 1033
1034 1034 user.auth_source = allowed_auth_source
1035 1035 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
1036 1036
1037 1037 user.auth_source = denied_auth_source
1038 1038 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
1039 1039 end
1040 1040
1041 1041 def test_own_account_deletable_should_be_true_with_unsubscrive_enabled
1042 1042 with_settings :unsubscribe => '1' do
1043 1043 assert_equal true, User.find(2).own_account_deletable?
1044 1044 end
1045 1045 end
1046 1046
1047 1047 def test_own_account_deletable_should_be_false_with_unsubscrive_disabled
1048 1048 with_settings :unsubscribe => '0' do
1049 1049 assert_equal false, User.find(2).own_account_deletable?
1050 1050 end
1051 1051 end
1052 1052
1053 1053 def test_own_account_deletable_should_be_false_for_a_single_admin
1054 User.delete_all(["admin = ? AND id <> ?", true, 1])
1054 User.where(["admin = ? AND id <> ?", true, 1]).delete_all
1055 1055
1056 1056 with_settings :unsubscribe => '1' do
1057 1057 assert_equal false, User.find(1).own_account_deletable?
1058 1058 end
1059 1059 end
1060 1060
1061 1061 def test_own_account_deletable_should_be_true_for_an_admin_if_other_admin_exists
1062 1062 User.generate! do |user|
1063 1063 user.admin = true
1064 1064 end
1065 1065
1066 1066 with_settings :unsubscribe => '1' do
1067 1067 assert_equal true, User.find(1).own_account_deletable?
1068 1068 end
1069 1069 end
1070 1070
1071 1071 test "#allowed_to? for archived project should return false" do
1072 1072 project = Project.find(1)
1073 1073 project.archive
1074 1074 project.reload
1075 1075 assert_equal false, @admin.allowed_to?(:view_issues, project)
1076 1076 end
1077 1077
1078 1078 test "#allowed_to? for closed project should return true for read actions" do
1079 1079 project = Project.find(1)
1080 1080 project.close
1081 1081 project.reload
1082 1082 assert_equal false, @admin.allowed_to?(:edit_project, project)
1083 1083 assert_equal true, @admin.allowed_to?(:view_project, project)
1084 1084 end
1085 1085
1086 1086 test "#allowed_to? for project with module disabled should return false" do
1087 1087 project = Project.find(1)
1088 1088 project.enabled_module_names = ["issue_tracking"]
1089 1089 assert_equal true, @admin.allowed_to?(:add_issues, project)
1090 1090 assert_equal false, @admin.allowed_to?(:view_wiki_pages, project)
1091 1091 end
1092 1092
1093 1093 test "#allowed_to? for admin users should return true" do
1094 1094 project = Project.find(1)
1095 1095 assert ! @admin.member_of?(project)
1096 1096 %w(edit_issues delete_issues manage_news add_documents manage_wiki).each do |p|
1097 1097 assert_equal true, @admin.allowed_to?(p.to_sym, project)
1098 1098 end
1099 1099 end
1100 1100
1101 1101 test "#allowed_to? for normal users" do
1102 1102 project = Project.find(1)
1103 1103 assert_equal true, @jsmith.allowed_to?(:delete_messages, project) #Manager
1104 1104 assert_equal false, @dlopper.allowed_to?(:delete_messages, project) #Developper
1105 1105 end
1106 1106
1107 1107 test "#allowed_to? with empty array should return false" do
1108 1108 assert_equal false, @admin.allowed_to?(:view_project, [])
1109 1109 end
1110 1110
1111 1111 test "#allowed_to? with multiple projects" do
1112 1112 assert_equal true, @admin.allowed_to?(:view_project, Project.all.to_a)
1113 1113 assert_equal false, @dlopper.allowed_to?(:view_project, Project.all.to_a) #cannot see Project(2)
1114 1114 assert_equal true, @jsmith.allowed_to?(:edit_issues, @jsmith.projects.to_a) #Manager or Developer everywhere
1115 1115 assert_equal false, @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects.to_a) #Dev cannot delete_issue_watchers
1116 1116 end
1117 1117
1118 1118 test "#allowed_to? with with options[:global] should return true if user has one role with the permission" do
1119 1119 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
1120 1120 @anonymous = User.find(6)
1121 1121 assert_equal true, @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
1122 1122 assert_equal false, @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
1123 1123 assert_equal true, @dlopper2.allowed_to?(:add_issues, nil, :global => true)
1124 1124 assert_equal false, @anonymous.allowed_to?(:add_issues, nil, :global => true)
1125 1125 assert_equal true, @anonymous.allowed_to?(:view_issues, nil, :global => true)
1126 1126 end
1127 1127
1128 1128 # this is just a proxy method, the test only calls it to ensure it doesn't break trivially
1129 1129 test "#allowed_to_globally?" do
1130 1130 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
1131 1131 @anonymous = User.find(6)
1132 1132 assert_equal true, @jsmith.allowed_to_globally?(:delete_issue_watchers)
1133 1133 assert_equal false, @dlopper2.allowed_to_globally?(:delete_issue_watchers)
1134 1134 assert_equal true, @dlopper2.allowed_to_globally?(:add_issues)
1135 1135 assert_equal false, @anonymous.allowed_to_globally?(:add_issues)
1136 1136 assert_equal true, @anonymous.allowed_to_globally?(:view_issues)
1137 1137 end
1138 1138
1139 1139 def test_notify_about_issue
1140 1140 project = Project.find(1)
1141 1141 author = User.generate!
1142 1142 assignee = User.generate!
1143 1143 member = User.generate!
1144 1144 Member.create!(:user => member, :project => project, :role_ids => [1])
1145 1145 issue = Issue.generate!(:project => project, :assigned_to => assignee, :author => author)
1146 1146
1147 1147 tests = {
1148 1148 author => %w(all only_my_events only_owner selected),
1149 1149 assignee => %w(all only_my_events only_assigned selected),
1150 1150 member => %w(all)
1151 1151 }
1152 1152
1153 1153 tests.each do |user, expected|
1154 1154 User::MAIL_NOTIFICATION_OPTIONS.map(&:first).each do |option|
1155 1155 user.mail_notification = option
1156 1156 assert_equal expected.include?(option), user.notify_about?(issue)
1157 1157 end
1158 1158 end
1159 1159 end
1160 1160
1161 1161 def test_notify_about_issue_for_previous_assignee
1162 1162 assignee = User.generate!(:mail_notification => 'only_assigned')
1163 1163 new_assignee = User.generate!(:mail_notification => 'only_assigned')
1164 1164 issue = Issue.generate!(:assigned_to => assignee)
1165 1165
1166 1166 assert assignee.notify_about?(issue)
1167 1167 assert !new_assignee.notify_about?(issue)
1168 1168
1169 1169 issue.assigned_to = new_assignee
1170 1170 assert assignee.notify_about?(issue)
1171 1171 assert new_assignee.notify_about?(issue)
1172 1172
1173 1173 issue.save!
1174 1174 assert !assignee.notify_about?(issue)
1175 1175 assert new_assignee.notify_about?(issue)
1176 1176 end
1177 1177
1178 1178 def test_notify_about_news
1179 1179 user = User.generate!
1180 1180 news = News.new
1181 1181
1182 1182 User::MAIL_NOTIFICATION_OPTIONS.map(&:first).each do |option|
1183 1183 user.mail_notification = option
1184 1184 assert_equal (option != 'none'), user.notify_about?(news)
1185 1185 end
1186 1186 end
1187 1187
1188 1188 def test_salt_unsalted_passwords
1189 1189 # Restore a user with an unsalted password
1190 1190 user = User.find(1)
1191 1191 user.salt = nil
1192 1192 user.hashed_password = User.hash_password("unsalted")
1193 1193 user.save!
1194 1194
1195 1195 User.salt_unsalted_passwords!
1196 1196
1197 1197 user.reload
1198 1198 # Salt added
1199 1199 assert !user.salt.blank?
1200 1200 # Password still valid
1201 1201 assert user.check_password?("unsalted")
1202 1202 assert_equal user, User.try_to_login(user.login, "unsalted")
1203 1203 end
1204 1204
1205 1205 if Object.const_defined?(:OpenID)
1206 1206 def test_setting_identity_url
1207 1207 normalized_open_id_url = 'http://example.com/'
1208 1208 u = User.new( :identity_url => 'http://example.com/' )
1209 1209 assert_equal normalized_open_id_url, u.identity_url
1210 1210 end
1211 1211
1212 1212 def test_setting_identity_url_without_trailing_slash
1213 1213 normalized_open_id_url = 'http://example.com/'
1214 1214 u = User.new( :identity_url => 'http://example.com' )
1215 1215 assert_equal normalized_open_id_url, u.identity_url
1216 1216 end
1217 1217
1218 1218 def test_setting_identity_url_without_protocol
1219 1219 normalized_open_id_url = 'http://example.com/'
1220 1220 u = User.new( :identity_url => 'example.com' )
1221 1221 assert_equal normalized_open_id_url, u.identity_url
1222 1222 end
1223 1223
1224 1224 def test_setting_blank_identity_url
1225 1225 u = User.new( :identity_url => 'example.com' )
1226 1226 u.identity_url = ''
1227 1227 assert u.identity_url.blank?
1228 1228 end
1229 1229
1230 1230 def test_setting_invalid_identity_url
1231 1231 u = User.new( :identity_url => 'this is not an openid url' )
1232 1232 assert u.identity_url.blank?
1233 1233 end
1234 1234 else
1235 1235 puts "Skipping openid tests."
1236 1236 end
1237 1237 end
@@ -1,201 +1,201
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 WatcherTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :email_addresses, :members, :member_roles, :roles, :enabled_modules,
22 22 :issues, :issue_statuses, :enumerations, :trackers, :projects_trackers,
23 23 :boards, :messages,
24 24 :wikis, :wiki_pages,
25 25 :watchers
26 26
27 27 def setup
28 28 @user = User.find(1)
29 29 @issue = Issue.find(1)
30 30 end
31 31
32 32 def test_validate
33 33 user = User.find(5)
34 34 assert !user.active?
35 35 watcher = Watcher.new(:user_id => user.id)
36 36 assert !watcher.save
37 37 end
38 38
39 39 def test_watch
40 40 assert @issue.add_watcher(@user)
41 41 @issue.reload
42 42 assert @issue.watchers.detect { |w| w.user == @user }
43 43 end
44 44
45 45 def test_cant_watch_twice
46 46 assert @issue.add_watcher(@user)
47 47 assert !@issue.add_watcher(@user)
48 48 end
49 49
50 50 def test_watched_by
51 51 assert @issue.add_watcher(@user)
52 52 @issue.reload
53 53 assert @issue.watched_by?(@user)
54 54 assert Issue.watched_by(@user).include?(@issue)
55 55 end
56 56
57 57 def test_watcher_users
58 58 watcher_users = Issue.find(2).watcher_users
59 59 assert_kind_of Array, watcher_users.collect{|w| w}
60 60 assert_kind_of User, watcher_users.first
61 61 end
62 62
63 63 def test_watcher_users_should_be_reloaded_after_adding_a_watcher
64 64 issue = Issue.find(2)
65 65 user = User.generate!
66 66
67 67 assert_difference 'issue.watcher_users.to_a.size' do
68 68 issue.add_watcher user
69 69 end
70 70 end
71 71
72 72 def test_watcher_users_should_not_validate_user
73 73 User.where(:id => 1).update_all("firstname = ''")
74 74 @user.reload
75 75 assert !@user.valid?
76 76
77 77 issue = Issue.new(:project => Project.find(1), :tracker_id => 1, :subject => "test", :author => User.find(2))
78 78 issue.watcher_users << @user
79 79 issue.save!
80 80 assert issue.watched_by?(@user)
81 81 end
82 82
83 83 def test_watcher_user_ids
84 84 assert_equal [1, 3], Issue.find(2).watcher_user_ids.sort
85 85 end
86 86
87 87 def test_watcher_user_ids=
88 88 issue = Issue.new
89 89 issue.watcher_user_ids = ['1', '3']
90 90 assert issue.watched_by?(User.find(1))
91 91 end
92 92
93 93 def test_watcher_user_ids_should_make_ids_uniq
94 94 issue = Issue.new(:project => Project.find(1), :tracker_id => 1, :subject => "test", :author => User.find(2))
95 95 issue.watcher_user_ids = ['1', '3', '1']
96 96 issue.save!
97 97 assert_equal 2, issue.watchers.count
98 98 end
99 99
100 100 def test_addable_watcher_users
101 101 addable_watcher_users = @issue.addable_watcher_users
102 102 assert_kind_of Array, addable_watcher_users
103 103 assert_kind_of User, addable_watcher_users.first
104 104 end
105 105
106 106 def test_addable_watcher_users_should_not_include_user_that_cannot_view_the_object
107 107 issue = Issue.new(:project => Project.find(1), :is_private => true)
108 108 assert_nil issue.addable_watcher_users.detect {|user| !issue.visible?(user)}
109 109 end
110 110
111 111 def test_any_watched_should_return_false_if_no_object_is_watched
112 112 objects = (0..2).map {Issue.generate!}
113 113
114 114 assert_equal false, Watcher.any_watched?(objects, @user)
115 115 end
116 116
117 117 def test_any_watched_should_return_true_if_one_object_is_watched
118 118 objects = (0..2).map {Issue.generate!}
119 119 objects.last.add_watcher(@user)
120 120
121 121 assert_equal true, Watcher.any_watched?(objects, @user)
122 122 end
123 123
124 124 def test_any_watched_should_return_false_with_no_object
125 125 assert_equal false, Watcher.any_watched?([], @user)
126 126 end
127 127
128 128 def test_recipients
129 129 @issue.watchers.delete_all
130 130 @issue.reload
131 131
132 132 assert @issue.watcher_recipients.empty?
133 133 assert @issue.add_watcher(@user)
134 134
135 135 @user.mail_notification = 'all'
136 136 @user.save!
137 137 @issue.reload
138 138 assert @issue.watcher_recipients.include?(@user.mail)
139 139
140 140 @user.mail_notification = 'none'
141 141 @user.save!
142 142 @issue.reload
143 143 assert !@issue.watcher_recipients.include?(@user.mail)
144 144 end
145 145
146 146 def test_unwatch
147 147 assert @issue.add_watcher(@user)
148 148 @issue.reload
149 149 assert_equal 1, @issue.remove_watcher(@user)
150 150 end
151 151
152 152 def test_prune_with_user
153 Watcher.delete_all("user_id = 9")
153 Watcher.where("user_id = 9").delete_all
154 154 user = User.find(9)
155 155
156 156 # public
157 157 Watcher.create!(:watchable => Issue.find(1), :user => user)
158 158 Watcher.create!(:watchable => Issue.find(2), :user => user)
159 159 Watcher.create!(:watchable => Message.find(1), :user => user)
160 160 Watcher.create!(:watchable => Wiki.find(1), :user => user)
161 161 Watcher.create!(:watchable => WikiPage.find(2), :user => user)
162 162
163 163 # private project (id: 2)
164 164 Member.create!(:project => Project.find(2), :principal => user, :role_ids => [1])
165 165 Watcher.create!(:watchable => Issue.find(4), :user => user)
166 166 Watcher.create!(:watchable => Message.find(7), :user => user)
167 167 Watcher.create!(:watchable => Wiki.find(2), :user => user)
168 168 Watcher.create!(:watchable => WikiPage.find(3), :user => user)
169 169
170 170 assert_no_difference 'Watcher.count' do
171 171 Watcher.prune(:user => User.find(9))
172 172 end
173 173
174 174 Member.delete_all
175 175
176 176 assert_difference 'Watcher.count', -4 do
177 177 Watcher.prune(:user => User.find(9))
178 178 end
179 179
180 180 assert Issue.find(1).watched_by?(user)
181 181 assert !Issue.find(4).watched_by?(user)
182 182 end
183 183
184 184 def test_prune_with_project
185 185 user = User.find(9)
186 186 Watcher.new(:watchable => Issue.find(4), :user => User.find(9)).save(:validate => false) # project 2
187 187 Watcher.new(:watchable => Issue.find(6), :user => User.find(9)).save(:validate => false) # project 5
188 188
189 189 assert Watcher.prune(:project => Project.find(5)) > 0
190 190 assert Issue.find(4).watched_by?(user)
191 191 assert !Issue.find(6).watched_by?(user)
192 192 end
193 193
194 194 def test_prune_all
195 195 user = User.find(9)
196 196 Watcher.new(:watchable => Issue.find(4), :user => User.find(9)).save(:validate => false)
197 197
198 198 assert Watcher.prune > 0
199 199 assert !Issue.find(4).watched_by?(user)
200 200 end
201 201 end
General Comments 0
You need to be logged in to leave comments. Login now