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