##// END OF EJS Templates
Memorize project override roles....
Jean-Philippe Lang -
r13552:9d3f3289b887
parent child
Show More
@@ -1,1032 +1,1035
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 :members,
33 33 lambda { joins(:principal, :roles).
34 34 where("#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}") }
35 35 has_many :memberships, :class_name => 'Member'
36 36 has_many :member_principals,
37 37 lambda { joins(:principal).
38 38 where("#{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}")},
39 39 :class_name => 'Member'
40 40 has_many :enabled_modules, :dependent => :delete_all
41 41 has_and_belongs_to_many :trackers, lambda {order(:position)}
42 42 has_many :issues, :dependent => :destroy
43 43 has_many :issue_changes, :through => :issues, :source => :journals
44 44 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
45 45 has_many :time_entries, :dependent => :destroy
46 46 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
47 47 has_many :documents, :dependent => :destroy
48 48 has_many :news, lambda {includes(:author)}, :dependent => :destroy
49 49 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
50 50 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
51 51 has_one :repository, lambda {where(["is_default = ?", true])}
52 52 has_many :repositories, :dependent => :destroy
53 53 has_many :changesets, :through => :repository
54 54 has_one :wiki, :dependent => :destroy
55 55 # Custom field for the project issues
56 56 has_and_belongs_to_many :issue_custom_fields,
57 57 lambda {order("#{CustomField.table_name}.position")},
58 58 :class_name => 'IssueCustomField',
59 59 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
60 60 :association_foreign_key => 'custom_field_id'
61 61
62 62 acts_as_attachable :view_permission => :view_files,
63 63 :edit_permission => :manage_files,
64 64 :delete_permission => :manage_files
65 65
66 66 acts_as_customizable
67 67 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
68 68 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
69 69 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
70 70 :author => nil
71 71
72 72 attr_protected :status
73 73
74 74 validates_presence_of :name, :identifier
75 75 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
76 76 validates_length_of :name, :maximum => 255
77 77 validates_length_of :homepage, :maximum => 255
78 78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 79 # downcase letters, digits, dashes but not digits only
80 80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 81 # reserved words
82 82 validates_exclusion_of :identifier, :in => %w( new )
83 83 validate :validate_parent
84 84
85 85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
86 86 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
87 87 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
88 88 before_destroy :delete_all_members
89 89
90 90 scope :has_module, lambda {|mod|
91 91 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
92 92 }
93 93 scope :active, lambda { where(:status => STATUS_ACTIVE) }
94 94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
95 95 scope :all_public, lambda { where(:is_public => true) }
96 96 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
97 97 scope :allowed_to, lambda {|*args|
98 98 user = User.current
99 99 permission = nil
100 100 if args.first.is_a?(Symbol)
101 101 permission = args.shift
102 102 else
103 103 user = args.shift
104 104 permission = args.shift
105 105 end
106 106 where(Project.allowed_to_condition(user, permission, *args))
107 107 }
108 108 scope :like, lambda {|arg|
109 109 if arg.blank?
110 110 where(nil)
111 111 else
112 112 pattern = "%#{arg.to_s.strip.downcase}%"
113 113 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
114 114 end
115 115 }
116 116 scope :sorted, lambda {order(:lft)}
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).order("created_on DESC").to_a
153 153 end
154 154
155 155 # Returns true if the project is visible to +user+ or to the current user.
156 156 def visible?(user=User.current)
157 157 user.allowed_to?(:view_project, self)
158 158 end
159 159
160 160 # Returns a SQL conditions string used to find all projects visible by the specified user.
161 161 #
162 162 # Examples:
163 163 # Project.visible_condition(admin) => "projects.status = 1"
164 164 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
165 165 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
166 166 def self.visible_condition(user, options={})
167 167 allowed_to_condition(user, :view_project, options)
168 168 end
169 169
170 170 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
171 171 #
172 172 # Valid options:
173 173 # * :project => limit the condition to project
174 174 # * :with_subprojects => limit the condition to project and its subprojects
175 175 # * :member => limit the condition to the user projects
176 176 def self.allowed_to_condition(user, permission, options={})
177 177 perm = Redmine::AccessControl.permission(permission)
178 178 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
179 179 if perm && perm.project_module
180 180 # If the permission belongs to a project module, make sure the module is enabled
181 181 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
182 182 end
183 183 if project = options[:project]
184 184 project_statement = project.project_condition(options[:with_subprojects])
185 185 base_statement = "(#{project_statement}) AND (#{base_statement})"
186 186 end
187 187
188 188 if user.admin?
189 189 base_statement
190 190 else
191 191 statement_by_role = {}
192 192 unless options[:member]
193 193 role = user.builtin_role
194 194 if role.allowed_to?(permission)
195 195 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
196 196 end
197 197 end
198 198 user.projects_by_role.each do |role, projects|
199 199 if role.allowed_to?(permission) && projects.any?
200 200 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
201 201 end
202 202 end
203 203 if statement_by_role.empty?
204 204 "1=0"
205 205 else
206 206 if block_given?
207 207 statement_by_role.each do |role, statement|
208 208 if s = yield(role, user)
209 209 statement_by_role[role] = "(#{statement} AND (#{s}))"
210 210 end
211 211 end
212 212 end
213 213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
214 214 end
215 215 end
216 216 end
217 217
218 218 def override_roles(role)
219 @override_members ||= member_principals.
220 where("#{Principal.table_name}.type IN (?)", ['GroupAnonymous', 'GroupNonMember']).to_a
221
219 222 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
220 member = member_principals.where("#{Principal.table_name}.type = ?", group_class.name).first
223 member = @override_members.detect {|m| m.principal.is_a? group_class}
221 224 member ? member.roles.to_a : [role]
222 225 end
223 226
224 227 def principals
225 228 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
226 229 end
227 230
228 231 def users
229 232 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
230 233 end
231 234
232 235 # Returns the Systemwide and project specific activities
233 236 def activities(include_inactive=false)
234 237 if include_inactive
235 238 return all_activities
236 239 else
237 240 return active_activities
238 241 end
239 242 end
240 243
241 244 # Will create a new Project specific Activity or update an existing one
242 245 #
243 246 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
244 247 # does not successfully save.
245 248 def update_or_create_time_entry_activity(id, activity_hash)
246 249 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
247 250 self.create_time_entry_activity_if_needed(activity_hash)
248 251 else
249 252 activity = project.time_entry_activities.find_by_id(id.to_i)
250 253 activity.update_attributes(activity_hash) if activity
251 254 end
252 255 end
253 256
254 257 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
255 258 #
256 259 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
257 260 # does not successfully save.
258 261 def create_time_entry_activity_if_needed(activity)
259 262 if activity['parent_id']
260 263 parent_activity = TimeEntryActivity.find(activity['parent_id'])
261 264 activity['name'] = parent_activity.name
262 265 activity['position'] = parent_activity.position
263 266 if Enumeration.overriding_change?(activity, parent_activity)
264 267 project_activity = self.time_entry_activities.create(activity)
265 268 if project_activity.new_record?
266 269 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
267 270 else
268 271 self.time_entries.
269 272 where(["activity_id = ?", parent_activity.id]).
270 273 update_all("activity_id = #{project_activity.id}")
271 274 end
272 275 end
273 276 end
274 277 end
275 278
276 279 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
277 280 #
278 281 # Examples:
279 282 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
280 283 # project.project_condition(false) => "projects.id = 1"
281 284 def project_condition(with_subprojects)
282 285 cond = "#{Project.table_name}.id = #{id}"
283 286 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
284 287 cond
285 288 end
286 289
287 290 def self.find(*args)
288 291 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
289 292 project = find_by_identifier(*args)
290 293 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
291 294 project
292 295 else
293 296 super
294 297 end
295 298 end
296 299
297 300 def self.find_by_param(*args)
298 301 self.find(*args)
299 302 end
300 303
301 304 alias :base_reload :reload
302 305 def reload(*args)
303 306 @principals = nil
304 307 @users = nil
305 308 @shared_versions = nil
306 309 @rolled_up_versions = nil
307 310 @rolled_up_trackers = nil
308 311 @all_issue_custom_fields = nil
309 312 @all_time_entry_custom_fields = nil
310 313 @to_param = nil
311 314 @allowed_parents = nil
312 315 @allowed_permissions = nil
313 316 @actions_allowed = nil
314 317 @start_date = nil
315 318 @due_date = nil
316 319 @override_members = nil
317 320 @assignable_users = nil
318 321 base_reload(*args)
319 322 end
320 323
321 324 def to_param
322 325 # id is used for projects with a numeric identifier (compatibility)
323 326 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
324 327 end
325 328
326 329 def active?
327 330 self.status == STATUS_ACTIVE
328 331 end
329 332
330 333 def archived?
331 334 self.status == STATUS_ARCHIVED
332 335 end
333 336
334 337 # Archives the project and its descendants
335 338 def archive
336 339 # Check that there is no issue of a non descendant project that is assigned
337 340 # to one of the project or descendant versions
338 341 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
339 342
340 343 if version_ids.any? &&
341 344 Issue.
342 345 includes(:project).
343 346 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
344 347 where(:fixed_version_id => version_ids).
345 348 exists?
346 349 return false
347 350 end
348 351 Project.transaction do
349 352 archive!
350 353 end
351 354 true
352 355 end
353 356
354 357 # Unarchives the project
355 358 # All its ancestors must be active
356 359 def unarchive
357 360 return false if ancestors.detect {|a| !a.active?}
358 361 update_attribute :status, STATUS_ACTIVE
359 362 end
360 363
361 364 def close
362 365 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
363 366 end
364 367
365 368 def reopen
366 369 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
367 370 end
368 371
369 372 # Returns an array of projects the project can be moved to
370 373 # by the current user
371 374 def allowed_parents(user=User.current)
372 375 return @allowed_parents if @allowed_parents
373 376 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
374 377 @allowed_parents = @allowed_parents - self_and_descendants
375 378 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
376 379 @allowed_parents << nil
377 380 end
378 381 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
379 382 @allowed_parents << parent
380 383 end
381 384 @allowed_parents
382 385 end
383 386
384 387 # Sets the parent of the project with authorization check
385 388 def set_allowed_parent!(p)
386 389 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
387 390 p = p.id if p.is_a?(Project)
388 391 send :safe_attributes, {:project_id => p}
389 392 save
390 393 end
391 394
392 395 # Sets the parent of the project and saves the project
393 396 # Argument can be either a Project, a String, a Fixnum or nil
394 397 def set_parent!(p)
395 398 if p.is_a?(Project)
396 399 self.parent = p
397 400 else
398 401 self.parent_id = p
399 402 end
400 403 save
401 404 end
402 405
403 406 # Returns an array of the trackers used by the project and its active sub projects
404 407 def rolled_up_trackers
405 408 @rolled_up_trackers ||=
406 409 Tracker.
407 410 joins(:projects).
408 411 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
409 412 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
410 413 uniq.
411 414 sorted.
412 415 to_a
413 416 end
414 417
415 418 # Closes open and locked project versions that are completed
416 419 def close_completed_versions
417 420 Version.transaction do
418 421 versions.where(:status => %w(open locked)).each do |version|
419 422 if version.completed?
420 423 version.update_attribute(:status, 'closed')
421 424 end
422 425 end
423 426 end
424 427 end
425 428
426 429 # Returns a scope of the Versions on subprojects
427 430 def rolled_up_versions
428 431 @rolled_up_versions ||=
429 432 Version.
430 433 joins(:project).
431 434 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
432 435 end
433 436
434 437 # Returns a scope of the Versions used by the project
435 438 def shared_versions
436 439 if new_record?
437 440 Version.
438 441 joins(:project).
439 442 preload(:project).
440 443 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
441 444 else
442 445 @shared_versions ||= begin
443 446 r = root? ? self : root
444 447 Version.
445 448 joins(:project).
446 449 preload(:project).
447 450 where("#{Project.table_name}.id = #{id}" +
448 451 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
449 452 " #{Version.table_name}.sharing = 'system'" +
450 453 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
451 454 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
452 455 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
453 456 "))")
454 457 end
455 458 end
456 459 end
457 460
458 461 # Returns a hash of project users grouped by role
459 462 def users_by_role
460 463 members.includes(:user, :roles).inject({}) do |h, m|
461 464 m.roles.each do |r|
462 465 h[r] ||= []
463 466 h[r] << m.user
464 467 end
465 468 h
466 469 end
467 470 end
468 471
469 472 # Adds user as a project member with the default role
470 473 # Used for when a non-admin user creates a project
471 474 def add_default_member(user)
472 475 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
473 476 member = Member.new(:project => self, :principal => user, :roles => [role])
474 477 self.members << member
475 478 member
476 479 end
477 480
478 481 # Deletes all project's members
479 482 def delete_all_members
480 483 me, mr = Member.table_name, MemberRole.table_name
481 484 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
482 485 Member.delete_all(['project_id = ?', id])
483 486 end
484 487
485 488 # Return a Principal scope of users/groups issues can be assigned to
486 489 def assignable_users
487 490 types = ['User']
488 491 types << 'Group' if Setting.issue_group_assignment?
489 492
490 493 @assignable_users ||= Principal.
491 494 active.
492 495 joins(:members => :roles).
493 496 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
494 497 uniq.
495 498 sorted
496 499 end
497 500
498 501 # Returns the mail addresses of users that should be always notified on project events
499 502 def recipients
500 503 notified_users.collect {|user| user.mail}
501 504 end
502 505
503 506 # Returns the users that should be notified on project events
504 507 def notified_users
505 508 # TODO: User part should be extracted to User#notify_about?
506 509 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
507 510 end
508 511
509 512 # Returns a scope of all custom fields enabled for project issues
510 513 # (explicitly associated custom fields and custom fields enabled for all projects)
511 514 def all_issue_custom_fields
512 515 @all_issue_custom_fields ||= IssueCustomField.
513 516 sorted.
514 517 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
515 518 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
516 519 " WHERE cfp.project_id = ?)", true, id)
517 520 end
518 521
519 522 def project
520 523 self
521 524 end
522 525
523 526 def <=>(project)
524 527 name.downcase <=> project.name.downcase
525 528 end
526 529
527 530 def to_s
528 531 name
529 532 end
530 533
531 534 # Returns a short description of the projects (first lines)
532 535 def short_description(length = 255)
533 536 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
534 537 end
535 538
536 539 def css_classes
537 540 s = 'project'
538 541 s << ' root' if root?
539 542 s << ' child' if child?
540 543 s << (leaf? ? ' leaf' : ' parent')
541 544 unless active?
542 545 if archived?
543 546 s << ' archived'
544 547 else
545 548 s << ' closed'
546 549 end
547 550 end
548 551 s
549 552 end
550 553
551 554 # The earliest start date of a project, based on it's issues and versions
552 555 def start_date
553 556 @start_date ||= [
554 557 issues.minimum('start_date'),
555 558 shared_versions.minimum('effective_date'),
556 559 Issue.fixed_version(shared_versions).minimum('start_date')
557 560 ].compact.min
558 561 end
559 562
560 563 # The latest due date of an issue or version
561 564 def due_date
562 565 @due_date ||= [
563 566 issues.maximum('due_date'),
564 567 shared_versions.maximum('effective_date'),
565 568 Issue.fixed_version(shared_versions).maximum('due_date')
566 569 ].compact.max
567 570 end
568 571
569 572 def overdue?
570 573 active? && !due_date.nil? && (due_date < Date.today)
571 574 end
572 575
573 576 # Returns the percent completed for this project, based on the
574 577 # progress on it's versions.
575 578 def completed_percent(options={:include_subprojects => false})
576 579 if options.delete(:include_subprojects)
577 580 total = self_and_descendants.collect(&:completed_percent).sum
578 581
579 582 total / self_and_descendants.count
580 583 else
581 584 if versions.count > 0
582 585 total = versions.collect(&:completed_percent).sum
583 586
584 587 total / versions.count
585 588 else
586 589 100
587 590 end
588 591 end
589 592 end
590 593
591 594 # Return true if this project allows to do the specified action.
592 595 # action can be:
593 596 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
594 597 # * a permission Symbol (eg. :edit_project)
595 598 def allows_to?(action)
596 599 if archived?
597 600 # No action allowed on archived projects
598 601 return false
599 602 end
600 603 unless active? || Redmine::AccessControl.read_action?(action)
601 604 # No write action allowed on closed projects
602 605 return false
603 606 end
604 607 # No action allowed on disabled modules
605 608 if action.is_a? Hash
606 609 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
607 610 else
608 611 allowed_permissions.include? action
609 612 end
610 613 end
611 614
612 615 # Return the enabled module with the given name
613 616 # or nil if the module is not enabled for the project
614 617 def enabled_module(name)
615 618 name = name.to_s
616 619 enabled_modules.detect {|m| m.name == name}
617 620 end
618 621
619 622 # Return true if the module with the given name is enabled
620 623 def module_enabled?(name)
621 624 enabled_module(name).present?
622 625 end
623 626
624 627 def enabled_module_names=(module_names)
625 628 if module_names && module_names.is_a?(Array)
626 629 module_names = module_names.collect(&:to_s).reject(&:blank?)
627 630 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
628 631 else
629 632 enabled_modules.clear
630 633 end
631 634 end
632 635
633 636 # Returns an array of the enabled modules names
634 637 def enabled_module_names
635 638 enabled_modules.collect(&:name)
636 639 end
637 640
638 641 # Enable a specific module
639 642 #
640 643 # Examples:
641 644 # project.enable_module!(:issue_tracking)
642 645 # project.enable_module!("issue_tracking")
643 646 def enable_module!(name)
644 647 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
645 648 end
646 649
647 650 # Disable a module if it exists
648 651 #
649 652 # Examples:
650 653 # project.disable_module!(:issue_tracking)
651 654 # project.disable_module!("issue_tracking")
652 655 # project.disable_module!(project.enabled_modules.first)
653 656 def disable_module!(target)
654 657 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
655 658 target.destroy unless target.blank?
656 659 end
657 660
658 661 safe_attributes 'name',
659 662 'description',
660 663 'homepage',
661 664 'is_public',
662 665 'identifier',
663 666 'custom_field_values',
664 667 'custom_fields',
665 668 'tracker_ids',
666 669 'issue_custom_field_ids',
667 670 'parent_id'
668 671
669 672 safe_attributes 'enabled_module_names',
670 673 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
671 674
672 675 safe_attributes 'inherit_members',
673 676 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
674 677
675 678 def safe_attributes=(attrs, user=User.current)
676 679 return unless attrs.is_a?(Hash)
677 680 attrs = attrs.deep_dup
678 681
679 682 @unallowed_parent_id = nil
680 683 parent_id_param = attrs['parent_id'].to_s
681 684 if parent_id_param.blank? || parent_id_param != parent_id.to_s
682 685 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
683 686 unless allowed_parents(user).include?(p)
684 687 attrs.delete('parent_id')
685 688 @unallowed_parent_id = true
686 689 end
687 690 end
688 691
689 692 super(attrs, user)
690 693 end
691 694
692 695 # Returns an auto-generated project identifier based on the last identifier used
693 696 def self.next_identifier
694 697 p = Project.order('id DESC').first
695 698 p.nil? ? nil : p.identifier.to_s.succ
696 699 end
697 700
698 701 # Copies and saves the Project instance based on the +project+.
699 702 # Duplicates the source project's:
700 703 # * Wiki
701 704 # * Versions
702 705 # * Categories
703 706 # * Issues
704 707 # * Members
705 708 # * Queries
706 709 #
707 710 # Accepts an +options+ argument to specify what to copy
708 711 #
709 712 # Examples:
710 713 # project.copy(1) # => copies everything
711 714 # project.copy(1, :only => 'members') # => copies members only
712 715 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
713 716 def copy(project, options={})
714 717 project = project.is_a?(Project) ? project : Project.find(project)
715 718
716 719 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
717 720 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
718 721
719 722 Project.transaction do
720 723 if save
721 724 reload
722 725 to_be_copied.each do |name|
723 726 send "copy_#{name}", project
724 727 end
725 728 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
726 729 save
727 730 else
728 731 false
729 732 end
730 733 end
731 734 end
732 735
733 736 # Returns a new unsaved Project instance with attributes copied from +project+
734 737 def self.copy_from(project)
735 738 project = project.is_a?(Project) ? project : Project.find(project)
736 739 # clear unique attributes
737 740 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
738 741 copy = Project.new(attributes)
739 742 copy.enabled_modules = project.enabled_modules
740 743 copy.trackers = project.trackers
741 744 copy.custom_values = project.custom_values.collect {|v| v.clone}
742 745 copy.issue_custom_fields = project.issue_custom_fields
743 746 copy
744 747 end
745 748
746 749 # Yields the given block for each project with its level in the tree
747 750 def self.project_tree(projects, &block)
748 751 ancestors = []
749 752 projects.sort_by(&:lft).each do |project|
750 753 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
751 754 ancestors.pop
752 755 end
753 756 yield project, ancestors.size
754 757 ancestors << project
755 758 end
756 759 end
757 760
758 761 private
759 762
760 763 def update_inherited_members
761 764 if parent
762 765 if inherit_members? && !inherit_members_was
763 766 remove_inherited_member_roles
764 767 add_inherited_member_roles
765 768 elsif !inherit_members? && inherit_members_was
766 769 remove_inherited_member_roles
767 770 end
768 771 end
769 772 end
770 773
771 774 def remove_inherited_member_roles
772 775 member_roles = memberships.map(&:member_roles).flatten
773 776 member_role_ids = member_roles.map(&:id)
774 777 member_roles.each do |member_role|
775 778 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
776 779 member_role.destroy
777 780 end
778 781 end
779 782 end
780 783
781 784 def add_inherited_member_roles
782 785 if inherit_members? && parent
783 786 parent.memberships.each do |parent_member|
784 787 member = Member.find_or_new(self.id, parent_member.user_id)
785 788 parent_member.member_roles.each do |parent_member_role|
786 789 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
787 790 end
788 791 member.save!
789 792 end
790 793 memberships.reset
791 794 end
792 795 end
793 796
794 797 def update_versions_from_hierarchy_change
795 798 Issue.update_versions_from_hierarchy_change(self)
796 799 end
797 800
798 801 def validate_parent
799 802 if @unallowed_parent_id
800 803 errors.add(:parent_id, :invalid)
801 804 elsif parent_id_changed?
802 805 unless parent.nil? || (parent.active? && move_possible?(parent))
803 806 errors.add(:parent_id, :invalid)
804 807 end
805 808 end
806 809 end
807 810
808 811 # Copies wiki from +project+
809 812 def copy_wiki(project)
810 813 # Check that the source project has a wiki first
811 814 unless project.wiki.nil?
812 815 wiki = self.wiki || Wiki.new
813 816 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
814 817 wiki_pages_map = {}
815 818 project.wiki.pages.each do |page|
816 819 # Skip pages without content
817 820 next if page.content.nil?
818 821 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
819 822 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
820 823 new_wiki_page.content = new_wiki_content
821 824 wiki.pages << new_wiki_page
822 825 wiki_pages_map[page.id] = new_wiki_page
823 826 end
824 827
825 828 self.wiki = wiki
826 829 wiki.save
827 830 # Reproduce page hierarchy
828 831 project.wiki.pages.each do |page|
829 832 if page.parent_id && wiki_pages_map[page.id]
830 833 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
831 834 wiki_pages_map[page.id].save
832 835 end
833 836 end
834 837 end
835 838 end
836 839
837 840 # Copies versions from +project+
838 841 def copy_versions(project)
839 842 project.versions.each do |version|
840 843 new_version = Version.new
841 844 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
842 845 self.versions << new_version
843 846 end
844 847 end
845 848
846 849 # Copies issue categories from +project+
847 850 def copy_issue_categories(project)
848 851 project.issue_categories.each do |issue_category|
849 852 new_issue_category = IssueCategory.new
850 853 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
851 854 self.issue_categories << new_issue_category
852 855 end
853 856 end
854 857
855 858 # Copies issues from +project+
856 859 def copy_issues(project)
857 860 # Stores the source issue id as a key and the copied issues as the
858 861 # value. Used to map the two together for issue relations.
859 862 issues_map = {}
860 863
861 864 # Store status and reopen locked/closed versions
862 865 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
863 866 version_statuses.each do |version, status|
864 867 version.update_attribute :status, 'open'
865 868 end
866 869
867 870 # Get issues sorted by root_id, lft so that parent issues
868 871 # get copied before their children
869 872 project.issues.reorder('root_id, lft').each do |issue|
870 873 new_issue = Issue.new
871 874 new_issue.copy_from(issue, :subtasks => false, :link => false)
872 875 new_issue.project = self
873 876 # Changing project resets the custom field values
874 877 # TODO: handle this in Issue#project=
875 878 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
876 879 # Reassign fixed_versions by name, since names are unique per project
877 880 if issue.fixed_version && issue.fixed_version.project == project
878 881 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
879 882 end
880 883 # Reassign the category by name, since names are unique per project
881 884 if issue.category
882 885 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
883 886 end
884 887 # Parent issue
885 888 if issue.parent_id
886 889 if copied_parent = issues_map[issue.parent_id]
887 890 new_issue.parent_issue_id = copied_parent.id
888 891 end
889 892 end
890 893
891 894 self.issues << new_issue
892 895 if new_issue.new_record?
893 896 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
894 897 else
895 898 issues_map[issue.id] = new_issue unless new_issue.new_record?
896 899 end
897 900 end
898 901
899 902 # Restore locked/closed version statuses
900 903 version_statuses.each do |version, status|
901 904 version.update_attribute :status, status
902 905 end
903 906
904 907 # Relations after in case issues related each other
905 908 project.issues.each do |issue|
906 909 new_issue = issues_map[issue.id]
907 910 unless new_issue
908 911 # Issue was not copied
909 912 next
910 913 end
911 914
912 915 # Relations
913 916 issue.relations_from.each do |source_relation|
914 917 new_issue_relation = IssueRelation.new
915 918 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
916 919 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
917 920 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
918 921 new_issue_relation.issue_to = source_relation.issue_to
919 922 end
920 923 new_issue.relations_from << new_issue_relation
921 924 end
922 925
923 926 issue.relations_to.each do |source_relation|
924 927 new_issue_relation = IssueRelation.new
925 928 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
926 929 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
927 930 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
928 931 new_issue_relation.issue_from = source_relation.issue_from
929 932 end
930 933 new_issue.relations_to << new_issue_relation
931 934 end
932 935 end
933 936 end
934 937
935 938 # Copies members from +project+
936 939 def copy_members(project)
937 940 # Copy users first, then groups to handle members with inherited and given roles
938 941 members_to_copy = []
939 942 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
940 943 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
941 944
942 945 members_to_copy.each do |member|
943 946 new_member = Member.new
944 947 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
945 948 # only copy non inherited roles
946 949 # inherited roles will be added when copying the group membership
947 950 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
948 951 next if role_ids.empty?
949 952 new_member.role_ids = role_ids
950 953 new_member.project = self
951 954 self.members << new_member
952 955 end
953 956 end
954 957
955 958 # Copies queries from +project+
956 959 def copy_queries(project)
957 960 project.queries.each do |query|
958 961 new_query = IssueQuery.new
959 962 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
960 963 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
961 964 new_query.project = self
962 965 new_query.user_id = query.user_id
963 966 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
964 967 self.queries << new_query
965 968 end
966 969 end
967 970
968 971 # Copies boards from +project+
969 972 def copy_boards(project)
970 973 project.boards.each do |board|
971 974 new_board = Board.new
972 975 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
973 976 new_board.project = self
974 977 self.boards << new_board
975 978 end
976 979 end
977 980
978 981 def allowed_permissions
979 982 @allowed_permissions ||= begin
980 983 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
981 984 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
982 985 end
983 986 end
984 987
985 988 def allowed_actions
986 989 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
987 990 end
988 991
989 992 # Returns all the active Systemwide and project specific activities
990 993 def active_activities
991 994 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
992 995
993 996 if overridden_activity_ids.empty?
994 997 return TimeEntryActivity.shared.active
995 998 else
996 999 return system_activities_and_project_overrides
997 1000 end
998 1001 end
999 1002
1000 1003 # Returns all the Systemwide and project specific activities
1001 1004 # (inactive and active)
1002 1005 def all_activities
1003 1006 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
1004 1007
1005 1008 if overridden_activity_ids.empty?
1006 1009 return TimeEntryActivity.shared
1007 1010 else
1008 1011 return system_activities_and_project_overrides(true)
1009 1012 end
1010 1013 end
1011 1014
1012 1015 # Returns the systemwide active activities merged with the project specific overrides
1013 1016 def system_activities_and_project_overrides(include_inactive=false)
1014 1017 t = TimeEntryActivity.table_name
1015 1018 scope = TimeEntryActivity.where(
1016 1019 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1017 1020 time_entry_activities.map(&:parent_id), id
1018 1021 )
1019 1022 unless include_inactive
1020 1023 scope = scope.active
1021 1024 end
1022 1025 scope
1023 1026 end
1024 1027
1025 1028 # Archives subprojects recursively
1026 1029 def archive!
1027 1030 children.each do |subproject|
1028 1031 subproject.send :archive!
1029 1032 end
1030 1033 update_attribute :status, STATUS_ARCHIVED
1031 1034 end
1032 1035 end
General Comments 0
You need to be logged in to leave comments. Login now