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