##// END OF EJS Templates
Project copy: let the user choose what to copy from the source project (everything by default)....
Jean-Philippe Lang -
r2852:5b787785b476
parent child
Show More
@@ -1,345 +1,345
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :activity, :only => :activity
21 21 menu_item :roadmap, :only => :roadmap
22 22 menu_item :files, :only => [:list_files, :add_file]
23 23 menu_item :settings, :only => :settings
24 24 menu_item :issues, :only => [:changelog]
25 25
26 26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
27 27 before_filter :find_optional_project, :only => :activity
28 28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
29 29 before_filter :authorize_global, :only => :add
30 30 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
31 31 accept_key_auth :activity
32 32
33 33 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
34 34 if controller.request.post?
35 35 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
36 36 end
37 37 end
38 38
39 39 helper :sort
40 40 include SortHelper
41 41 helper :custom_fields
42 42 include CustomFieldsHelper
43 43 helper :issues
44 44 helper IssuesHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :repositories
48 48 include RepositoriesHelper
49 49 include ProjectsHelper
50 50
51 51 # Lists visible projects
52 52 def index
53 53 respond_to do |format|
54 54 format.html {
55 55 @projects = Project.visible.find(:all, :order => 'lft')
56 56 }
57 57 format.atom {
58 58 projects = Project.visible.find(:all, :order => 'created_on DESC',
59 59 :limit => Setting.feeds_limit.to_i)
60 60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 61 }
62 62 end
63 63 end
64 64
65 65 # Add a new project
66 66 def add
67 67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
68 68 @trackers = Tracker.all
69 69 @project = Project.new(params[:project])
70 70 if request.get?
71 71 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
72 72 @project.trackers = Tracker.all
73 73 @project.is_public = Setting.default_projects_public?
74 74 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
75 75 else
76 76 @project.enabled_module_names = params[:enabled_modules]
77 77 if @project.save
78 78 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
79 79 # Add current user as a project member if he is not admin
80 80 unless User.current.admin?
81 81 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
82 82 m = Member.new(:user => User.current, :roles => [r])
83 83 @project.members << m
84 84 end
85 85 flash[:notice] = l(:notice_successful_create)
86 86 redirect_to :controller => 'projects', :action => 'settings', :id => @project
87 87 end
88 88 end
89 89 end
90 90
91 91 def copy
92 92 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
93 93 @trackers = Tracker.all
94 94 @root_projects = Project.find(:all,
95 95 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
96 96 :order => 'name')
97 97 if request.get?
98 98 @project = Project.copy_from(params[:id])
99 99 if @project
100 100 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
101 101 else
102 102 redirect_to :controller => 'admin', :action => 'projects'
103 103 end
104 104 else
105 105 @project = Project.new(params[:project])
106 106 @project.enabled_module_names = params[:enabled_modules]
107 if @project.copy(params[:id])
107 if @project.copy(params[:id], :only => params[:only])
108 108 flash[:notice] = l(:notice_successful_create)
109 109 redirect_to :controller => 'admin', :action => 'projects'
110 110 end
111 111 end
112 112 end
113 113
114 114
115 115 # Show @project
116 116 def show
117 117 if params[:jump]
118 118 # try to redirect to the requested menu item
119 119 redirect_to_project_menu_item(@project, params[:jump]) && return
120 120 end
121 121
122 122 @users_by_role = @project.users_by_role
123 123 @subprojects = @project.children.visible
124 124 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
125 125 @trackers = @project.rolled_up_trackers
126 126
127 127 cond = @project.project_condition(Setting.display_subprojects_issues?)
128 128
129 129 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
130 130 :include => [:project, :status, :tracker],
131 131 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
132 132 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
133 133 :include => [:project, :status, :tracker],
134 134 :conditions => cond)
135 135
136 136 TimeEntry.visible_by(User.current) do
137 137 @total_hours = TimeEntry.sum(:hours,
138 138 :include => :project,
139 139 :conditions => cond).to_f
140 140 end
141 141 @key = User.current.rss_key
142 142 end
143 143
144 144 def settings
145 145 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
146 146 @issue_category ||= IssueCategory.new
147 147 @member ||= @project.members.new
148 148 @trackers = Tracker.all
149 149 @repository ||= @project.repository
150 150 @wiki ||= @project.wiki
151 151 end
152 152
153 153 # Edit @project
154 154 def edit
155 155 if request.post?
156 156 @project.attributes = params[:project]
157 157 if @project.save
158 158 @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id')
159 159 flash[:notice] = l(:notice_successful_update)
160 160 redirect_to :action => 'settings', :id => @project
161 161 else
162 162 settings
163 163 render :action => 'settings'
164 164 end
165 165 end
166 166 end
167 167
168 168 def modules
169 169 @project.enabled_module_names = params[:enabled_modules]
170 170 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
171 171 end
172 172
173 173 def archive
174 174 @project.archive if request.post? && @project.active?
175 175 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
176 176 end
177 177
178 178 def unarchive
179 179 @project.unarchive if request.post? && !@project.active?
180 180 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
181 181 end
182 182
183 183 # Delete @project
184 184 def destroy
185 185 @project_to_destroy = @project
186 186 if request.post? and params[:confirm]
187 187 @project_to_destroy.destroy
188 188 redirect_to :controller => 'admin', :action => 'projects'
189 189 end
190 190 # hide project in layout
191 191 @project = nil
192 192 end
193 193
194 194 # Add a new issue category to @project
195 195 def add_issue_category
196 196 @category = @project.issue_categories.build(params[:category])
197 197 if request.post? and @category.save
198 198 respond_to do |format|
199 199 format.html do
200 200 flash[:notice] = l(:notice_successful_create)
201 201 redirect_to :action => 'settings', :tab => 'categories', :id => @project
202 202 end
203 203 format.js do
204 204 # IE doesn't support the replace_html rjs method for select box options
205 205 render(:update) {|page| page.replace "issue_category_id",
206 206 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
207 207 }
208 208 end
209 209 end
210 210 end
211 211 end
212 212
213 213 # Add a new version to @project
214 214 def add_version
215 215 @version = @project.versions.build(params[:version])
216 216 if request.post? and @version.save
217 217 flash[:notice] = l(:notice_successful_create)
218 218 redirect_to :action => 'settings', :tab => 'versions', :id => @project
219 219 end
220 220 end
221 221
222 222 def add_file
223 223 if request.post?
224 224 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
225 225 attachments = attach_files(container, params[:attachments])
226 226 if !attachments.empty? && Setting.notified_events.include?('file_added')
227 227 Mailer.deliver_attachments_added(attachments)
228 228 end
229 229 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
230 230 return
231 231 end
232 232 @versions = @project.versions.sort
233 233 end
234 234
235 235 def save_activities
236 236 if request.post? && params[:enumerations]
237 237 Project.transaction do
238 238 params[:enumerations].each do |id, activity|
239 239 @project.update_or_create_time_entry_activity(id, activity)
240 240 end
241 241 end
242 242 end
243 243
244 244 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
245 245 end
246 246
247 247 def reset_activities
248 248 @project.time_entry_activities.each do |time_entry_activity|
249 249 time_entry_activity.destroy(time_entry_activity.parent)
250 250 end
251 251 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
252 252 end
253 253
254 254 def list_files
255 255 sort_init 'filename', 'asc'
256 256 sort_update 'filename' => "#{Attachment.table_name}.filename",
257 257 'created_on' => "#{Attachment.table_name}.created_on",
258 258 'size' => "#{Attachment.table_name}.filesize",
259 259 'downloads' => "#{Attachment.table_name}.downloads"
260 260
261 261 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
262 262 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
263 263 render :layout => !request.xhr?
264 264 end
265 265
266 266 # Show changelog for @project
267 267 def changelog
268 268 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
269 269 retrieve_selected_tracker_ids(@trackers)
270 270 @versions = @project.versions.sort
271 271 end
272 272
273 273 def roadmap
274 274 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
275 275 retrieve_selected_tracker_ids(@trackers)
276 276 @versions = @project.versions.sort
277 277 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
278 278 end
279 279
280 280 def activity
281 281 @days = Setting.activity_days_default.to_i
282 282
283 283 if params[:from]
284 284 begin; @date_to = params[:from].to_date + 1; rescue; end
285 285 end
286 286
287 287 @date_to ||= Date.today + 1
288 288 @date_from = @date_to - @days
289 289 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
290 290 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
291 291
292 292 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
293 293 :with_subprojects => @with_subprojects,
294 294 :author => @author)
295 295 @activity.scope_select {|t| !params["show_#{t}"].nil?}
296 296 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
297 297
298 298 events = @activity.events(@date_from, @date_to)
299 299
300 300 respond_to do |format|
301 301 format.html {
302 302 @events_by_day = events.group_by(&:event_date)
303 303 render :layout => false if request.xhr?
304 304 }
305 305 format.atom {
306 306 title = l(:label_activity)
307 307 if @author
308 308 title = @author.name
309 309 elsif @activity.scope.size == 1
310 310 title = l("label_#{@activity.scope.first.singularize}_plural")
311 311 end
312 312 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
313 313 }
314 314 end
315 315
316 316 rescue ActiveRecord::RecordNotFound
317 317 render_404
318 318 end
319 319
320 320 private
321 321 # Find project of id params[:id]
322 322 # if not found, redirect to project list
323 323 # Used as a before_filter
324 324 def find_project
325 325 @project = Project.find(params[:id])
326 326 rescue ActiveRecord::RecordNotFound
327 327 render_404
328 328 end
329 329
330 330 def find_optional_project
331 331 return true unless params[:id]
332 332 @project = Project.find(params[:id])
333 333 authorize
334 334 rescue ActiveRecord::RecordNotFound
335 335 render_404
336 336 end
337 337
338 338 def retrieve_selected_tracker_ids(selectable_trackers)
339 339 if ids = params[:tracker_ids]
340 340 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
341 341 else
342 342 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
343 343 end
344 344 end
345 345 end
@@ -1,538 +1,566
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 # Specific overidden Activities
24 24 has_many :time_entry_activities do
25 25 def active
26 26 find(:all, :conditions => {:active => true})
27 27 end
28 28 end
29 29 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 30 has_many :member_principals, :class_name => 'Member',
31 31 :include => :principal,
32 32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 33 has_many :users, :through => :members
34 34 has_many :principals, :through => :member_principals, :source => :principal
35 35
36 36 has_many :enabled_modules, :dependent => :delete_all
37 37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 39 has_many :issue_changes, :through => :issues, :source => :journals
40 40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 41 has_many :time_entries, :dependent => :delete_all
42 42 has_many :queries, :dependent => :delete_all
43 43 has_many :documents, :dependent => :destroy
44 44 has_many :news, :dependent => :delete_all, :include => :author
45 45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 46 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 47 has_one :repository, :dependent => :destroy
48 48 has_many :changesets, :through => :repository
49 49 has_one :wiki, :dependent => :destroy
50 50 # Custom field for the project issues
51 51 has_and_belongs_to_many :issue_custom_fields,
52 52 :class_name => 'IssueCustomField',
53 53 :order => "#{CustomField.table_name}.position",
54 54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 55 :association_foreign_key => 'custom_field_id'
56 56
57 57 acts_as_nested_set :order => 'name', :dependent => :destroy
58 58 acts_as_attachable :view_permission => :view_files,
59 59 :delete_permission => :manage_files
60 60
61 61 acts_as_customizable
62 62 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
63 63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
65 65 :author => nil
66 66
67 67 attr_protected :status, :enabled_module_names
68 68
69 69 validates_presence_of :name, :identifier
70 70 validates_uniqueness_of :name, :identifier
71 71 validates_associated :repository, :wiki
72 72 validates_length_of :name, :maximum => 30
73 73 validates_length_of :homepage, :maximum => 255
74 74 validates_length_of :identifier, :in => 1..20
75 75 # donwcase letters, digits, dashes but not digits only
76 76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 77 # reserved words
78 78 validates_exclusion_of :identifier, :in => %w( new )
79 79
80 80 before_destroy :delete_all_members
81 81
82 82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
83 83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 84 named_scope :all_public, { :conditions => { :is_public => true } }
85 85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86 86
87 87 def identifier=(identifier)
88 88 super unless identifier_frozen?
89 89 end
90 90
91 91 def identifier_frozen?
92 92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
93 93 end
94 94
95 95 def issues_with_subprojects(include_subprojects=false)
96 96 conditions = nil
97 97 if include_subprojects
98 98 ids = [id] + descendants.collect(&:id)
99 99 conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
100 100 end
101 101 conditions ||= ["#{Project.table_name}.id = ?", id]
102 102 # Quick and dirty fix for Rails 2 compatibility
103 103 Issue.send(:with_scope, :find => { :conditions => conditions }) do
104 104 Version.send(:with_scope, :find => { :conditions => conditions }) do
105 105 yield
106 106 end
107 107 end
108 108 end
109 109
110 110 # returns latest created projects
111 111 # non public projects will be returned only if user is a member of those
112 112 def self.latest(user=nil, count=5)
113 113 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
114 114 end
115 115
116 116 # Returns a SQL :conditions string used to find all active projects for the specified user.
117 117 #
118 118 # Examples:
119 119 # Projects.visible_by(admin) => "projects.status = 1"
120 120 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
121 121 def self.visible_by(user=nil)
122 122 user ||= User.current
123 123 if user && user.admin?
124 124 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
125 125 elsif user && user.memberships.any?
126 126 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
127 127 else
128 128 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
129 129 end
130 130 end
131 131
132 132 def self.allowed_to_condition(user, permission, options={})
133 133 statements = []
134 134 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
135 135 if perm = Redmine::AccessControl.permission(permission)
136 136 unless perm.project_module.nil?
137 137 # If the permission belongs to a project module, make sure the module is enabled
138 138 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
139 139 end
140 140 end
141 141 if options[:project]
142 142 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
143 143 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
144 144 base_statement = "(#{project_statement}) AND (#{base_statement})"
145 145 end
146 146 if user.admin?
147 147 # no restriction
148 148 else
149 149 statements << "1=0"
150 150 if user.logged?
151 151 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
152 152 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
153 153 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
154 154 elsif Role.anonymous.allowed_to?(permission)
155 155 # anonymous user allowed on public project
156 156 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 157 else
158 158 # anonymous user is not authorized
159 159 end
160 160 end
161 161 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
162 162 end
163 163
164 164 # Returns the Systemwide and project specific activities
165 165 def activities(include_inactive=false)
166 166 if include_inactive
167 167 return all_activities
168 168 else
169 169 return active_activities
170 170 end
171 171 end
172 172
173 173 # Will create a new Project specific Activity or update an existing one
174 174 #
175 175 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
176 176 # does not successfully save.
177 177 def update_or_create_time_entry_activity(id, activity_hash)
178 178 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
179 179 self.create_time_entry_activity_if_needed(activity_hash)
180 180 else
181 181 activity = project.time_entry_activities.find_by_id(id.to_i)
182 182 activity.update_attributes(activity_hash) if activity
183 183 end
184 184 end
185 185
186 186 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
187 187 #
188 188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
189 189 # does not successfully save.
190 190 def create_time_entry_activity_if_needed(activity)
191 191 if activity['parent_id']
192 192
193 193 parent_activity = TimeEntryActivity.find(activity['parent_id'])
194 194 activity['name'] = parent_activity.name
195 195 activity['position'] = parent_activity.position
196 196
197 197 if Enumeration.overridding_change?(activity, parent_activity)
198 198 project_activity = self.time_entry_activities.create(activity)
199 199
200 200 if project_activity.new_record?
201 201 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
202 202 else
203 203 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
204 204 end
205 205 end
206 206 end
207 207 end
208 208
209 209 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
210 210 #
211 211 # Examples:
212 212 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
213 213 # project.project_condition(false) => "projects.id = 1"
214 214 def project_condition(with_subprojects)
215 215 cond = "#{Project.table_name}.id = #{id}"
216 216 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
217 217 cond
218 218 end
219 219
220 220 def self.find(*args)
221 221 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
222 222 project = find_by_identifier(*args)
223 223 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
224 224 project
225 225 else
226 226 super
227 227 end
228 228 end
229 229
230 230 def to_param
231 231 # id is used for projects with a numeric identifier (compatibility)
232 232 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
233 233 end
234 234
235 235 def active?
236 236 self.status == STATUS_ACTIVE
237 237 end
238 238
239 239 # Archives the project and its descendants recursively
240 240 def archive
241 241 # Archive subprojects if any
242 242 children.each do |subproject|
243 243 subproject.archive
244 244 end
245 245 update_attribute :status, STATUS_ARCHIVED
246 246 end
247 247
248 248 # Unarchives the project
249 249 # All its ancestors must be active
250 250 def unarchive
251 251 return false if ancestors.detect {|a| !a.active?}
252 252 update_attribute :status, STATUS_ACTIVE
253 253 end
254 254
255 255 # Returns an array of projects the project can be moved to
256 256 def possible_parents
257 257 @possible_parents ||= (Project.active.find(:all) - self_and_descendants)
258 258 end
259 259
260 260 # Sets the parent of the project
261 261 # Argument can be either a Project, a String, a Fixnum or nil
262 262 def set_parent!(p)
263 263 unless p.nil? || p.is_a?(Project)
264 264 if p.to_s.blank?
265 265 p = nil
266 266 else
267 267 p = Project.find_by_id(p)
268 268 return false unless p
269 269 end
270 270 end
271 271 if p == parent && !p.nil?
272 272 # Nothing to do
273 273 true
274 274 elsif p.nil? || (p.active? && move_possible?(p))
275 275 # Insert the project so that target's children or root projects stay alphabetically sorted
276 276 sibs = (p.nil? ? self.class.roots : p.children)
277 277 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
278 278 if to_be_inserted_before
279 279 move_to_left_of(to_be_inserted_before)
280 280 elsif p.nil?
281 281 if sibs.empty?
282 282 # move_to_root adds the project in first (ie. left) position
283 283 move_to_root
284 284 else
285 285 move_to_right_of(sibs.last) unless self == sibs.last
286 286 end
287 287 else
288 288 # move_to_child_of adds the project in last (ie.right) position
289 289 move_to_child_of(p)
290 290 end
291 291 true
292 292 else
293 293 # Can not move to the given target
294 294 false
295 295 end
296 296 end
297 297
298 298 # Returns an array of the trackers used by the project and its active sub projects
299 299 def rolled_up_trackers
300 300 @rolled_up_trackers ||=
301 301 Tracker.find(:all, :include => :projects,
302 302 :select => "DISTINCT #{Tracker.table_name}.*",
303 303 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
304 304 :order => "#{Tracker.table_name}.position")
305 305 end
306 306
307 307 # Returns a hash of project users grouped by role
308 308 def users_by_role
309 309 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
310 310 m.roles.each do |r|
311 311 h[r] ||= []
312 312 h[r] << m.user
313 313 end
314 314 h
315 315 end
316 316 end
317 317
318 318 # Deletes all project's members
319 319 def delete_all_members
320 320 me, mr = Member.table_name, MemberRole.table_name
321 321 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
322 322 Member.delete_all(['project_id = ?', id])
323 323 end
324 324
325 325 # Users issues can be assigned to
326 326 def assignable_users
327 327 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
328 328 end
329 329
330 330 # Returns the mail adresses of users that should be always notified on project events
331 331 def recipients
332 332 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
333 333 end
334 334
335 335 # Returns an array of all custom fields enabled for project issues
336 336 # (explictly associated custom fields and custom fields enabled for all projects)
337 337 def all_issue_custom_fields
338 338 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
339 339 end
340 340
341 341 def project
342 342 self
343 343 end
344 344
345 345 def <=>(project)
346 346 name.downcase <=> project.name.downcase
347 347 end
348 348
349 349 def to_s
350 350 name
351 351 end
352 352
353 353 # Returns a short description of the projects (first lines)
354 354 def short_description(length = 255)
355 355 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
356 356 end
357 357
358 358 # Return true if this project is allowed to do the specified action.
359 359 # action can be:
360 360 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
361 361 # * a permission Symbol (eg. :edit_project)
362 362 def allows_to?(action)
363 363 if action.is_a? Hash
364 364 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
365 365 else
366 366 allowed_permissions.include? action
367 367 end
368 368 end
369 369
370 370 def module_enabled?(module_name)
371 371 module_name = module_name.to_s
372 372 enabled_modules.detect {|m| m.name == module_name}
373 373 end
374 374
375 375 def enabled_module_names=(module_names)
376 376 if module_names && module_names.is_a?(Array)
377 377 module_names = module_names.collect(&:to_s)
378 378 # remove disabled modules
379 379 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
380 380 # add new modules
381 381 module_names.each {|name| enabled_modules << EnabledModule.new(:name => name)}
382 382 else
383 383 enabled_modules.clear
384 384 end
385 385 end
386 386
387 387 # Returns an auto-generated project identifier based on the last identifier used
388 388 def self.next_identifier
389 389 p = Project.find(:first, :order => 'created_on DESC')
390 390 p.nil? ? nil : p.identifier.to_s.succ
391 391 end
392 392
393 393 # Copies and saves the Project instance based on the +project+.
394 # Will duplicate the source project's:
394 # Duplicates the source project's:
395 # * Wiki
396 # * Versions
397 # * Categories
395 398 # * Issues
396 399 # * Members
397 400 # * Queries
398 def copy(project)
401 #
402 # Accepts an +options+ argument to specify what to copy
403 #
404 # Examples:
405 # project.copy(1) # => copies everything
406 # project.copy(1, :only => 'members') # => copies members only
407 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
408 def copy(project, options={})
399 409 project = project.is_a?(Project) ? project : Project.find(project)
400
401 Project.transaction do
402 # Wikis
403 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
404 project.wiki.pages.each do |page|
405 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
406 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
407 new_wiki_page.content = new_wiki_content
408
409 self.wiki.pages << new_wiki_page
410 end
411
412 # Versions
413 project.versions.each do |version|
414 new_version = Version.new
415 new_version.attributes = version.attributes.dup.except("project_id")
416 self.versions << new_version
417 end
418
419 project.issue_categories.each do |issue_category|
420 new_issue_category = IssueCategory.new
421 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
422 self.issue_categories << new_issue_category
423 end
424
425 # Issues
426 project.issues.each do |issue|
427 new_issue = Issue.new
428 new_issue.copy_from(issue)
429 # Reassign fixed_versions by name, since names are unique per
430 # project and the versions for self are not yet saved
431 if issue.fixed_version
432 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
433 end
434 # Reassign the category by name, since names are unique per
435 # project and the categories for self are not yet saved
436 if issue.category
437 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
438 end
439
440 self.issues << new_issue
441 end
442 410
443 # Members
444 project.members.each do |member|
445 new_member = Member.new
446 new_member.attributes = member.attributes.dup.except("project_id")
447 new_member.role_ids = member.role_ids.dup
448 new_member.project = self
449 self.members << new_member
450 end
451
452 # Queries
453 project.queries.each do |query|
454 new_query = Query.new
455 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
456 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
457 new_query.project = self
458 self.queries << new_query
411 to_be_copied = %w(wiki versions issue_categories issues members queries)
412 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
413
414 Project.transaction do
415 to_be_copied.each do |name|
416 send "copy_#{name}", project
459 417 end
460
461 418 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
462 419 self.save
463 420 end
464 421 end
465 422
466 423
467 424 # Copies +project+ and returns the new instance. This will not save
468 425 # the copy
469 426 def self.copy_from(project)
470 427 begin
471 428 project = project.is_a?(Project) ? project : Project.find(project)
472 429 if project
473 430 # clear unique attributes
474 431 attributes = project.attributes.dup.except('name', 'identifier', 'id', 'status')
475 432 copy = Project.new(attributes)
476 433 copy.enabled_modules = project.enabled_modules
477 434 copy.trackers = project.trackers
478 435 copy.custom_values = project.custom_values.collect {|v| v.clone}
479 436 copy.issue_custom_fields = project.issue_custom_fields
480 437 return copy
481 438 else
482 439 return nil
483 440 end
484 441 rescue ActiveRecord::RecordNotFound
485 442 return nil
486 443 end
487 444 end
488 445
489 private
446 private
447
448 # Copies wiki from +project+
449 def copy_wiki(project)
450 self.wiki = Wiki.new(project.wiki.attributes.dup.except("project_id"))
451 project.wiki.pages.each do |page|
452 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("page_id"))
453 new_wiki_page = WikiPage.new(page.attributes.dup.except("wiki_id"))
454 new_wiki_page.content = new_wiki_content
455 self.wiki.pages << new_wiki_page
456 end
457 end
458
459 # Copies versions from +project+
460 def copy_versions(project)
461 project.versions.each do |version|
462 new_version = Version.new
463 new_version.attributes = version.attributes.dup.except("project_id")
464 self.versions << new_version
465 end
466 end
467
468 # Copies issue categories from +project+
469 def copy_issue_categories(project)
470 project.issue_categories.each do |issue_category|
471 new_issue_category = IssueCategory.new
472 new_issue_category.attributes = issue_category.attributes.dup.except("project_id")
473 self.issue_categories << new_issue_category
474 end
475 end
476
477 # Copies issues from +project+
478 def copy_issues(project)
479 project.issues.each do |issue|
480 new_issue = Issue.new
481 new_issue.copy_from(issue)
482 # Reassign fixed_versions by name, since names are unique per
483 # project and the versions for self are not yet saved
484 if issue.fixed_version
485 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
486 end
487 # Reassign the category by name, since names are unique per
488 # project and the categories for self are not yet saved
489 if issue.category
490 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
491 end
492 self.issues << new_issue
493 end
494 end
495
496 # Copies members from +project+
497 def copy_members(project)
498 project.members.each do |member|
499 new_member = Member.new
500 new_member.attributes = member.attributes.dup.except("project_id")
501 new_member.role_ids = member.role_ids.dup
502 new_member.project = self
503 self.members << new_member
504 end
505 end
506
507 # Copies queries from +project+
508 def copy_queries(project)
509 project.queries.each do |query|
510 new_query = Query.new
511 new_query.attributes = query.attributes.dup.except("project_id", "sort_criteria")
512 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
513 new_query.project = self
514 self.queries << new_query
515 end
516 end
517
490 518 def allowed_permissions
491 519 @allowed_permissions ||= begin
492 520 module_names = enabled_modules.collect {|m| m.name}
493 521 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
494 522 end
495 523 end
496 524
497 525 def allowed_actions
498 526 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
499 527 end
500 528
501 529 # Returns all the active Systemwide and project specific activities
502 530 def active_activities
503 531 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
504 532
505 533 if overridden_activity_ids.empty?
506 534 return TimeEntryActivity.active
507 535 else
508 536 return system_activities_and_project_overrides
509 537 end
510 538 end
511 539
512 540 # Returns all the Systemwide and project specific activities
513 541 # (inactive and active)
514 542 def all_activities
515 543 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
516 544
517 545 if overridden_activity_ids.empty?
518 546 return TimeEntryActivity.all
519 547 else
520 548 return system_activities_and_project_overrides(true)
521 549 end
522 550 end
523 551
524 552 # Returns the systemwide active activities merged with the project specific overrides
525 553 def system_activities_and_project_overrides(include_inactive=false)
526 554 if include_inactive
527 555 return TimeEntryActivity.all.
528 556 find(:all,
529 557 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
530 558 self.time_entry_activities
531 559 else
532 560 return TimeEntryActivity.active.
533 561 find(:all,
534 562 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
535 563 self.time_entry_activities.active
536 564 end
537 565 end
538 566 end
@@ -1,16 +1,26
1 1 <h2><%=l(:label_project_new)%></h2>
2 2
3 3 <% labelled_tabular_form_for :project, @project, :url => { :action => "copy" } do |f| %>
4 4 <%= render :partial => 'form', :locals => { :f => f } %>
5 5
6 6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 8 <label class="floating">
9 9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
10 10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 11 </label>
12 12 <% end %>
13 13 </fieldset>
14 14
15 <fieldset class="box"><legend><%= l(:button_copy) %></legend>
16 <label class="floating"><%= check_box_tag 'only[]', 'members', true %> <%= l(:label_member_plural) %></label>
17 <label class="floating"><%= check_box_tag 'only[]', 'versions', true %> <%= l(:label_version_plural) %></label>
18 <label class="floating"><%= check_box_tag 'only[]', 'issue_categories', true %> <%= l(:label_issue_category_plural) %></label>
19 <label class="floating"><%= check_box_tag 'only[]', 'issues', true %> <%= l(:label_issue_plural) %></label>
20 <label class="floating"><%= check_box_tag 'only[]', 'queries', true %> <%= l(:label_query_plural) %></label>
21 <label class="floating"><%= check_box_tag 'only[]', 'wiki', true %> <%= l(:label_wiki) %></label>
22 <%= hidden_field_tag 'only[]', '' %>
23 </fieldset>
24
15 25 <%= submit_tag l(:button_copy) %>
16 26 <% end %>
@@ -1,507 +1,519
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :projects, :enabled_modules,
22 22 :issues, :issue_statuses, :journals, :journal_details,
23 23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 24 :queries
25 25
26 26 def setup
27 27 @ecookbook = Project.find(1)
28 28 @ecookbook_sub1 = Project.find(3)
29 29 end
30 30
31 31 should_validate_presence_of :name
32 32 should_validate_presence_of :identifier
33 33
34 34 should_validate_uniqueness_of :name
35 35 should_validate_uniqueness_of :identifier
36 36
37 37 context "associations" do
38 38 should_have_many :members
39 39 should_have_many :users, :through => :members
40 40 should_have_many :member_principals
41 41 should_have_many :principals, :through => :member_principals
42 42 should_have_many :enabled_modules
43 43 should_have_many :issues
44 44 should_have_many :issue_changes, :through => :issues
45 45 should_have_many :versions
46 46 should_have_many :time_entries
47 47 should_have_many :queries
48 48 should_have_many :documents
49 49 should_have_many :news
50 50 should_have_many :issue_categories
51 51 should_have_many :boards
52 52 should_have_many :changesets, :through => :repository
53 53
54 54 should_have_one :repository
55 55 should_have_one :wiki
56 56
57 57 should_have_and_belong_to_many :trackers
58 58 should_have_and_belong_to_many :issue_custom_fields
59 59 end
60 60
61 61 def test_truth
62 62 assert_kind_of Project, @ecookbook
63 63 assert_equal "eCookbook", @ecookbook.name
64 64 end
65 65
66 66 def test_update
67 67 assert_equal "eCookbook", @ecookbook.name
68 68 @ecookbook.name = "eCook"
69 69 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
70 70 @ecookbook.reload
71 71 assert_equal "eCook", @ecookbook.name
72 72 end
73 73
74 74 def test_validate_identifier
75 75 to_test = {"abc" => true,
76 76 "ab12" => true,
77 77 "ab-12" => true,
78 78 "12" => false,
79 79 "new" => false}
80 80
81 81 to_test.each do |identifier, valid|
82 82 p = Project.new
83 83 p.identifier = identifier
84 84 p.valid?
85 85 assert_equal valid, p.errors.on('identifier').nil?
86 86 end
87 87 end
88 88
89 89 def test_members_should_be_active_users
90 90 Project.all.each do |project|
91 91 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
92 92 end
93 93 end
94 94
95 95 def test_users_should_be_active_users
96 96 Project.all.each do |project|
97 97 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
98 98 end
99 99 end
100 100
101 101 def test_archive
102 102 user = @ecookbook.members.first.user
103 103 @ecookbook.archive
104 104 @ecookbook.reload
105 105
106 106 assert !@ecookbook.active?
107 107 assert !user.projects.include?(@ecookbook)
108 108 # Subproject are also archived
109 109 assert !@ecookbook.children.empty?
110 110 assert @ecookbook.descendants.active.empty?
111 111 end
112 112
113 113 def test_unarchive
114 114 user = @ecookbook.members.first.user
115 115 @ecookbook.archive
116 116 # A subproject of an archived project can not be unarchived
117 117 assert !@ecookbook_sub1.unarchive
118 118
119 119 # Unarchive project
120 120 assert @ecookbook.unarchive
121 121 @ecookbook.reload
122 122 assert @ecookbook.active?
123 123 assert user.projects.include?(@ecookbook)
124 124 # Subproject can now be unarchived
125 125 @ecookbook_sub1.reload
126 126 assert @ecookbook_sub1.unarchive
127 127 end
128 128
129 129 def test_destroy
130 130 # 2 active members
131 131 assert_equal 2, @ecookbook.members.size
132 132 # and 1 is locked
133 133 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
134 134 # some boards
135 135 assert @ecookbook.boards.any?
136 136
137 137 @ecookbook.destroy
138 138 # make sure that the project non longer exists
139 139 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
140 140 # make sure related data was removed
141 141 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
142 142 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
143 143 end
144 144
145 145 def test_move_an_orphan_project_to_a_root_project
146 146 sub = Project.find(2)
147 147 sub.set_parent! @ecookbook
148 148 assert_equal @ecookbook.id, sub.parent.id
149 149 @ecookbook.reload
150 150 assert_equal 4, @ecookbook.children.size
151 151 end
152 152
153 153 def test_move_an_orphan_project_to_a_subproject
154 154 sub = Project.find(2)
155 155 assert sub.set_parent!(@ecookbook_sub1)
156 156 end
157 157
158 158 def test_move_a_root_project_to_a_project
159 159 sub = @ecookbook
160 160 assert sub.set_parent!(Project.find(2))
161 161 end
162 162
163 163 def test_should_not_move_a_project_to_its_children
164 164 sub = @ecookbook
165 165 assert !(sub.set_parent!(Project.find(3)))
166 166 end
167 167
168 168 def test_set_parent_should_add_roots_in_alphabetical_order
169 169 ProjectCustomField.delete_all
170 170 Project.delete_all
171 171 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
172 172 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
173 173 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
174 174 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
175 175
176 176 assert_equal 4, Project.count
177 177 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
178 178 end
179 179
180 180 def test_set_parent_should_add_children_in_alphabetical_order
181 181 ProjectCustomField.delete_all
182 182 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
183 183 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
184 184 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
185 185 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
186 186 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
187 187
188 188 parent.reload
189 189 assert_equal 4, parent.children.size
190 190 assert_equal parent.children.sort_by(&:name), parent.children
191 191 end
192 192
193 193 def test_rebuild_should_sort_children_alphabetically
194 194 ProjectCustomField.delete_all
195 195 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
196 196 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
197 197 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
198 198 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
199 199 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
200 200
201 201 Project.update_all("lft = NULL, rgt = NULL")
202 202 Project.rebuild!
203 203
204 204 parent.reload
205 205 assert_equal 4, parent.children.size
206 206 assert_equal parent.children.sort_by(&:name), parent.children
207 207 end
208 208
209 209 def test_parent
210 210 p = Project.find(6).parent
211 211 assert p.is_a?(Project)
212 212 assert_equal 5, p.id
213 213 end
214 214
215 215 def test_ancestors
216 216 a = Project.find(6).ancestors
217 217 assert a.first.is_a?(Project)
218 218 assert_equal [1, 5], a.collect(&:id)
219 219 end
220 220
221 221 def test_root
222 222 r = Project.find(6).root
223 223 assert r.is_a?(Project)
224 224 assert_equal 1, r.id
225 225 end
226 226
227 227 def test_children
228 228 c = Project.find(1).children
229 229 assert c.first.is_a?(Project)
230 230 assert_equal [5, 3, 4], c.collect(&:id)
231 231 end
232 232
233 233 def test_descendants
234 234 d = Project.find(1).descendants
235 235 assert d.first.is_a?(Project)
236 236 assert_equal [5, 6, 3, 4], d.collect(&:id)
237 237 end
238 238
239 239 def test_users_by_role
240 240 users_by_role = Project.find(1).users_by_role
241 241 assert_kind_of Hash, users_by_role
242 242 role = Role.find(1)
243 243 assert_kind_of Array, users_by_role[role]
244 244 assert users_by_role[role].include?(User.find(2))
245 245 end
246 246
247 247 def test_rolled_up_trackers
248 248 parent = Project.find(1)
249 249 parent.trackers = Tracker.find([1,2])
250 250 child = parent.children.find(3)
251 251
252 252 assert_equal [1, 2], parent.tracker_ids
253 253 assert_equal [2, 3], child.trackers.collect(&:id)
254 254
255 255 assert_kind_of Tracker, parent.rolled_up_trackers.first
256 256 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
257 257
258 258 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
259 259 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
260 260 end
261 261
262 262 def test_rolled_up_trackers_should_ignore_archived_subprojects
263 263 parent = Project.find(1)
264 264 parent.trackers = Tracker.find([1,2])
265 265 child = parent.children.find(3)
266 266 child.trackers = Tracker.find([1,3])
267 267 parent.children.each(&:archive)
268 268
269 269 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
270 270 end
271 271
272 272 def test_next_identifier
273 273 ProjectCustomField.delete_all
274 274 Project.create!(:name => 'last', :identifier => 'p2008040')
275 275 assert_equal 'p2008041', Project.next_identifier
276 276 end
277 277
278 278 def test_next_identifier_first_project
279 279 Project.delete_all
280 280 assert_nil Project.next_identifier
281 281 end
282 282
283 283
284 284 def test_enabled_module_names_should_not_recreate_enabled_modules
285 285 project = Project.find(1)
286 286 # Remove one module
287 287 modules = project.enabled_modules.slice(0..-2)
288 288 assert modules.any?
289 289 assert_difference 'EnabledModule.count', -1 do
290 290 project.enabled_module_names = modules.collect(&:name)
291 291 end
292 292 project.reload
293 293 # Ids should be preserved
294 294 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
295 295 end
296 296
297 297 def test_copy_from_existing_project
298 298 source_project = Project.find(1)
299 299 copied_project = Project.copy_from(1)
300 300
301 301 assert copied_project
302 302 # Cleared attributes
303 303 assert copied_project.id.blank?
304 304 assert copied_project.name.blank?
305 305 assert copied_project.identifier.blank?
306 306
307 307 # Duplicated attributes
308 308 assert_equal source_project.description, copied_project.description
309 309 assert_equal source_project.enabled_modules, copied_project.enabled_modules
310 310 assert_equal source_project.trackers, copied_project.trackers
311 311
312 312 # Default attributes
313 313 assert_equal 1, copied_project.status
314 314 end
315 315
316 316 def test_activities_should_use_the_system_activities
317 317 project = Project.find(1)
318 318 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
319 319 end
320 320
321 321
322 322 def test_activities_should_use_the_project_specific_activities
323 323 project = Project.find(1)
324 324 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
325 325 assert overridden_activity.save!
326 326
327 327 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
328 328 end
329 329
330 330 def test_activities_should_not_include_the_inactive_project_specific_activities
331 331 project = Project.find(1)
332 332 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
333 333 assert overridden_activity.save!
334 334
335 335 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
336 336 end
337 337
338 338 def test_activities_should_not_include_project_specific_activities_from_other_projects
339 339 project = Project.find(1)
340 340 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
341 341 assert overridden_activity.save!
342 342
343 343 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
344 344 end
345 345
346 346 def test_activities_should_handle_nils
347 347 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
348 348 TimeEntryActivity.delete_all
349 349
350 350 # No activities
351 351 project = Project.find(1)
352 352 assert project.activities.empty?
353 353
354 354 # No system, one overridden
355 355 assert overridden_activity.save!
356 356 project.reload
357 357 assert_equal [overridden_activity], project.activities
358 358 end
359 359
360 360 def test_activities_should_override_system_activities_with_project_activities
361 361 project = Project.find(1)
362 362 parent_activity = TimeEntryActivity.find(:first)
363 363 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
364 364 assert overridden_activity.save!
365 365
366 366 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
367 367 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
368 368 end
369 369
370 370 def test_activities_should_include_inactive_activities_if_specified
371 371 project = Project.find(1)
372 372 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
373 373 assert overridden_activity.save!
374 374
375 375 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
376 376 end
377 377
378 378 context "Project#copy" do
379 379 setup do
380 380 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
381 381 Project.destroy_all :identifier => "copy-test"
382 382 @source_project = Project.find(2)
383 383 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
384 384 @project.trackers = @source_project.trackers
385 385 @project.enabled_modules = @source_project.enabled_modules
386 386 end
387 387
388 388 should "copy issues" do
389 389 assert @project.valid?
390 390 assert @project.issues.empty?
391 391 assert @project.copy(@source_project)
392 392
393 393 assert_equal @source_project.issues.size, @project.issues.size
394 394 @project.issues.each do |issue|
395 395 assert issue.valid?
396 396 assert ! issue.assigned_to.blank?
397 397 assert_equal @project, issue.project
398 398 end
399 399 end
400 400
401 401 should "change the new issues to use the copied version" do
402 402 assigned_version = Version.generate!(:name => "Assigned Issues")
403 403 @source_project.versions << assigned_version
404 404 assert_equal 1, @source_project.versions.size
405 405 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
406 406 :subject => "change the new issues to use the copied version",
407 407 :tracker_id => 1,
408 408 :project_id => @source_project.id)
409 409
410 410 assert @project.copy(@source_project)
411 411 @project.reload
412 412 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
413 413
414 414 assert copied_issue
415 415 assert copied_issue.fixed_version
416 416 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
417 417 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
418 418 end
419 419
420 420 should "copy members" do
421 421 assert @project.valid?
422 422 assert @project.members.empty?
423 423 assert @project.copy(@source_project)
424 424
425 425 assert_equal @source_project.members.size, @project.members.size
426 426 @project.members.each do |member|
427 427 assert member
428 428 assert_equal @project, member.project
429 429 end
430 430 end
431 431
432 432 should "copy project specific queries" do
433 433 assert @project.valid?
434 434 assert @project.queries.empty?
435 435 assert @project.copy(@source_project)
436 436
437 437 assert_equal @source_project.queries.size, @project.queries.size
438 438 @project.queries.each do |query|
439 439 assert query
440 440 assert_equal @project, query.project
441 441 end
442 442 end
443 443
444 444 should "copy versions" do
445 445 @source_project.versions << Version.generate!
446 446 @source_project.versions << Version.generate!
447 447
448 448 assert @project.versions.empty?
449 449 assert @project.copy(@source_project)
450 450
451 451 assert_equal @source_project.versions.size, @project.versions.size
452 452 @project.versions.each do |version|
453 453 assert version
454 454 assert_equal @project, version.project
455 455 end
456 456 end
457 457
458 458 should "copy wiki" do
459 459 assert @project.copy(@source_project)
460 460
461 461 assert @project.wiki
462 462 assert_not_equal @source_project.wiki, @project.wiki
463 463 assert_equal "Start page", @project.wiki.start_page
464 464 end
465 465
466 466 should "copy wiki pages and content" do
467 467 assert @project.copy(@source_project)
468 468
469 469 assert @project.wiki
470 470 assert_equal 1, @project.wiki.pages.length
471 471
472 472 @project.wiki.pages.each do |wiki_page|
473 473 assert wiki_page.content
474 474 assert !@source_project.wiki.pages.include?(wiki_page)
475 475 end
476 476 end
477 477
478 478 should "copy custom fields"
479 479
480 480 should "copy issue categories" do
481 481 assert @project.copy(@source_project)
482 482
483 483 assert_equal 2, @project.issue_categories.size
484 484 @project.issue_categories.each do |issue_category|
485 485 assert !@source_project.issue_categories.include?(issue_category)
486 486 end
487 487 end
488 488
489 489 should "change the new issues to use the copied issue categories" do
490 490 issue = Issue.find(4)
491 491 issue.update_attribute(:category_id, 3)
492 492
493 493 assert @project.copy(@source_project)
494 494
495 495 @project.issues.each do |issue|
496 496 assert issue.category
497 497 assert_equal "Stock management", issue.category.name # Same name
498 498 assert_not_equal IssueCategory.find(3), issue.category # Different record
499 499 end
500 500 end
501 501
502 should "limit copy with :only option" do
503 assert @project.members.empty?
504 assert @project.issue_categories.empty?
505 assert @source_project.issues.any?
506
507 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
508
509 assert @project.members.any?
510 assert @project.issue_categories.any?
511 assert @project.issues.empty?
512 end
513
502 514 should "copy issue relations"
503 515 should "link issue relations if cross project issue relations are valid"
504 516
505 517 end
506 518
507 519 end
General Comments 0
You need to be logged in to leave comments. Login now