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