##// END OF EJS Templates
Refactor: move method to Project#css_classes...
Eric Davis -
r3966:878bb5552276
parent child
Show More
@@ -1,31 +1,23
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module AdminHelper
18 module AdminHelper
19 def project_status_options_for_select(selected)
19 def project_status_options_for_select(selected)
20 options_for_select([[l(:label_all), ''],
20 options_for_select([[l(:label_all), ''],
21 [l(:status_active), 1]], selected)
21 [l(:status_active), 1]], selected)
22 end
22 end
23
24 def css_project_classes(project)
25 s = 'project'
26 s << ' root' if project.root?
27 s << ' child' if project.child?
28 s << (project.leaf? ? ' leaf' : ' parent')
29 s
30 end
31 end
23 end
@@ -1,757 +1,765
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities
24 has_many :time_entry_activities
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 has_many :memberships, :class_name => 'Member'
26 has_many :memberships, :class_name => 'Member'
27 has_many :member_principals, :class_name => 'Member',
27 has_many :member_principals, :class_name => 'Member',
28 :include => :principal,
28 :include => :principal,
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 has_many :users, :through => :members
30 has_many :users, :through => :members
31 has_many :principals, :through => :member_principals, :source => :principal
31 has_many :principals, :through => :member_principals, :source => :principal
32
32
33 has_many :enabled_modules, :dependent => :delete_all
33 has_many :enabled_modules, :dependent => :delete_all
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 has_many :issue_changes, :through => :issues, :source => :journals
36 has_many :issue_changes, :through => :issues, :source => :journals
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 has_many :time_entries, :dependent => :delete_all
38 has_many :time_entries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
40 has_many :documents, :dependent => :destroy
40 has_many :documents, :dependent => :destroy
41 has_many :news, :dependent => :delete_all, :include => :author
41 has_many :news, :dependent => :delete_all, :include => :author
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 has_one :repository, :dependent => :destroy
44 has_one :repository, :dependent => :destroy
45 has_many :changesets, :through => :repository
45 has_many :changesets, :through => :repository
46 has_one :wiki, :dependent => :destroy
46 has_one :wiki, :dependent => :destroy
47 # Custom field for the project issues
47 # Custom field for the project issues
48 has_and_belongs_to_many :issue_custom_fields,
48 has_and_belongs_to_many :issue_custom_fields,
49 :class_name => 'IssueCustomField',
49 :class_name => 'IssueCustomField',
50 :order => "#{CustomField.table_name}.position",
50 :order => "#{CustomField.table_name}.position",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 :association_foreign_key => 'custom_field_id'
52 :association_foreign_key => 'custom_field_id'
53
53
54 acts_as_nested_set :order => 'name'
54 acts_as_nested_set :order => 'name'
55 acts_as_attachable :view_permission => :view_files,
55 acts_as_attachable :view_permission => :view_files,
56 :delete_permission => :manage_files
56 :delete_permission => :manage_files
57
57
58 acts_as_customizable
58 acts_as_customizable
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
62 :author => nil
62 :author => nil
63
63
64 attr_protected :status, :enabled_module_names
64 attr_protected :status, :enabled_module_names
65
65
66 validates_presence_of :name, :identifier
66 validates_presence_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
68 validates_associated :repository, :wiki
68 validates_associated :repository, :wiki
69 validates_length_of :name, :maximum => 30
69 validates_length_of :name, :maximum => 30
70 validates_length_of :homepage, :maximum => 255
70 validates_length_of :homepage, :maximum => 255
71 validates_length_of :identifier, :in => 1..20
71 validates_length_of :identifier, :in => 1..20
72 # donwcase letters, digits, dashes but not digits only
72 # donwcase letters, digits, dashes but not digits only
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 # reserved words
74 # reserved words
75 validates_exclusion_of :identifier, :in => %w( new )
75 validates_exclusion_of :identifier, :in => %w( new )
76
76
77 before_destroy :delete_all_members, :destroy_children
77 before_destroy :delete_all_members, :destroy_children
78
78
79 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] } }
79 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] } }
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 named_scope :all_public, { :conditions => { :is_public => true } }
81 named_scope :all_public, { :conditions => { :is_public => true } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83
83
84 def identifier=(identifier)
84 def identifier=(identifier)
85 super unless identifier_frozen?
85 super unless identifier_frozen?
86 end
86 end
87
87
88 def identifier_frozen?
88 def identifier_frozen?
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 end
90 end
91
91
92 # returns latest created projects
92 # returns latest created projects
93 # non public projects will be returned only if user is a member of those
93 # non public projects will be returned only if user is a member of those
94 def self.latest(user=nil, count=5)
94 def self.latest(user=nil, count=5)
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 end
96 end
97
97
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 #
99 #
100 # Examples:
100 # Examples:
101 # Projects.visible_by(admin) => "projects.status = 1"
101 # Projects.visible_by(admin) => "projects.status = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 def self.visible_by(user=nil)
103 def self.visible_by(user=nil)
104 user ||= User.current
104 user ||= User.current
105 if user && user.admin?
105 if user && user.admin?
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 elsif user && user.memberships.any?
107 elsif user && user.memberships.any?
108 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(',')}))"
108 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(',')}))"
109 else
109 else
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 end
111 end
112 end
112 end
113
113
114 def self.allowed_to_condition(user, permission, options={})
114 def self.allowed_to_condition(user, permission, options={})
115 statements = []
115 statements = []
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 if perm = Redmine::AccessControl.permission(permission)
117 if perm = Redmine::AccessControl.permission(permission)
118 unless perm.project_module.nil?
118 unless perm.project_module.nil?
119 # If the permission belongs to a project module, make sure the module is enabled
119 # If the permission belongs to a project module, make sure the module is enabled
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 end
121 end
122 end
122 end
123 if options[:project]
123 if options[:project]
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 end
127 end
128 if user.admin?
128 if user.admin?
129 # no restriction
129 # no restriction
130 else
130 else
131 statements << "1=0"
131 statements << "1=0"
132 if user.logged?
132 if user.logged?
133 if Role.non_member.allowed_to?(permission) && !options[:member]
133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 end
135 end
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 else
138 else
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 # anonymous user allowed on public project
140 # anonymous user allowed on public project
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 end
142 end
143 end
143 end
144 end
144 end
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 end
146 end
147
147
148 # Returns the Systemwide and project specific activities
148 # Returns the Systemwide and project specific activities
149 def activities(include_inactive=false)
149 def activities(include_inactive=false)
150 if include_inactive
150 if include_inactive
151 return all_activities
151 return all_activities
152 else
152 else
153 return active_activities
153 return active_activities
154 end
154 end
155 end
155 end
156
156
157 # Will create a new Project specific Activity or update an existing one
157 # Will create a new Project specific Activity or update an existing one
158 #
158 #
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 # does not successfully save.
160 # does not successfully save.
161 def update_or_create_time_entry_activity(id, activity_hash)
161 def update_or_create_time_entry_activity(id, activity_hash)
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 self.create_time_entry_activity_if_needed(activity_hash)
163 self.create_time_entry_activity_if_needed(activity_hash)
164 else
164 else
165 activity = project.time_entry_activities.find_by_id(id.to_i)
165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 activity.update_attributes(activity_hash) if activity
166 activity.update_attributes(activity_hash) if activity
167 end
167 end
168 end
168 end
169
169
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 #
171 #
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 # does not successfully save.
173 # does not successfully save.
174 def create_time_entry_activity_if_needed(activity)
174 def create_time_entry_activity_if_needed(activity)
175 if activity['parent_id']
175 if activity['parent_id']
176
176
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 activity['name'] = parent_activity.name
178 activity['name'] = parent_activity.name
179 activity['position'] = parent_activity.position
179 activity['position'] = parent_activity.position
180
180
181 if Enumeration.overridding_change?(activity, parent_activity)
181 if Enumeration.overridding_change?(activity, parent_activity)
182 project_activity = self.time_entry_activities.create(activity)
182 project_activity = self.time_entry_activities.create(activity)
183
183
184 if project_activity.new_record?
184 if project_activity.new_record?
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 else
186 else
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 end
188 end
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 #
194 #
195 # Examples:
195 # Examples:
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 # project.project_condition(false) => "projects.id = 1"
197 # project.project_condition(false) => "projects.id = 1"
198 def project_condition(with_subprojects)
198 def project_condition(with_subprojects)
199 cond = "#{Project.table_name}.id = #{id}"
199 cond = "#{Project.table_name}.id = #{id}"
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 cond
201 cond
202 end
202 end
203
203
204 def self.find(*args)
204 def self.find(*args)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 project = find_by_identifier(*args)
206 project = find_by_identifier(*args)
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 project
208 project
209 else
209 else
210 super
210 super
211 end
211 end
212 end
212 end
213
213
214 def to_param
214 def to_param
215 # id is used for projects with a numeric identifier (compatibility)
215 # id is used for projects with a numeric identifier (compatibility)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 end
217 end
218
218
219 def active?
219 def active?
220 self.status == STATUS_ACTIVE
220 self.status == STATUS_ACTIVE
221 end
221 end
222
222
223 # Archives the project and its descendants
223 # Archives the project and its descendants
224 def archive
224 def archive
225 # Check that there is no issue of a non descendant project that is assigned
225 # Check that there is no issue of a non descendant project that is assigned
226 # to one of the project or descendant versions
226 # to one of the project or descendant versions
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 if v_ids.any? && Issue.find(:first, :include => :project,
228 if v_ids.any? && Issue.find(:first, :include => :project,
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 return false
231 return false
232 end
232 end
233 Project.transaction do
233 Project.transaction do
234 archive!
234 archive!
235 end
235 end
236 true
236 true
237 end
237 end
238
238
239 # Unarchives the project
239 # Unarchives the project
240 # All its ancestors must be active
240 # All its ancestors must be active
241 def unarchive
241 def unarchive
242 return false if ancestors.detect {|a| !a.active?}
242 return false if ancestors.detect {|a| !a.active?}
243 update_attribute :status, STATUS_ACTIVE
243 update_attribute :status, STATUS_ACTIVE
244 end
244 end
245
245
246 # Returns an array of projects the project can be moved to
246 # Returns an array of projects the project can be moved to
247 # by the current user
247 # by the current user
248 def allowed_parents
248 def allowed_parents
249 return @allowed_parents if @allowed_parents
249 return @allowed_parents if @allowed_parents
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 @allowed_parents = @allowed_parents - self_and_descendants
251 @allowed_parents = @allowed_parents - self_and_descendants
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 @allowed_parents << nil
253 @allowed_parents << nil
254 end
254 end
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 @allowed_parents << parent
256 @allowed_parents << parent
257 end
257 end
258 @allowed_parents
258 @allowed_parents
259 end
259 end
260
260
261 # Sets the parent of the project with authorization check
261 # Sets the parent of the project with authorization check
262 def set_allowed_parent!(p)
262 def set_allowed_parent!(p)
263 unless p.nil? || p.is_a?(Project)
263 unless p.nil? || p.is_a?(Project)
264 if p.to_s.blank?
264 if p.to_s.blank?
265 p = nil
265 p = nil
266 else
266 else
267 p = Project.find_by_id(p)
267 p = Project.find_by_id(p)
268 return false unless p
268 return false unless p
269 end
269 end
270 end
270 end
271 if p.nil?
271 if p.nil?
272 if !new_record? && allowed_parents.empty?
272 if !new_record? && allowed_parents.empty?
273 return false
273 return false
274 end
274 end
275 elsif !allowed_parents.include?(p)
275 elsif !allowed_parents.include?(p)
276 return false
276 return false
277 end
277 end
278 set_parent!(p)
278 set_parent!(p)
279 end
279 end
280
280
281 # Sets the parent of the project
281 # Sets the parent of the project
282 # Argument can be either a Project, a String, a Fixnum or nil
282 # Argument can be either a Project, a String, a Fixnum or nil
283 def set_parent!(p)
283 def set_parent!(p)
284 unless p.nil? || p.is_a?(Project)
284 unless p.nil? || p.is_a?(Project)
285 if p.to_s.blank?
285 if p.to_s.blank?
286 p = nil
286 p = nil
287 else
287 else
288 p = Project.find_by_id(p)
288 p = Project.find_by_id(p)
289 return false unless p
289 return false unless p
290 end
290 end
291 end
291 end
292 if p == parent && !p.nil?
292 if p == parent && !p.nil?
293 # Nothing to do
293 # Nothing to do
294 true
294 true
295 elsif p.nil? || (p.active? && move_possible?(p))
295 elsif p.nil? || (p.active? && move_possible?(p))
296 # Insert the project so that target's children or root projects stay alphabetically sorted
296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 sibs = (p.nil? ? self.class.roots : p.children)
297 sibs = (p.nil? ? self.class.roots : p.children)
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 if to_be_inserted_before
299 if to_be_inserted_before
300 move_to_left_of(to_be_inserted_before)
300 move_to_left_of(to_be_inserted_before)
301 elsif p.nil?
301 elsif p.nil?
302 if sibs.empty?
302 if sibs.empty?
303 # move_to_root adds the project in first (ie. left) position
303 # move_to_root adds the project in first (ie. left) position
304 move_to_root
304 move_to_root
305 else
305 else
306 move_to_right_of(sibs.last) unless self == sibs.last
306 move_to_right_of(sibs.last) unless self == sibs.last
307 end
307 end
308 else
308 else
309 # move_to_child_of adds the project in last (ie.right) position
309 # move_to_child_of adds the project in last (ie.right) position
310 move_to_child_of(p)
310 move_to_child_of(p)
311 end
311 end
312 Issue.update_versions_from_hierarchy_change(self)
312 Issue.update_versions_from_hierarchy_change(self)
313 true
313 true
314 else
314 else
315 # Can not move to the given target
315 # Can not move to the given target
316 false
316 false
317 end
317 end
318 end
318 end
319
319
320 # Returns an array of the trackers used by the project and its active sub projects
320 # Returns an array of the trackers used by the project and its active sub projects
321 def rolled_up_trackers
321 def rolled_up_trackers
322 @rolled_up_trackers ||=
322 @rolled_up_trackers ||=
323 Tracker.find(:all, :include => :projects,
323 Tracker.find(:all, :include => :projects,
324 :select => "DISTINCT #{Tracker.table_name}.*",
324 :select => "DISTINCT #{Tracker.table_name}.*",
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 :order => "#{Tracker.table_name}.position")
326 :order => "#{Tracker.table_name}.position")
327 end
327 end
328
328
329 # Closes open and locked project versions that are completed
329 # Closes open and locked project versions that are completed
330 def close_completed_versions
330 def close_completed_versions
331 Version.transaction do
331 Version.transaction do
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 if version.completed?
333 if version.completed?
334 version.update_attribute(:status, 'closed')
334 version.update_attribute(:status, 'closed')
335 end
335 end
336 end
336 end
337 end
337 end
338 end
338 end
339
339
340 # Returns a scope of the Versions on subprojects
340 # Returns a scope of the Versions on subprojects
341 def rolled_up_versions
341 def rolled_up_versions
342 @rolled_up_versions ||=
342 @rolled_up_versions ||=
343 Version.scoped(:include => :project,
343 Version.scoped(:include => :project,
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
345 end
345 end
346
346
347 # Returns a scope of the Versions used by the project
347 # Returns a scope of the Versions used by the project
348 def shared_versions
348 def shared_versions
349 @shared_versions ||=
349 @shared_versions ||=
350 Version.scoped(:include => :project,
350 Version.scoped(:include => :project,
351 :conditions => "#{Project.table_name}.id = #{id}" +
351 :conditions => "#{Project.table_name}.id = #{id}" +
352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
353 " #{Version.table_name}.sharing = 'system'" +
353 " #{Version.table_name}.sharing = 'system'" +
354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
357 "))")
357 "))")
358 end
358 end
359
359
360 # Returns a hash of project users grouped by role
360 # Returns a hash of project users grouped by role
361 def users_by_role
361 def users_by_role
362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
363 m.roles.each do |r|
363 m.roles.each do |r|
364 h[r] ||= []
364 h[r] ||= []
365 h[r] << m.user
365 h[r] << m.user
366 end
366 end
367 h
367 h
368 end
368 end
369 end
369 end
370
370
371 # Deletes all project's members
371 # Deletes all project's members
372 def delete_all_members
372 def delete_all_members
373 me, mr = Member.table_name, MemberRole.table_name
373 me, mr = Member.table_name, MemberRole.table_name
374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
375 Member.delete_all(['project_id = ?', id])
375 Member.delete_all(['project_id = ?', id])
376 end
376 end
377
377
378 # Users issues can be assigned to
378 # Users issues can be assigned to
379 def assignable_users
379 def assignable_users
380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
381 end
381 end
382
382
383 # Returns the mail adresses of users that should be always notified on project events
383 # Returns the mail adresses of users that should be always notified on project events
384 def recipients
384 def recipients
385 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
385 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
386 end
386 end
387
387
388 # Returns the users that should be notified on project events
388 # Returns the users that should be notified on project events
389 def notified_users
389 def notified_users
390 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
390 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
391 end
391 end
392
392
393 # Returns an array of all custom fields enabled for project issues
393 # Returns an array of all custom fields enabled for project issues
394 # (explictly associated custom fields and custom fields enabled for all projects)
394 # (explictly associated custom fields and custom fields enabled for all projects)
395 def all_issue_custom_fields
395 def all_issue_custom_fields
396 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
396 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
397 end
397 end
398
398
399 def project
399 def project
400 self
400 self
401 end
401 end
402
402
403 def <=>(project)
403 def <=>(project)
404 name.downcase <=> project.name.downcase
404 name.downcase <=> project.name.downcase
405 end
405 end
406
406
407 def to_s
407 def to_s
408 name
408 name
409 end
409 end
410
410
411 # Returns a short description of the projects (first lines)
411 # Returns a short description of the projects (first lines)
412 def short_description(length = 255)
412 def short_description(length = 255)
413 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
413 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
414 end
414 end
415
415
416 def css_classes
417 s = 'project'
418 s << ' root' if root?
419 s << ' child' if child?
420 s << (leaf? ? ' leaf' : ' parent')
421 s
422 end
423
416 # The earliest start date of a project, based on it's issues and versions
424 # The earliest start date of a project, based on it's issues and versions
417 def start_date
425 def start_date
418 if module_enabled?(:issue_tracking)
426 if module_enabled?(:issue_tracking)
419 [
427 [
420 issues.minimum('start_date'),
428 issues.minimum('start_date'),
421 shared_versions.collect(&:effective_date),
429 shared_versions.collect(&:effective_date),
422 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
430 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
423 ].flatten.compact.min
431 ].flatten.compact.min
424 end
432 end
425 end
433 end
426
434
427 # The latest due date of an issue or version
435 # The latest due date of an issue or version
428 def due_date
436 def due_date
429 if module_enabled?(:issue_tracking)
437 if module_enabled?(:issue_tracking)
430 [
438 [
431 issues.maximum('due_date'),
439 issues.maximum('due_date'),
432 shared_versions.collect(&:effective_date),
440 shared_versions.collect(&:effective_date),
433 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
441 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
434 ].flatten.compact.max
442 ].flatten.compact.max
435 end
443 end
436 end
444 end
437
445
438 def overdue?
446 def overdue?
439 active? && !due_date.nil? && (due_date < Date.today)
447 active? && !due_date.nil? && (due_date < Date.today)
440 end
448 end
441
449
442 # Returns the percent completed for this project, based on the
450 # Returns the percent completed for this project, based on the
443 # progress on it's versions.
451 # progress on it's versions.
444 def completed_percent(options={:include_subprojects => false})
452 def completed_percent(options={:include_subprojects => false})
445 if options.delete(:include_subprojects)
453 if options.delete(:include_subprojects)
446 total = self_and_descendants.collect(&:completed_percent).sum
454 total = self_and_descendants.collect(&:completed_percent).sum
447
455
448 total / self_and_descendants.count
456 total / self_and_descendants.count
449 else
457 else
450 if versions.count > 0
458 if versions.count > 0
451 total = versions.collect(&:completed_pourcent).sum
459 total = versions.collect(&:completed_pourcent).sum
452
460
453 total / versions.count
461 total / versions.count
454 else
462 else
455 100
463 100
456 end
464 end
457 end
465 end
458 end
466 end
459
467
460 # Return true if this project is allowed to do the specified action.
468 # Return true if this project is allowed to do the specified action.
461 # action can be:
469 # action can be:
462 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
470 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
463 # * a permission Symbol (eg. :edit_project)
471 # * a permission Symbol (eg. :edit_project)
464 def allows_to?(action)
472 def allows_to?(action)
465 if action.is_a? Hash
473 if action.is_a? Hash
466 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
474 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
467 else
475 else
468 allowed_permissions.include? action
476 allowed_permissions.include? action
469 end
477 end
470 end
478 end
471
479
472 def module_enabled?(module_name)
480 def module_enabled?(module_name)
473 module_name = module_name.to_s
481 module_name = module_name.to_s
474 enabled_modules.detect {|m| m.name == module_name}
482 enabled_modules.detect {|m| m.name == module_name}
475 end
483 end
476
484
477 def enabled_module_names=(module_names)
485 def enabled_module_names=(module_names)
478 if module_names && module_names.is_a?(Array)
486 if module_names && module_names.is_a?(Array)
479 module_names = module_names.collect(&:to_s)
487 module_names = module_names.collect(&:to_s)
480 # remove disabled modules
488 # remove disabled modules
481 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
489 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
482 # add new modules
490 # add new modules
483 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
491 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
484 else
492 else
485 enabled_modules.clear
493 enabled_modules.clear
486 end
494 end
487 end
495 end
488
496
489 # Returns an auto-generated project identifier based on the last identifier used
497 # Returns an auto-generated project identifier based on the last identifier used
490 def self.next_identifier
498 def self.next_identifier
491 p = Project.find(:first, :order => 'created_on DESC')
499 p = Project.find(:first, :order => 'created_on DESC')
492 p.nil? ? nil : p.identifier.to_s.succ
500 p.nil? ? nil : p.identifier.to_s.succ
493 end
501 end
494
502
495 # Copies and saves the Project instance based on the +project+.
503 # Copies and saves the Project instance based on the +project+.
496 # Duplicates the source project's:
504 # Duplicates the source project's:
497 # * Wiki
505 # * Wiki
498 # * Versions
506 # * Versions
499 # * Categories
507 # * Categories
500 # * Issues
508 # * Issues
501 # * Members
509 # * Members
502 # * Queries
510 # * Queries
503 #
511 #
504 # Accepts an +options+ argument to specify what to copy
512 # Accepts an +options+ argument to specify what to copy
505 #
513 #
506 # Examples:
514 # Examples:
507 # project.copy(1) # => copies everything
515 # project.copy(1) # => copies everything
508 # project.copy(1, :only => 'members') # => copies members only
516 # project.copy(1, :only => 'members') # => copies members only
509 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
517 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
510 def copy(project, options={})
518 def copy(project, options={})
511 project = project.is_a?(Project) ? project : Project.find(project)
519 project = project.is_a?(Project) ? project : Project.find(project)
512
520
513 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
521 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
514 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
522 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
515
523
516 Project.transaction do
524 Project.transaction do
517 if save
525 if save
518 reload
526 reload
519 to_be_copied.each do |name|
527 to_be_copied.each do |name|
520 send "copy_#{name}", project
528 send "copy_#{name}", project
521 end
529 end
522 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
530 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
523 save
531 save
524 end
532 end
525 end
533 end
526 end
534 end
527
535
528
536
529 # Copies +project+ and returns the new instance. This will not save
537 # Copies +project+ and returns the new instance. This will not save
530 # the copy
538 # the copy
531 def self.copy_from(project)
539 def self.copy_from(project)
532 begin
540 begin
533 project = project.is_a?(Project) ? project : Project.find(project)
541 project = project.is_a?(Project) ? project : Project.find(project)
534 if project
542 if project
535 # clear unique attributes
543 # clear unique attributes
536 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
544 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
537 copy = Project.new(attributes)
545 copy = Project.new(attributes)
538 copy.enabled_modules = project.enabled_modules
546 copy.enabled_modules = project.enabled_modules
539 copy.trackers = project.trackers
547 copy.trackers = project.trackers
540 copy.custom_values = project.custom_values.collect {|v| v.clone}
548 copy.custom_values = project.custom_values.collect {|v| v.clone}
541 copy.issue_custom_fields = project.issue_custom_fields
549 copy.issue_custom_fields = project.issue_custom_fields
542 return copy
550 return copy
543 else
551 else
544 return nil
552 return nil
545 end
553 end
546 rescue ActiveRecord::RecordNotFound
554 rescue ActiveRecord::RecordNotFound
547 return nil
555 return nil
548 end
556 end
549 end
557 end
550
558
551 private
559 private
552
560
553 # Destroys children before destroying self
561 # Destroys children before destroying self
554 def destroy_children
562 def destroy_children
555 children.each do |child|
563 children.each do |child|
556 child.destroy
564 child.destroy
557 end
565 end
558 end
566 end
559
567
560 # Copies wiki from +project+
568 # Copies wiki from +project+
561 def copy_wiki(project)
569 def copy_wiki(project)
562 # Check that the source project has a wiki first
570 # Check that the source project has a wiki first
563 unless project.wiki.nil?
571 unless project.wiki.nil?
564 self.wiki ||= Wiki.new
572 self.wiki ||= Wiki.new
565 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
573 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
566 wiki_pages_map = {}
574 wiki_pages_map = {}
567 project.wiki.pages.each do |page|
575 project.wiki.pages.each do |page|
568 # Skip pages without content
576 # Skip pages without content
569 next if page.content.nil?
577 next if page.content.nil?
570 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
578 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
571 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
579 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
572 new_wiki_page.content = new_wiki_content
580 new_wiki_page.content = new_wiki_content
573 wiki.pages << new_wiki_page
581 wiki.pages << new_wiki_page
574 wiki_pages_map[page.id] = new_wiki_page
582 wiki_pages_map[page.id] = new_wiki_page
575 end
583 end
576 wiki.save
584 wiki.save
577 # Reproduce page hierarchy
585 # Reproduce page hierarchy
578 project.wiki.pages.each do |page|
586 project.wiki.pages.each do |page|
579 if page.parent_id && wiki_pages_map[page.id]
587 if page.parent_id && wiki_pages_map[page.id]
580 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
588 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
581 wiki_pages_map[page.id].save
589 wiki_pages_map[page.id].save
582 end
590 end
583 end
591 end
584 end
592 end
585 end
593 end
586
594
587 # Copies versions from +project+
595 # Copies versions from +project+
588 def copy_versions(project)
596 def copy_versions(project)
589 project.versions.each do |version|
597 project.versions.each do |version|
590 new_version = Version.new
598 new_version = Version.new
591 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
599 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
592 self.versions << new_version
600 self.versions << new_version
593 end
601 end
594 end
602 end
595
603
596 # Copies issue categories from +project+
604 # Copies issue categories from +project+
597 def copy_issue_categories(project)
605 def copy_issue_categories(project)
598 project.issue_categories.each do |issue_category|
606 project.issue_categories.each do |issue_category|
599 new_issue_category = IssueCategory.new
607 new_issue_category = IssueCategory.new
600 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
608 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
601 self.issue_categories << new_issue_category
609 self.issue_categories << new_issue_category
602 end
610 end
603 end
611 end
604
612
605 # Copies issues from +project+
613 # Copies issues from +project+
606 def copy_issues(project)
614 def copy_issues(project)
607 # Stores the source issue id as a key and the copied issues as the
615 # Stores the source issue id as a key and the copied issues as the
608 # value. Used to map the two togeather for issue relations.
616 # value. Used to map the two togeather for issue relations.
609 issues_map = {}
617 issues_map = {}
610
618
611 # Get issues sorted by root_id, lft so that parent issues
619 # Get issues sorted by root_id, lft so that parent issues
612 # get copied before their children
620 # get copied before their children
613 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
621 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
614 new_issue = Issue.new
622 new_issue = Issue.new
615 new_issue.copy_from(issue)
623 new_issue.copy_from(issue)
616 new_issue.project = self
624 new_issue.project = self
617 # Reassign fixed_versions by name, since names are unique per
625 # Reassign fixed_versions by name, since names are unique per
618 # project and the versions for self are not yet saved
626 # project and the versions for self are not yet saved
619 if issue.fixed_version
627 if issue.fixed_version
620 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
628 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
621 end
629 end
622 # Reassign the category by name, since names are unique per
630 # Reassign the category by name, since names are unique per
623 # project and the categories for self are not yet saved
631 # project and the categories for self are not yet saved
624 if issue.category
632 if issue.category
625 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
633 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
626 end
634 end
627 # Parent issue
635 # Parent issue
628 if issue.parent_id
636 if issue.parent_id
629 if copied_parent = issues_map[issue.parent_id]
637 if copied_parent = issues_map[issue.parent_id]
630 new_issue.parent_issue_id = copied_parent.id
638 new_issue.parent_issue_id = copied_parent.id
631 end
639 end
632 end
640 end
633
641
634 self.issues << new_issue
642 self.issues << new_issue
635 issues_map[issue.id] = new_issue
643 issues_map[issue.id] = new_issue
636 end
644 end
637
645
638 # Relations after in case issues related each other
646 # Relations after in case issues related each other
639 project.issues.each do |issue|
647 project.issues.each do |issue|
640 new_issue = issues_map[issue.id]
648 new_issue = issues_map[issue.id]
641
649
642 # Relations
650 # Relations
643 issue.relations_from.each do |source_relation|
651 issue.relations_from.each do |source_relation|
644 new_issue_relation = IssueRelation.new
652 new_issue_relation = IssueRelation.new
645 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
653 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
646 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
654 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
647 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
655 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
648 new_issue_relation.issue_to = source_relation.issue_to
656 new_issue_relation.issue_to = source_relation.issue_to
649 end
657 end
650 new_issue.relations_from << new_issue_relation
658 new_issue.relations_from << new_issue_relation
651 end
659 end
652
660
653 issue.relations_to.each do |source_relation|
661 issue.relations_to.each do |source_relation|
654 new_issue_relation = IssueRelation.new
662 new_issue_relation = IssueRelation.new
655 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
663 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
656 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
664 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
657 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
665 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
658 new_issue_relation.issue_from = source_relation.issue_from
666 new_issue_relation.issue_from = source_relation.issue_from
659 end
667 end
660 new_issue.relations_to << new_issue_relation
668 new_issue.relations_to << new_issue_relation
661 end
669 end
662 end
670 end
663 end
671 end
664
672
665 # Copies members from +project+
673 # Copies members from +project+
666 def copy_members(project)
674 def copy_members(project)
667 project.memberships.each do |member|
675 project.memberships.each do |member|
668 new_member = Member.new
676 new_member = Member.new
669 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
677 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
670 # only copy non inherited roles
678 # only copy non inherited roles
671 # inherited roles will be added when copying the group membership
679 # inherited roles will be added when copying the group membership
672 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
680 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
673 next if role_ids.empty?
681 next if role_ids.empty?
674 new_member.role_ids = role_ids
682 new_member.role_ids = role_ids
675 new_member.project = self
683 new_member.project = self
676 self.members << new_member
684 self.members << new_member
677 end
685 end
678 end
686 end
679
687
680 # Copies queries from +project+
688 # Copies queries from +project+
681 def copy_queries(project)
689 def copy_queries(project)
682 project.queries.each do |query|
690 project.queries.each do |query|
683 new_query = Query.new
691 new_query = Query.new
684 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
692 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
685 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
693 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
686 new_query.project = self
694 new_query.project = self
687 self.queries << new_query
695 self.queries << new_query
688 end
696 end
689 end
697 end
690
698
691 # Copies boards from +project+
699 # Copies boards from +project+
692 def copy_boards(project)
700 def copy_boards(project)
693 project.boards.each do |board|
701 project.boards.each do |board|
694 new_board = Board.new
702 new_board = Board.new
695 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
703 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
696 new_board.project = self
704 new_board.project = self
697 self.boards << new_board
705 self.boards << new_board
698 end
706 end
699 end
707 end
700
708
701 def allowed_permissions
709 def allowed_permissions
702 @allowed_permissions ||= begin
710 @allowed_permissions ||= begin
703 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
711 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
704 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
712 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
705 end
713 end
706 end
714 end
707
715
708 def allowed_actions
716 def allowed_actions
709 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
717 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
710 end
718 end
711
719
712 # Returns all the active Systemwide and project specific activities
720 # Returns all the active Systemwide and project specific activities
713 def active_activities
721 def active_activities
714 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
722 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
715
723
716 if overridden_activity_ids.empty?
724 if overridden_activity_ids.empty?
717 return TimeEntryActivity.shared.active
725 return TimeEntryActivity.shared.active
718 else
726 else
719 return system_activities_and_project_overrides
727 return system_activities_and_project_overrides
720 end
728 end
721 end
729 end
722
730
723 # Returns all the Systemwide and project specific activities
731 # Returns all the Systemwide and project specific activities
724 # (inactive and active)
732 # (inactive and active)
725 def all_activities
733 def all_activities
726 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
734 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
727
735
728 if overridden_activity_ids.empty?
736 if overridden_activity_ids.empty?
729 return TimeEntryActivity.shared
737 return TimeEntryActivity.shared
730 else
738 else
731 return system_activities_and_project_overrides(true)
739 return system_activities_and_project_overrides(true)
732 end
740 end
733 end
741 end
734
742
735 # Returns the systemwide active activities merged with the project specific overrides
743 # Returns the systemwide active activities merged with the project specific overrides
736 def system_activities_and_project_overrides(include_inactive=false)
744 def system_activities_and_project_overrides(include_inactive=false)
737 if include_inactive
745 if include_inactive
738 return TimeEntryActivity.shared.
746 return TimeEntryActivity.shared.
739 find(:all,
747 find(:all,
740 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
748 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
741 self.time_entry_activities
749 self.time_entry_activities
742 else
750 else
743 return TimeEntryActivity.shared.active.
751 return TimeEntryActivity.shared.active.
744 find(:all,
752 find(:all,
745 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
753 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
746 self.time_entry_activities.active
754 self.time_entry_activities.active
747 end
755 end
748 end
756 end
749
757
750 # Archives subprojects recursively
758 # Archives subprojects recursively
751 def archive!
759 def archive!
752 children.each do |subproject|
760 children.each do |subproject|
753 subproject.send :archive!
761 subproject.send :archive!
754 end
762 end
755 update_attribute :status, STATUS_ARCHIVED
763 update_attribute :status, STATUS_ARCHIVED
756 end
764 end
757 end
765 end
@@ -1,46 +1,46
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
2 <%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_project_plural)%></h2>
5 <h2><%=l(:label_project_plural)%></h2>
6
6
7 <% form_tag({}, :method => :get) do %>
7 <% form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label><%= l(:field_status) %> :</label>
9 <label><%= l(:field_status) %> :</label>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
11 <label><%= l(:label_project) %>:</label>
11 <label><%= l(:label_project) %>:</label>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
12 <%= text_field_tag 'name', params[:name], :size => 30 %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
13 <%= submit_tag l(:button_apply), :class => "small", :name => nil %>
14 </fieldset>
14 </fieldset>
15 <% end %>
15 <% end %>
16 &nbsp;
16 &nbsp;
17
17
18 <div class="autoscroll">
18 <div class="autoscroll">
19 <table class="list">
19 <table class="list">
20 <thead><tr>
20 <thead><tr>
21 <th><%=l(:label_project)%></th>
21 <th><%=l(:label_project)%></th>
22 <th><%=l(:field_description)%></th>
22 <th><%=l(:field_description)%></th>
23 <th><%=l(:field_is_public)%></th>
23 <th><%=l(:field_is_public)%></th>
24 <th><%=l(:field_created_on)%></th>
24 <th><%=l(:field_created_on)%></th>
25 <th></th>
25 <th></th>
26 </tr></thead>
26 </tr></thead>
27 <tbody>
27 <tbody>
28 <% project_tree(@projects) do |project, level| %>
28 <% project_tree(@projects) do |project, level| %>
29 <tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 <tr class="<%= cycle("odd", "even") %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
30 <td class="name"><%= link_to_project(project, :action => 'settings') %></td>
30 <td class="name"><%= link_to_project(project, :action => 'settings') %></td>
31 <td><%= textilizable project.short_description, :project => project %></td>
31 <td><%= textilizable project.short_description, :project => project %></td>
32 <td align="center"><%= checked_image project.is_public? %></td>
32 <td align="center"><%= checked_image project.is_public? %></td>
33 <td align="center"><%= format_date(project.created_on) %></td>
33 <td align="center"><%= format_date(project.created_on) %></td>
34 <td class="buttons">
34 <td class="buttons">
35 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
35 <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %>
36 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
36 <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %>
37 <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
37 <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %>
38 <%= link_to(l(:button_delete), project_destroy_confirm_path(project), :class => 'icon icon-del') %>
38 <%= link_to(l(:button_delete), project_destroy_confirm_path(project), :class => 'icon icon-del') %>
39 </td>
39 </td>
40 </tr>
40 </tr>
41 <% end %>
41 <% end %>
42 </tbody>
42 </tbody>
43 </table>
43 </table>
44 </div>
44 </div>
45
45
46 <% html_title(l(:label_project_plural)) -%>
46 <% html_title(l(:label_project_plural)) -%>
General Comments 0
You need to be logged in to leave comments. Login now