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