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