Mercurial > hg > soundsoftware-site
comparison app/models/.svn/text-base/project.rb.svn-base @ 511:107d36338b70 live
Merge from branch "cannam"
author | Chris Cannam |
---|---|
date | Thu, 14 Jul 2011 10:43:07 +0100 |
parents | 0c939c159af4 |
children |
comparison
equal
deleted
inserted
replaced
451:a9f6345cb43d | 511:107d36338b70 |
---|---|
1 # redMine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006 Jean-Philippe Lang | 2 # Copyright (C) 2006-2011 Jean-Philippe Lang |
3 # | 3 # |
4 # This program is free software; you can redistribute it and/or | 4 # This program is free software; you can redistribute it and/or |
5 # modify it under the terms of the GNU General Public License | 5 # modify it under the terms of the GNU General Public License |
6 # as published by the Free Software Foundation; either version 2 | 6 # as published by the Free Software Foundation; either version 2 |
7 # of the License, or (at your option) any later version. | 7 # of the License, or (at your option) any later version. |
14 # You should have received a copy of the GNU General Public License | 14 # You should have received a copy of the GNU General Public License |
15 # along with this program; if not, write to the Free Software | 15 # along with this program; if not, write to the Free Software |
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 |
18 class Project < ActiveRecord::Base | 18 class Project < ActiveRecord::Base |
19 include Redmine::SafeAttributes | |
20 | |
19 # Project statuses | 21 # Project statuses |
20 STATUS_ACTIVE = 1 | 22 STATUS_ACTIVE = 1 |
21 STATUS_ARCHIVED = 9 | 23 STATUS_ARCHIVED = 9 |
22 | 24 |
23 # Maximum length for project identifiers | 25 # Maximum length for project identifiers |
39 has_many :issue_changes, :through => :issues, :source => :journals | 41 has_many :issue_changes, :through => :issues, :source => :journals |
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" | 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" |
41 has_many :time_entries, :dependent => :delete_all | 43 has_many :time_entries, :dependent => :delete_all |
42 has_many :queries, :dependent => :delete_all | 44 has_many :queries, :dependent => :delete_all |
43 has_many :documents, :dependent => :destroy | 45 has_many :documents, :dependent => :destroy |
44 has_many :news, :dependent => :delete_all, :include => :author | 46 has_many :news, :dependent => :destroy, :include => :author |
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" | 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" |
46 has_many :boards, :dependent => :destroy, :order => "position ASC" | 48 has_many :boards, :dependent => :destroy, :order => "position ASC" |
47 has_one :repository, :dependent => :destroy | 49 has_one :repository, :dependent => :destroy |
48 has_many :changesets, :through => :repository | 50 has_many :changesets, :through => :repository |
49 has_one :wiki, :dependent => :destroy | 51 has_one :wiki, :dependent => :destroy |
52 :class_name => 'IssueCustomField', | 54 :class_name => 'IssueCustomField', |
53 :order => "#{CustomField.table_name}.position", | 55 :order => "#{CustomField.table_name}.position", |
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", | 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", |
55 :association_foreign_key => 'custom_field_id' | 57 :association_foreign_key => 'custom_field_id' |
56 | 58 |
57 acts_as_nested_set :order => 'name' | 59 acts_as_nested_set :order => 'name', :dependent => :destroy |
58 acts_as_attachable :view_permission => :view_files, | 60 acts_as_attachable :view_permission => :view_files, |
59 :delete_permission => :manage_files | 61 :delete_permission => :manage_files |
60 | 62 |
61 acts_as_customizable | 63 acts_as_customizable |
62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil | 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil |
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, | 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, |
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, | 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, |
65 :author => nil | 67 :author => nil |
66 | 68 |
67 attr_protected :status, :enabled_module_names | 69 attr_protected :status |
68 | 70 |
69 validates_presence_of :name, :identifier | 71 validates_presence_of :name, :identifier |
70 validates_uniqueness_of :identifier | 72 validates_uniqueness_of :identifier |
71 validates_associated :repository, :wiki | 73 validates_associated :repository, :wiki |
72 validates_length_of :name, :maximum => 255 | 74 validates_length_of :name, :maximum => 255 |
75 # donwcase letters, digits, dashes but not digits only | 77 # donwcase letters, digits, dashes but not digits only |
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } | 78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } |
77 # reserved words | 79 # reserved words |
78 validates_exclusion_of :identifier, :in => %w( new ) | 80 validates_exclusion_of :identifier, :in => %w( new ) |
79 | 81 |
80 before_destroy :delete_all_members, :destroy_children | 82 before_destroy :delete_all_members |
81 | 83 |
82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } | 84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } |
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} | 85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} |
84 named_scope :all_public, { :conditions => { :is_public => true } } | 86 named_scope :all_public, { :conditions => { :is_public => true } } |
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } } | 87 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} |
88 | |
89 def initialize(attributes = nil) | |
90 super | |
91 | |
92 initialized = (attributes || {}).stringify_keys | |
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers? | |
94 self.identifier = Project.next_identifier | |
95 end | |
96 if !initialized.key?('is_public') | |
97 self.is_public = Setting.default_projects_public? | |
98 end | |
99 if !initialized.key?('enabled_module_names') | |
100 self.enabled_module_names = Setting.default_projects_modules | |
101 end | |
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids') | |
103 self.trackers = Tracker.all | |
104 end | |
105 end | |
86 | 106 |
87 def identifier=(identifier) | 107 def identifier=(identifier) |
88 super unless identifier_frozen? | 108 super unless identifier_frozen? |
89 end | 109 end |
90 | 110 |
93 end | 113 end |
94 | 114 |
95 # returns latest created projects | 115 # returns latest created projects |
96 # non public projects will be returned only if user is a member of those | 116 # non public projects will be returned only if user is a member of those |
97 def self.latest(user=nil, count=5) | 117 def self.latest(user=nil, count=5) |
98 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") | 118 visible(user).find(:all, :limit => count, :order => "created_on DESC") |
99 end | 119 end |
100 | 120 |
101 # Returns a SQL :conditions string used to find all active projects for the specified user. | 121 # Returns true if the project is visible to +user+ or to the current user. |
122 def visible?(user=User.current) | |
123 user.allowed_to?(:view_project, self) | |
124 end | |
125 | |
126 def self.visible_by(user=nil) | |
127 ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead." | |
128 visible_condition(user || User.current) | |
129 end | |
130 | |
131 # Returns a SQL conditions string used to find all projects visible by the specified user. | |
102 # | 132 # |
103 # Examples: | 133 # Examples: |
104 # Projects.visible_by(admin) => "projects.status = 1" | 134 # Project.visible_condition(admin) => "projects.status = 1" |
105 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1" | 135 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))" |
106 def self.visible_by(user=nil) | 136 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))" |
107 user ||= User.current | 137 def self.visible_condition(user, options={}) |
108 if user && user.admin? | 138 allowed_to_condition(user, :view_project, options) |
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" | 139 end |
110 elsif user && user.memberships.any? | 140 |
111 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))" | 141 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+ |
112 else | 142 # |
113 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}" | 143 # Valid options: |
114 end | 144 # * :project => limit the condition to project |
115 end | 145 # * :with_subprojects => limit the condition to project and its subprojects |
116 | 146 # * :member => limit the condition to the user projects |
117 def self.allowed_to_condition(user, permission, options={}) | 147 def self.allowed_to_condition(user, permission, options={}) |
118 statements = [] | |
119 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" | 148 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" |
120 if perm = Redmine::AccessControl.permission(permission) | 149 if perm = Redmine::AccessControl.permission(permission) |
121 unless perm.project_module.nil? | 150 unless perm.project_module.nil? |
122 # If the permission belongs to a project module, make sure the module is enabled | 151 # If the permission belongs to a project module, make sure the module is enabled |
123 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" | 152 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" |
126 if options[:project] | 155 if options[:project] |
127 project_statement = "#{Project.table_name}.id = #{options[:project].id}" | 156 project_statement = "#{Project.table_name}.id = #{options[:project].id}" |
128 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] | 157 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] |
129 base_statement = "(#{project_statement}) AND (#{base_statement})" | 158 base_statement = "(#{project_statement}) AND (#{base_statement})" |
130 end | 159 end |
160 | |
131 if user.admin? | 161 if user.admin? |
132 # no restriction | 162 base_statement |
133 else | 163 else |
134 statements << "1=0" | 164 statement_by_role = {} |
165 unless options[:member] | |
166 role = user.logged? ? Role.non_member : Role.anonymous | |
167 if role.allowed_to?(permission) | |
168 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" | |
169 end | |
170 end | |
135 if user.logged? | 171 if user.logged? |
136 if Role.non_member.allowed_to?(permission) && !options[:member] | 172 user.projects_by_role.each do |role, projects| |
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" | 173 if role.allowed_to?(permission) |
138 end | 174 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" |
139 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id} | 175 end |
140 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? | 176 end |
177 end | |
178 if statement_by_role.empty? | |
179 "1=0" | |
141 else | 180 else |
142 if Role.anonymous.allowed_to?(permission) && !options[:member] | 181 if block_given? |
143 # anonymous user allowed on public project | 182 statement_by_role.each do |role, statement| |
144 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" | 183 if s = yield(role, user) |
145 end | 184 statement_by_role[role] = "(#{statement} AND (#{s}))" |
146 end | 185 end |
147 end | 186 end |
148 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" | 187 end |
188 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))" | |
189 end | |
190 end | |
149 end | 191 end |
150 | 192 |
151 # Returns the Systemwide and project specific activities | 193 # Returns the Systemwide and project specific activities |
152 def activities(include_inactive=false) | 194 def activities(include_inactive=false) |
153 if include_inactive | 195 if include_inactive |
325 end | 367 end |
326 | 368 |
327 # Returns an array of the trackers used by the project and its active sub projects | 369 # Returns an array of the trackers used by the project and its active sub projects |
328 def rolled_up_trackers | 370 def rolled_up_trackers |
329 @rolled_up_trackers ||= | 371 @rolled_up_trackers ||= |
330 Tracker.find(:all, :include => :projects, | 372 Tracker.find(:all, :joins => :projects, |
331 :select => "DISTINCT #{Tracker.table_name}.*", | 373 :select => "DISTINCT #{Tracker.table_name}.*", |
332 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], | 374 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], |
333 :order => "#{Tracker.table_name}.position") | 375 :order => "#{Tracker.table_name}.position") |
334 end | 376 end |
335 | 377 |
351 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) | 393 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) |
352 end | 394 end |
353 | 395 |
354 # Returns a scope of the Versions used by the project | 396 # Returns a scope of the Versions used by the project |
355 def shared_versions | 397 def shared_versions |
356 @shared_versions ||= | 398 @shared_versions ||= begin |
399 r = root? ? self : root | |
357 Version.scoped(:include => :project, | 400 Version.scoped(:include => :project, |
358 :conditions => "#{Project.table_name}.id = #{id}" + | 401 :conditions => "#{Project.table_name}.id = #{id}" + |
359 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + | 402 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + |
360 " #{Version.table_name}.sharing = 'system'" + | 403 " #{Version.table_name}.sharing = 'system'" + |
361 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" + | 404 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + |
362 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + | 405 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + |
363 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + | 406 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + |
364 "))") | 407 "))") |
408 end | |
365 end | 409 end |
366 | 410 |
367 # Returns a hash of project users grouped by role | 411 # Returns a hash of project users grouped by role |
368 def users_by_role | 412 def users_by_role |
369 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m| | 413 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m| |
401 # Returns an array of all custom fields enabled for project issues | 445 # Returns an array of all custom fields enabled for project issues |
402 # (explictly associated custom fields and custom fields enabled for all projects) | 446 # (explictly associated custom fields and custom fields enabled for all projects) |
403 def all_issue_custom_fields | 447 def all_issue_custom_fields |
404 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort | 448 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort |
405 end | 449 end |
450 | |
451 # Returns an array of all custom fields enabled for project time entries | |
452 # (explictly associated custom fields and custom fields enabled for all projects) | |
453 def all_time_entry_custom_fields | |
454 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort | |
455 end | |
406 | 456 |
407 def project | 457 def project |
408 self | 458 self |
409 end | 459 end |
410 | 460 |
429 s | 479 s |
430 end | 480 end |
431 | 481 |
432 # The earliest start date of a project, based on it's issues and versions | 482 # The earliest start date of a project, based on it's issues and versions |
433 def start_date | 483 def start_date |
434 if module_enabled?(:issue_tracking) | 484 [ |
435 [ | 485 issues.minimum('start_date'), |
436 issues.minimum('start_date'), | 486 shared_versions.collect(&:effective_date), |
437 shared_versions.collect(&:effective_date), | 487 shared_versions.collect(&:start_date) |
438 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')} | 488 ].flatten.compact.min |
439 ].flatten.compact.min | |
440 end | |
441 end | 489 end |
442 | 490 |
443 # The latest due date of an issue or version | 491 # The latest due date of an issue or version |
444 def due_date | 492 def due_date |
445 if module_enabled?(:issue_tracking) | 493 [ |
446 [ | 494 issues.maximum('due_date'), |
447 issues.maximum('due_date'), | 495 shared_versions.collect(&:effective_date), |
448 shared_versions.collect(&:effective_date), | 496 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} |
449 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} | 497 ].flatten.compact.max |
450 ].flatten.compact.max | |
451 end | |
452 end | 498 end |
453 | 499 |
454 def overdue? | 500 def overdue? |
455 active? && !due_date.nil? && (due_date < Date.today) | 501 active? && !due_date.nil? && (due_date < Date.today) |
456 end | 502 end |
490 enabled_modules.detect {|m| m.name == module_name} | 536 enabled_modules.detect {|m| m.name == module_name} |
491 end | 537 end |
492 | 538 |
493 def enabled_module_names=(module_names) | 539 def enabled_module_names=(module_names) |
494 if module_names && module_names.is_a?(Array) | 540 if module_names && module_names.is_a?(Array) |
495 module_names = module_names.collect(&:to_s) | 541 module_names = module_names.collect(&:to_s).reject(&:blank?) |
496 # remove disabled modules | 542 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)} |
497 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)} | |
498 # add new modules | |
499 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)} | |
500 else | 543 else |
501 enabled_modules.clear | 544 enabled_modules.clear |
502 end | 545 end |
503 end | 546 end |
504 | 547 |
548 # Returns an array of the enabled modules names | |
549 def enabled_module_names | |
550 enabled_modules.collect(&:name) | |
551 end | |
552 | |
553 # Enable a specific module | |
554 # | |
555 # Examples: | |
556 # project.enable_module!(:issue_tracking) | |
557 # project.enable_module!("issue_tracking") | |
558 def enable_module!(name) | |
559 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name) | |
560 end | |
561 | |
562 # Disable a module if it exists | |
563 # | |
564 # Examples: | |
565 # project.disable_module!(:issue_tracking) | |
566 # project.disable_module!("issue_tracking") | |
567 # project.disable_module!(project.enabled_modules.first) | |
568 def disable_module!(target) | |
569 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target) | |
570 target.destroy unless target.blank? | |
571 end | |
572 | |
573 safe_attributes 'name', | |
574 'description', | |
575 'homepage', | |
576 'is_public', | |
577 'identifier', | |
578 'custom_field_values', | |
579 'custom_fields', | |
580 'tracker_ids', | |
581 'issue_custom_field_ids' | |
582 | |
583 safe_attributes 'enabled_module_names', | |
584 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) } | |
585 | |
505 # Returns an array of projects that are in this project's hierarchy | 586 # Returns an array of projects that are in this project's hierarchy |
506 # | 587 # |
507 # Example: parents, children, siblings | 588 # Example: parents, children, siblings |
508 def hierarchy | 589 def hierarchy |
509 parents = project.self_and_ancestors || [] | 590 parents = project.self_and_ancestors || [] |
585 end | 666 end |
586 end | 667 end |
587 | 668 |
588 private | 669 private |
589 | 670 |
590 # Destroys children before destroying self | |
591 def destroy_children | |
592 children.each do |child| | |
593 child.destroy | |
594 end | |
595 end | |
596 | |
597 # Copies wiki from +project+ | 671 # Copies wiki from +project+ |
598 def copy_wiki(project) | 672 def copy_wiki(project) |
599 # Check that the source project has a wiki first | 673 # Check that the source project has a wiki first |
600 unless project.wiki.nil? | 674 unless project.wiki.nil? |
601 self.wiki ||= Wiki.new | 675 self.wiki ||= Wiki.new |
638 self.issue_categories << new_issue_category | 712 self.issue_categories << new_issue_category |
639 end | 713 end |
640 end | 714 end |
641 | 715 |
642 # Copies issues from +project+ | 716 # Copies issues from +project+ |
717 # Note: issues assigned to a closed version won't be copied due to validation rules | |
643 def copy_issues(project) | 718 def copy_issues(project) |
644 # Stores the source issue id as a key and the copied issues as the | 719 # Stores the source issue id as a key and the copied issues as the |
645 # value. Used to map the two togeather for issue relations. | 720 # value. Used to map the two togeather for issue relations. |
646 issues_map = {} | 721 issues_map = {} |
647 | 722 |
667 new_issue.parent_issue_id = copied_parent.id | 742 new_issue.parent_issue_id = copied_parent.id |
668 end | 743 end |
669 end | 744 end |
670 | 745 |
671 self.issues << new_issue | 746 self.issues << new_issue |
672 issues_map[issue.id] = new_issue | 747 if new_issue.new_record? |
748 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info | |
749 else | |
750 issues_map[issue.id] = new_issue unless new_issue.new_record? | |
751 end | |
673 end | 752 end |
674 | 753 |
675 # Relations after in case issues related each other | 754 # Relations after in case issues related each other |
676 project.issues.each do |issue| | 755 project.issues.each do |issue| |
677 new_issue = issues_map[issue.id] | 756 new_issue = issues_map[issue.id] |
757 unless new_issue | |
758 # Issue was not copied | |
759 next | |
760 end | |
678 | 761 |
679 # Relations | 762 # Relations |
680 issue.relations_from.each do |source_relation| | 763 issue.relations_from.each do |source_relation| |
681 new_issue_relation = IssueRelation.new | 764 new_issue_relation = IssueRelation.new |
682 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") | 765 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") |
699 end | 782 end |
700 end | 783 end |
701 | 784 |
702 # Copies members from +project+ | 785 # Copies members from +project+ |
703 def copy_members(project) | 786 def copy_members(project) |
704 project.memberships.each do |member| | 787 # Copy users first, then groups to handle members with inherited and given roles |
788 members_to_copy = [] | |
789 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)} | |
790 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)} | |
791 | |
792 members_to_copy.each do |member| | |
705 new_member = Member.new | 793 new_member = Member.new |
706 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") | 794 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") |
707 # only copy non inherited roles | 795 # only copy non inherited roles |
708 # inherited roles will be added when copying the group membership | 796 # inherited roles will be added when copying the group membership |
709 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) | 797 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) |