##// END OF EJS Templates
Show subproject versions on the Roadmap....
Eric Davis -
r3646:f3cc84b3437a
parent child
Show More
@@ -1,387 +1,389
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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
24
25 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
25 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
26 before_filter :find_optional_project, :only => :activity
26 before_filter :find_optional_project, :only => :activity
27 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
27 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize_global, :only => :add
28 before_filter :authorize_global, :only => :add
29 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 if controller.request.post?
33 if controller.request.post?
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 end
35 end
36 end
36 end
37
37
38 helper :sort
38 helper :sort
39 include SortHelper
39 include SortHelper
40 helper :custom_fields
40 helper :custom_fields
41 include CustomFieldsHelper
41 include CustomFieldsHelper
42 helper :issues
42 helper :issues
43 helper :queries
43 helper :queries
44 include QueriesHelper
44 include QueriesHelper
45 helper :repositories
45 helper :repositories
46 include RepositoriesHelper
46 include RepositoriesHelper
47 include ProjectsHelper
47 include ProjectsHelper
48
48
49 # Lists visible projects
49 # Lists visible projects
50 def index
50 def index
51 respond_to do |format|
51 respond_to do |format|
52 format.html {
52 format.html {
53 @projects = Project.visible.find(:all, :order => 'lft')
53 @projects = Project.visible.find(:all, :order => 'lft')
54 }
54 }
55 format.xml {
55 format.xml {
56 @projects = Project.visible.find(:all, :order => 'lft')
56 @projects = Project.visible.find(:all, :order => 'lft')
57 }
57 }
58 format.atom {
58 format.atom {
59 projects = Project.visible.find(:all, :order => 'created_on DESC',
59 projects = Project.visible.find(:all, :order => 'created_on DESC',
60 :limit => Setting.feeds_limit.to_i)
60 :limit => Setting.feeds_limit.to_i)
61 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
62 }
62 }
63 end
63 end
64 end
64 end
65
65
66 # Add a new project
66 # Add a new project
67 def add
67 def add
68 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
68 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
69 @trackers = Tracker.all
69 @trackers = Tracker.all
70 @project = Project.new(params[:project])
70 @project = Project.new(params[:project])
71 if request.get?
71 if request.get?
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 @project.trackers = Tracker.all
73 @project.trackers = Tracker.all
74 @project.is_public = Setting.default_projects_public?
74 @project.is_public = Setting.default_projects_public?
75 @project.enabled_module_names = Setting.default_projects_modules
75 @project.enabled_module_names = Setting.default_projects_modules
76 else
76 else
77 @project.enabled_module_names = params[:enabled_modules]
77 @project.enabled_module_names = params[:enabled_modules]
78 if validate_parent_id && @project.save
78 if validate_parent_id && @project.save
79 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
79 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
80 # Add current user as a project member if he is not admin
80 # Add current user as a project member if he is not admin
81 unless User.current.admin?
81 unless User.current.admin?
82 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
82 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
83 m = Member.new(:user => User.current, :roles => [r])
83 m = Member.new(:user => User.current, :roles => [r])
84 @project.members << m
84 @project.members << m
85 end
85 end
86 respond_to do |format|
86 respond_to do |format|
87 format.html {
87 format.html {
88 flash[:notice] = l(:notice_successful_create)
88 flash[:notice] = l(:notice_successful_create)
89 redirect_to :controller => 'projects', :action => 'settings', :id => @project
89 redirect_to :controller => 'projects', :action => 'settings', :id => @project
90 }
90 }
91 format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
91 format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 end
92 end
93 else
93 else
94 respond_to do |format|
94 respond_to do |format|
95 format.html
95 format.html
96 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
96 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
97 end
97 end
98 end
98 end
99 end
99 end
100 end
100 end
101
101
102 def copy
102 def copy
103 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
103 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
104 @trackers = Tracker.all
104 @trackers = Tracker.all
105 @root_projects = Project.find(:all,
105 @root_projects = Project.find(:all,
106 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
106 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
107 :order => 'name')
107 :order => 'name')
108 @source_project = Project.find(params[:id])
108 @source_project = Project.find(params[:id])
109 if request.get?
109 if request.get?
110 @project = Project.copy_from(@source_project)
110 @project = Project.copy_from(@source_project)
111 if @project
111 if @project
112 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
112 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
113 else
113 else
114 redirect_to :controller => 'admin', :action => 'projects'
114 redirect_to :controller => 'admin', :action => 'projects'
115 end
115 end
116 else
116 else
117 Mailer.with_deliveries(params[:notifications] == '1') do
117 Mailer.with_deliveries(params[:notifications] == '1') do
118 @project = Project.new(params[:project])
118 @project = Project.new(params[:project])
119 @project.enabled_module_names = params[:enabled_modules]
119 @project.enabled_module_names = params[:enabled_modules]
120 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
120 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
121 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
121 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 flash[:notice] = l(:notice_successful_create)
122 flash[:notice] = l(:notice_successful_create)
123 redirect_to :controller => 'admin', :action => 'projects'
123 redirect_to :controller => 'admin', :action => 'projects'
124 elsif !@project.new_record?
124 elsif !@project.new_record?
125 # Project was created
125 # Project was created
126 # But some objects were not copied due to validation failures
126 # But some objects were not copied due to validation failures
127 # (eg. issues from disabled trackers)
127 # (eg. issues from disabled trackers)
128 # TODO: inform about that
128 # TODO: inform about that
129 redirect_to :controller => 'admin', :action => 'projects'
129 redirect_to :controller => 'admin', :action => 'projects'
130 end
130 end
131 end
131 end
132 end
132 end
133 rescue ActiveRecord::RecordNotFound
133 rescue ActiveRecord::RecordNotFound
134 redirect_to :controller => 'admin', :action => 'projects'
134 redirect_to :controller => 'admin', :action => 'projects'
135 end
135 end
136
136
137 # Show @project
137 # Show @project
138 def show
138 def show
139 if params[:jump]
139 if params[:jump]
140 # try to redirect to the requested menu item
140 # try to redirect to the requested menu item
141 redirect_to_project_menu_item(@project, params[:jump]) && return
141 redirect_to_project_menu_item(@project, params[:jump]) && return
142 end
142 end
143
143
144 @users_by_role = @project.users_by_role
144 @users_by_role = @project.users_by_role
145 @subprojects = @project.children.visible
145 @subprojects = @project.children.visible
146 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
146 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
147 @trackers = @project.rolled_up_trackers
147 @trackers = @project.rolled_up_trackers
148
148
149 cond = @project.project_condition(Setting.display_subprojects_issues?)
149 cond = @project.project_condition(Setting.display_subprojects_issues?)
150
150
151 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
151 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
152 :include => [:project, :status, :tracker],
152 :include => [:project, :status, :tracker],
153 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
153 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
154 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
154 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
155 :include => [:project, :status, :tracker],
155 :include => [:project, :status, :tracker],
156 :conditions => cond)
156 :conditions => cond)
157
157
158 TimeEntry.visible_by(User.current) do
158 TimeEntry.visible_by(User.current) do
159 @total_hours = TimeEntry.sum(:hours,
159 @total_hours = TimeEntry.sum(:hours,
160 :include => :project,
160 :include => :project,
161 :conditions => cond).to_f
161 :conditions => cond).to_f
162 end
162 end
163 @key = User.current.rss_key
163 @key = User.current.rss_key
164
164
165 respond_to do |format|
165 respond_to do |format|
166 format.html
166 format.html
167 format.xml
167 format.xml
168 end
168 end
169 end
169 end
170
170
171 def settings
171 def settings
172 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
172 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
173 @issue_category ||= IssueCategory.new
173 @issue_category ||= IssueCategory.new
174 @member ||= @project.members.new
174 @member ||= @project.members.new
175 @trackers = Tracker.all
175 @trackers = Tracker.all
176 @repository ||= @project.repository
176 @repository ||= @project.repository
177 @wiki ||= @project.wiki
177 @wiki ||= @project.wiki
178 end
178 end
179
179
180 # Edit @project
180 # Edit @project
181 def edit
181 def edit
182 if request.get?
182 if request.get?
183 else
183 else
184 @project.attributes = params[:project]
184 @project.attributes = params[:project]
185 if validate_parent_id && @project.save
185 if validate_parent_id && @project.save
186 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
186 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
187 respond_to do |format|
187 respond_to do |format|
188 format.html {
188 format.html {
189 flash[:notice] = l(:notice_successful_update)
189 flash[:notice] = l(:notice_successful_update)
190 redirect_to :action => 'settings', :id => @project
190 redirect_to :action => 'settings', :id => @project
191 }
191 }
192 format.xml { head :ok }
192 format.xml { head :ok }
193 end
193 end
194 else
194 else
195 respond_to do |format|
195 respond_to do |format|
196 format.html {
196 format.html {
197 settings
197 settings
198 render :action => 'settings'
198 render :action => 'settings'
199 }
199 }
200 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
200 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
201 end
201 end
202 end
202 end
203 end
203 end
204 end
204 end
205
205
206 def modules
206 def modules
207 @project.enabled_module_names = params[:enabled_modules]
207 @project.enabled_module_names = params[:enabled_modules]
208 flash[:notice] = l(:notice_successful_update)
208 flash[:notice] = l(:notice_successful_update)
209 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
209 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
210 end
210 end
211
211
212 def archive
212 def archive
213 if request.post?
213 if request.post?
214 unless @project.archive
214 unless @project.archive
215 flash[:error] = l(:error_can_not_archive_project)
215 flash[:error] = l(:error_can_not_archive_project)
216 end
216 end
217 end
217 end
218 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
218 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
219 end
219 end
220
220
221 def unarchive
221 def unarchive
222 @project.unarchive if request.post? && !@project.active?
222 @project.unarchive if request.post? && !@project.active?
223 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
223 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
224 end
224 end
225
225
226 # Delete @project
226 # Delete @project
227 def destroy
227 def destroy
228 @project_to_destroy = @project
228 @project_to_destroy = @project
229 if request.get?
229 if request.get?
230 # display confirmation view
230 # display confirmation view
231 else
231 else
232 if params[:format] == 'xml' || params[:confirm]
232 if params[:format] == 'xml' || params[:confirm]
233 @project_to_destroy.destroy
233 @project_to_destroy.destroy
234 respond_to do |format|
234 respond_to do |format|
235 format.html { redirect_to :controller => 'admin', :action => 'projects' }
235 format.html { redirect_to :controller => 'admin', :action => 'projects' }
236 format.xml { head :ok }
236 format.xml { head :ok }
237 end
237 end
238 end
238 end
239 end
239 end
240 # hide project in layout
240 # hide project in layout
241 @project = nil
241 @project = nil
242 end
242 end
243
243
244 def add_file
244 def add_file
245 if request.post?
245 if request.post?
246 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
246 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
247 attachments = Attachment.attach_files(container, params[:attachments])
247 attachments = Attachment.attach_files(container, params[:attachments])
248 render_attachment_warning_if_needed(container)
248 render_attachment_warning_if_needed(container)
249
249
250 if !attachments.empty? && Setting.notified_events.include?('file_added')
250 if !attachments.empty? && Setting.notified_events.include?('file_added')
251 Mailer.deliver_attachments_added(attachments[:files])
251 Mailer.deliver_attachments_added(attachments[:files])
252 end
252 end
253 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
253 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
254 return
254 return
255 end
255 end
256 @versions = @project.versions.sort
256 @versions = @project.versions.sort
257 end
257 end
258
258
259 def save_activities
259 def save_activities
260 if request.post? && params[:enumerations]
260 if request.post? && params[:enumerations]
261 Project.transaction do
261 Project.transaction do
262 params[:enumerations].each do |id, activity|
262 params[:enumerations].each do |id, activity|
263 @project.update_or_create_time_entry_activity(id, activity)
263 @project.update_or_create_time_entry_activity(id, activity)
264 end
264 end
265 end
265 end
266 flash[:notice] = l(:notice_successful_update)
266 flash[:notice] = l(:notice_successful_update)
267 end
267 end
268
268
269 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
269 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
270 end
270 end
271
271
272 def reset_activities
272 def reset_activities
273 @project.time_entry_activities.each do |time_entry_activity|
273 @project.time_entry_activities.each do |time_entry_activity|
274 time_entry_activity.destroy(time_entry_activity.parent)
274 time_entry_activity.destroy(time_entry_activity.parent)
275 end
275 end
276 flash[:notice] = l(:notice_successful_update)
276 flash[:notice] = l(:notice_successful_update)
277 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
277 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
278 end
278 end
279
279
280 def list_files
280 def list_files
281 sort_init 'filename', 'asc'
281 sort_init 'filename', 'asc'
282 sort_update 'filename' => "#{Attachment.table_name}.filename",
282 sort_update 'filename' => "#{Attachment.table_name}.filename",
283 'created_on' => "#{Attachment.table_name}.created_on",
283 'created_on' => "#{Attachment.table_name}.created_on",
284 'size' => "#{Attachment.table_name}.filesize",
284 'size' => "#{Attachment.table_name}.filesize",
285 'downloads' => "#{Attachment.table_name}.downloads"
285 'downloads' => "#{Attachment.table_name}.downloads"
286
286
287 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
287 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
288 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
288 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
289 render :layout => !request.xhr?
289 render :layout => !request.xhr?
290 end
290 end
291
291
292 def roadmap
292 def roadmap
293 @trackers = @project.trackers.find(:all, :order => 'position')
293 @trackers = @project.trackers.find(:all, :order => 'position')
294 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
294 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
295 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
295 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
296 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
296 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
297
297
298 @versions = @project.shared_versions.sort
298 @versions = @project.shared_versions || []
299 @versions += @project.rolled_up_versions.visible if @with_subprojects
300 @versions = @versions.uniq.sort
299 @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
301 @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
300
302
301 @issues_by_version = {}
303 @issues_by_version = {}
302 unless @selected_tracker_ids.empty?
304 unless @selected_tracker_ids.empty?
303 @versions.each do |version|
305 @versions.each do |version|
304 issues = version.fixed_issues.visible.find(:all,
306 issues = version.fixed_issues.visible.find(:all,
305 :include => [:project, :status, :tracker, :priority],
307 :include => [:project, :status, :tracker, :priority],
306 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
308 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
307 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
309 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
308 @issues_by_version[version] = issues
310 @issues_by_version[version] = issues
309 end
311 end
310 end
312 end
311 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
313 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
312 end
314 end
313
315
314 def activity
316 def activity
315 @days = Setting.activity_days_default.to_i
317 @days = Setting.activity_days_default.to_i
316
318
317 if params[:from]
319 if params[:from]
318 begin; @date_to = params[:from].to_date + 1; rescue; end
320 begin; @date_to = params[:from].to_date + 1; rescue; end
319 end
321 end
320
322
321 @date_to ||= Date.today + 1
323 @date_to ||= Date.today + 1
322 @date_from = @date_to - @days
324 @date_from = @date_to - @days
323 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
325 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
324 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
326 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
325
327
326 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
328 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
327 :with_subprojects => @with_subprojects,
329 :with_subprojects => @with_subprojects,
328 :author => @author)
330 :author => @author)
329 @activity.scope_select {|t| !params["show_#{t}"].nil?}
331 @activity.scope_select {|t| !params["show_#{t}"].nil?}
330 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
332 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
331
333
332 events = @activity.events(@date_from, @date_to)
334 events = @activity.events(@date_from, @date_to)
333
335
334 if events.empty? || stale?(:etag => [events.first, User.current])
336 if events.empty? || stale?(:etag => [events.first, User.current])
335 respond_to do |format|
337 respond_to do |format|
336 format.html {
338 format.html {
337 @events_by_day = events.group_by(&:event_date)
339 @events_by_day = events.group_by(&:event_date)
338 render :layout => false if request.xhr?
340 render :layout => false if request.xhr?
339 }
341 }
340 format.atom {
342 format.atom {
341 title = l(:label_activity)
343 title = l(:label_activity)
342 if @author
344 if @author
343 title = @author.name
345 title = @author.name
344 elsif @activity.scope.size == 1
346 elsif @activity.scope.size == 1
345 title = l("label_#{@activity.scope.first.singularize}_plural")
347 title = l("label_#{@activity.scope.first.singularize}_plural")
346 end
348 end
347 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
349 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
348 }
350 }
349 end
351 end
350 end
352 end
351
353
352 rescue ActiveRecord::RecordNotFound
354 rescue ActiveRecord::RecordNotFound
353 render_404
355 render_404
354 end
356 end
355
357
356 private
358 private
357 def find_optional_project
359 def find_optional_project
358 return true unless params[:id]
360 return true unless params[:id]
359 @project = Project.find(params[:id])
361 @project = Project.find(params[:id])
360 authorize
362 authorize
361 rescue ActiveRecord::RecordNotFound
363 rescue ActiveRecord::RecordNotFound
362 render_404
364 render_404
363 end
365 end
364
366
365 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
367 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
366 if ids = params[:tracker_ids]
368 if ids = params[:tracker_ids]
367 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
369 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
368 else
370 else
369 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
371 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
370 end
372 end
371 end
373 end
372
374
373 # Validates parent_id param according to user's permissions
375 # Validates parent_id param according to user's permissions
374 # TODO: move it to Project model in a validation that depends on User.current
376 # TODO: move it to Project model in a validation that depends on User.current
375 def validate_parent_id
377 def validate_parent_id
376 return true if User.current.admin?
378 return true if User.current.admin?
377 parent_id = params[:project] && params[:project][:parent_id]
379 parent_id = params[:project] && params[:project][:parent_id]
378 if parent_id || @project.new_record?
380 if parent_id || @project.new_record?
379 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
381 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
380 unless @project.allowed_parents.include?(parent)
382 unless @project.allowed_parents.include?(parent)
381 @project.errors.add :parent_id, :invalid
383 @project.errors.add :parent_id, :invalid
382 return false
384 return false
383 end
385 end
384 end
386 end
385 true
387 true
386 end
388 end
387 end
389 end
@@ -1,706 +1,713
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
24 has_many :time_entry_activities
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 has_many :memberships, :class_name => 'Member'
26 has_many :memberships, :class_name => 'Member'
27 has_many :member_principals, :class_name => 'Member',
27 has_many :member_principals, :class_name => 'Member',
28 :include => :principal,
28 :include => :principal,
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 has_many :users, :through => :members
30 has_many :users, :through => :members
31 has_many :principals, :through => :member_principals, :source => :principal
31 has_many :principals, :through => :member_principals, :source => :principal
32
32
33 has_many :enabled_modules, :dependent => :delete_all
33 has_many :enabled_modules, :dependent => :delete_all
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 has_many :issue_changes, :through => :issues, :source => :journals
36 has_many :issue_changes, :through => :issues, :source => :journals
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 has_many :time_entries, :dependent => :delete_all
38 has_many :time_entries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
40 has_many :documents, :dependent => :destroy
40 has_many :documents, :dependent => :destroy
41 has_many :news, :dependent => :delete_all, :include => :author
41 has_many :news, :dependent => :delete_all, :include => :author
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 has_one :repository, :dependent => :destroy
44 has_one :repository, :dependent => :destroy
45 has_many :changesets, :through => :repository
45 has_many :changesets, :through => :repository
46 has_one :wiki, :dependent => :destroy
46 has_one :wiki, :dependent => :destroy
47 # Custom field for the project issues
47 # Custom field for the project issues
48 has_and_belongs_to_many :issue_custom_fields,
48 has_and_belongs_to_many :issue_custom_fields,
49 :class_name => 'IssueCustomField',
49 :class_name => 'IssueCustomField',
50 :order => "#{CustomField.table_name}.position",
50 :order => "#{CustomField.table_name}.position",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 :association_foreign_key => 'custom_field_id'
52 :association_foreign_key => 'custom_field_id'
53
53
54 acts_as_nested_set :order => 'name'
54 acts_as_nested_set :order => 'name'
55 acts_as_attachable :view_permission => :view_files,
55 acts_as_attachable :view_permission => :view_files,
56 :delete_permission => :manage_files
56 :delete_permission => :manage_files
57
57
58 acts_as_customizable
58 acts_as_customizable
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
62 :author => nil
62 :author => nil
63
63
64 attr_protected :status, :enabled_module_names
64 attr_protected :status, :enabled_module_names
65
65
66 validates_presence_of :name, :identifier
66 validates_presence_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
68 validates_associated :repository, :wiki
68 validates_associated :repository, :wiki
69 validates_length_of :name, :maximum => 30
69 validates_length_of :name, :maximum => 30
70 validates_length_of :homepage, :maximum => 255
70 validates_length_of :homepage, :maximum => 255
71 validates_length_of :identifier, :in => 1..20
71 validates_length_of :identifier, :in => 1..20
72 # donwcase letters, digits, dashes but not digits only
72 # donwcase letters, digits, dashes but not digits only
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 # reserved words
74 # reserved words
75 validates_exclusion_of :identifier, :in => %w( new )
75 validates_exclusion_of :identifier, :in => %w( new )
76
76
77 before_destroy :delete_all_members, :destroy_children
77 before_destroy :delete_all_members, :destroy_children
78
78
79 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] } }
79 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] } }
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 named_scope :all_public, { :conditions => { :is_public => true } }
81 named_scope :all_public, { :conditions => { :is_public => true } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83
83
84 def identifier=(identifier)
84 def identifier=(identifier)
85 super unless identifier_frozen?
85 super unless identifier_frozen?
86 end
86 end
87
87
88 def identifier_frozen?
88 def identifier_frozen?
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 end
90 end
91
91
92 # returns latest created projects
92 # returns latest created projects
93 # non public projects will be returned only if user is a member of those
93 # non public projects will be returned only if user is a member of those
94 def self.latest(user=nil, count=5)
94 def self.latest(user=nil, count=5)
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 end
96 end
97
97
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 #
99 #
100 # Examples:
100 # Examples:
101 # Projects.visible_by(admin) => "projects.status = 1"
101 # Projects.visible_by(admin) => "projects.status = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 def self.visible_by(user=nil)
103 def self.visible_by(user=nil)
104 user ||= User.current
104 user ||= User.current
105 if user && user.admin?
105 if user && user.admin?
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 elsif user && user.memberships.any?
107 elsif user && user.memberships.any?
108 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(',')}))"
108 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(',')}))"
109 else
109 else
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 end
111 end
112 end
112 end
113
113
114 def self.allowed_to_condition(user, permission, options={})
114 def self.allowed_to_condition(user, permission, options={})
115 statements = []
115 statements = []
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 if perm = Redmine::AccessControl.permission(permission)
117 if perm = Redmine::AccessControl.permission(permission)
118 unless perm.project_module.nil?
118 unless perm.project_module.nil?
119 # If the permission belongs to a project module, make sure the module is enabled
119 # If the permission belongs to a project module, make sure the module is enabled
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 end
121 end
122 end
122 end
123 if options[:project]
123 if options[:project]
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 end
127 end
128 if user.admin?
128 if user.admin?
129 # no restriction
129 # no restriction
130 else
130 else
131 statements << "1=0"
131 statements << "1=0"
132 if user.logged?
132 if user.logged?
133 if Role.non_member.allowed_to?(permission) && !options[:member]
133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 end
135 end
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 else
138 else
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 # anonymous user allowed on public project
140 # anonymous user allowed on public project
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 end
142 end
143 end
143 end
144 end
144 end
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 end
146 end
147
147
148 # Returns the Systemwide and project specific activities
148 # Returns the Systemwide and project specific activities
149 def activities(include_inactive=false)
149 def activities(include_inactive=false)
150 if include_inactive
150 if include_inactive
151 return all_activities
151 return all_activities
152 else
152 else
153 return active_activities
153 return active_activities
154 end
154 end
155 end
155 end
156
156
157 # Will create a new Project specific Activity or update an existing one
157 # Will create a new Project specific Activity or update an existing one
158 #
158 #
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 # does not successfully save.
160 # does not successfully save.
161 def update_or_create_time_entry_activity(id, activity_hash)
161 def update_or_create_time_entry_activity(id, activity_hash)
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 self.create_time_entry_activity_if_needed(activity_hash)
163 self.create_time_entry_activity_if_needed(activity_hash)
164 else
164 else
165 activity = project.time_entry_activities.find_by_id(id.to_i)
165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 activity.update_attributes(activity_hash) if activity
166 activity.update_attributes(activity_hash) if activity
167 end
167 end
168 end
168 end
169
169
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 #
171 #
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 # does not successfully save.
173 # does not successfully save.
174 def create_time_entry_activity_if_needed(activity)
174 def create_time_entry_activity_if_needed(activity)
175 if activity['parent_id']
175 if activity['parent_id']
176
176
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 activity['name'] = parent_activity.name
178 activity['name'] = parent_activity.name
179 activity['position'] = parent_activity.position
179 activity['position'] = parent_activity.position
180
180
181 if Enumeration.overridding_change?(activity, parent_activity)
181 if Enumeration.overridding_change?(activity, parent_activity)
182 project_activity = self.time_entry_activities.create(activity)
182 project_activity = self.time_entry_activities.create(activity)
183
183
184 if project_activity.new_record?
184 if project_activity.new_record?
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 else
186 else
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 end
188 end
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 #
194 #
195 # Examples:
195 # Examples:
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 # project.project_condition(false) => "projects.id = 1"
197 # project.project_condition(false) => "projects.id = 1"
198 def project_condition(with_subprojects)
198 def project_condition(with_subprojects)
199 cond = "#{Project.table_name}.id = #{id}"
199 cond = "#{Project.table_name}.id = #{id}"
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 cond
201 cond
202 end
202 end
203
203
204 def self.find(*args)
204 def self.find(*args)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 project = find_by_identifier(*args)
206 project = find_by_identifier(*args)
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 project
208 project
209 else
209 else
210 super
210 super
211 end
211 end
212 end
212 end
213
213
214 def to_param
214 def to_param
215 # id is used for projects with a numeric identifier (compatibility)
215 # id is used for projects with a numeric identifier (compatibility)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 end
217 end
218
218
219 def active?
219 def active?
220 self.status == STATUS_ACTIVE
220 self.status == STATUS_ACTIVE
221 end
221 end
222
222
223 # Archives the project and its descendants
223 # Archives the project and its descendants
224 def archive
224 def archive
225 # Check that there is no issue of a non descendant project that is assigned
225 # Check that there is no issue of a non descendant project that is assigned
226 # to one of the project or descendant versions
226 # to one of the project or descendant versions
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 if v_ids.any? && Issue.find(:first, :include => :project,
228 if v_ids.any? && Issue.find(:first, :include => :project,
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 return false
231 return false
232 end
232 end
233 Project.transaction do
233 Project.transaction do
234 archive!
234 archive!
235 end
235 end
236 true
236 true
237 end
237 end
238
238
239 # Unarchives the project
239 # Unarchives the project
240 # All its ancestors must be active
240 # All its ancestors must be active
241 def unarchive
241 def unarchive
242 return false if ancestors.detect {|a| !a.active?}
242 return false if ancestors.detect {|a| !a.active?}
243 update_attribute :status, STATUS_ACTIVE
243 update_attribute :status, STATUS_ACTIVE
244 end
244 end
245
245
246 # Returns an array of projects the project can be moved to
246 # Returns an array of projects the project can be moved to
247 # by the current user
247 # by the current user
248 def allowed_parents
248 def allowed_parents
249 return @allowed_parents if @allowed_parents
249 return @allowed_parents if @allowed_parents
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 @allowed_parents = @allowed_parents - self_and_descendants
251 @allowed_parents = @allowed_parents - self_and_descendants
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 @allowed_parents << nil
253 @allowed_parents << nil
254 end
254 end
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 @allowed_parents << parent
256 @allowed_parents << parent
257 end
257 end
258 @allowed_parents
258 @allowed_parents
259 end
259 end
260
260
261 # Sets the parent of the project with authorization check
261 # Sets the parent of the project with authorization check
262 def set_allowed_parent!(p)
262 def set_allowed_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.nil?
271 if p.nil?
272 if !new_record? && allowed_parents.empty?
272 if !new_record? && allowed_parents.empty?
273 return false
273 return false
274 end
274 end
275 elsif !allowed_parents.include?(p)
275 elsif !allowed_parents.include?(p)
276 return false
276 return false
277 end
277 end
278 set_parent!(p)
278 set_parent!(p)
279 end
279 end
280
280
281 # Sets the parent of the project
281 # Sets the parent of the project
282 # Argument can be either a Project, a String, a Fixnum or nil
282 # Argument can be either a Project, a String, a Fixnum or nil
283 def set_parent!(p)
283 def set_parent!(p)
284 unless p.nil? || p.is_a?(Project)
284 unless p.nil? || p.is_a?(Project)
285 if p.to_s.blank?
285 if p.to_s.blank?
286 p = nil
286 p = nil
287 else
287 else
288 p = Project.find_by_id(p)
288 p = Project.find_by_id(p)
289 return false unless p
289 return false unless p
290 end
290 end
291 end
291 end
292 if p == parent && !p.nil?
292 if p == parent && !p.nil?
293 # Nothing to do
293 # Nothing to do
294 true
294 true
295 elsif p.nil? || (p.active? && move_possible?(p))
295 elsif p.nil? || (p.active? && move_possible?(p))
296 # Insert the project so that target's children or root projects stay alphabetically sorted
296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 sibs = (p.nil? ? self.class.roots : p.children)
297 sibs = (p.nil? ? self.class.roots : p.children)
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 if to_be_inserted_before
299 if to_be_inserted_before
300 move_to_left_of(to_be_inserted_before)
300 move_to_left_of(to_be_inserted_before)
301 elsif p.nil?
301 elsif p.nil?
302 if sibs.empty?
302 if sibs.empty?
303 # move_to_root adds the project in first (ie. left) position
303 # move_to_root adds the project in first (ie. left) position
304 move_to_root
304 move_to_root
305 else
305 else
306 move_to_right_of(sibs.last) unless self == sibs.last
306 move_to_right_of(sibs.last) unless self == sibs.last
307 end
307 end
308 else
308 else
309 # move_to_child_of adds the project in last (ie.right) position
309 # move_to_child_of adds the project in last (ie.right) position
310 move_to_child_of(p)
310 move_to_child_of(p)
311 end
311 end
312 Issue.update_versions_from_hierarchy_change(self)
312 Issue.update_versions_from_hierarchy_change(self)
313 true
313 true
314 else
314 else
315 # Can not move to the given target
315 # Can not move to the given target
316 false
316 false
317 end
317 end
318 end
318 end
319
319
320 # Returns an array of the trackers used by the project and its active sub projects
320 # Returns an array of the trackers used by the project and its active sub projects
321 def rolled_up_trackers
321 def rolled_up_trackers
322 @rolled_up_trackers ||=
322 @rolled_up_trackers ||=
323 Tracker.find(:all, :include => :projects,
323 Tracker.find(:all, :include => :projects,
324 :select => "DISTINCT #{Tracker.table_name}.*",
324 :select => "DISTINCT #{Tracker.table_name}.*",
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 :order => "#{Tracker.table_name}.position")
326 :order => "#{Tracker.table_name}.position")
327 end
327 end
328
328
329 # Closes open and locked project versions that are completed
329 # Closes open and locked project versions that are completed
330 def close_completed_versions
330 def close_completed_versions
331 Version.transaction do
331 Version.transaction do
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 if version.completed?
333 if version.completed?
334 version.update_attribute(:status, 'closed')
334 version.update_attribute(:status, 'closed')
335 end
335 end
336 end
336 end
337 end
337 end
338 end
338 end
339
340 # Returns a scope of the Versions on subprojects
341 def rolled_up_versions
342 @rolled_up_versions ||=
343 Version.scoped(:include => :project,
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
345 end
339
346
340 # Returns a scope of the Versions used by the project
347 # Returns a scope of the Versions used by the project
341 def shared_versions
348 def shared_versions
342 @shared_versions ||=
349 @shared_versions ||=
343 Version.scoped(:include => :project,
350 Version.scoped(:include => :project,
344 :conditions => "#{Project.table_name}.id = #{id}" +
351 :conditions => "#{Project.table_name}.id = #{id}" +
345 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
346 " #{Version.table_name}.sharing = 'system'" +
353 " #{Version.table_name}.sharing = 'system'" +
347 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
348 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
349 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
350 "))")
357 "))")
351 end
358 end
352
359
353 # Returns a hash of project users grouped by role
360 # Returns a hash of project users grouped by role
354 def users_by_role
361 def users_by_role
355 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
356 m.roles.each do |r|
363 m.roles.each do |r|
357 h[r] ||= []
364 h[r] ||= []
358 h[r] << m.user
365 h[r] << m.user
359 end
366 end
360 h
367 h
361 end
368 end
362 end
369 end
363
370
364 # Deletes all project's members
371 # Deletes all project's members
365 def delete_all_members
372 def delete_all_members
366 me, mr = Member.table_name, MemberRole.table_name
373 me, mr = Member.table_name, MemberRole.table_name
367 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
368 Member.delete_all(['project_id = ?', id])
375 Member.delete_all(['project_id = ?', id])
369 end
376 end
370
377
371 # Users issues can be assigned to
378 # Users issues can be assigned to
372 def assignable_users
379 def assignable_users
373 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
374 end
381 end
375
382
376 # Returns the mail adresses of users that should be always notified on project events
383 # Returns the mail adresses of users that should be always notified on project events
377 def recipients
384 def recipients
378 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
385 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
379 end
386 end
380
387
381 # Returns the users that should be notified on project events
388 # Returns the users that should be notified on project events
382 def notified_users
389 def notified_users
383 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
390 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
384 end
391 end
385
392
386 # Returns an array of all custom fields enabled for project issues
393 # Returns an array of all custom fields enabled for project issues
387 # (explictly associated custom fields and custom fields enabled for all projects)
394 # (explictly associated custom fields and custom fields enabled for all projects)
388 def all_issue_custom_fields
395 def all_issue_custom_fields
389 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
396 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
390 end
397 end
391
398
392 def project
399 def project
393 self
400 self
394 end
401 end
395
402
396 def <=>(project)
403 def <=>(project)
397 name.downcase <=> project.name.downcase
404 name.downcase <=> project.name.downcase
398 end
405 end
399
406
400 def to_s
407 def to_s
401 name
408 name
402 end
409 end
403
410
404 # Returns a short description of the projects (first lines)
411 # Returns a short description of the projects (first lines)
405 def short_description(length = 255)
412 def short_description(length = 255)
406 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
413 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
407 end
414 end
408
415
409 # Return true if this project is allowed to do the specified action.
416 # Return true if this project is allowed to do the specified action.
410 # action can be:
417 # action can be:
411 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
418 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
412 # * a permission Symbol (eg. :edit_project)
419 # * a permission Symbol (eg. :edit_project)
413 def allows_to?(action)
420 def allows_to?(action)
414 if action.is_a? Hash
421 if action.is_a? Hash
415 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
422 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
416 else
423 else
417 allowed_permissions.include? action
424 allowed_permissions.include? action
418 end
425 end
419 end
426 end
420
427
421 def module_enabled?(module_name)
428 def module_enabled?(module_name)
422 module_name = module_name.to_s
429 module_name = module_name.to_s
423 enabled_modules.detect {|m| m.name == module_name}
430 enabled_modules.detect {|m| m.name == module_name}
424 end
431 end
425
432
426 def enabled_module_names=(module_names)
433 def enabled_module_names=(module_names)
427 if module_names && module_names.is_a?(Array)
434 if module_names && module_names.is_a?(Array)
428 module_names = module_names.collect(&:to_s)
435 module_names = module_names.collect(&:to_s)
429 # remove disabled modules
436 # remove disabled modules
430 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
437 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
431 # add new modules
438 # add new modules
432 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
439 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
433 else
440 else
434 enabled_modules.clear
441 enabled_modules.clear
435 end
442 end
436 end
443 end
437
444
438 # Returns an auto-generated project identifier based on the last identifier used
445 # Returns an auto-generated project identifier based on the last identifier used
439 def self.next_identifier
446 def self.next_identifier
440 p = Project.find(:first, :order => 'created_on DESC')
447 p = Project.find(:first, :order => 'created_on DESC')
441 p.nil? ? nil : p.identifier.to_s.succ
448 p.nil? ? nil : p.identifier.to_s.succ
442 end
449 end
443
450
444 # Copies and saves the Project instance based on the +project+.
451 # Copies and saves the Project instance based on the +project+.
445 # Duplicates the source project's:
452 # Duplicates the source project's:
446 # * Wiki
453 # * Wiki
447 # * Versions
454 # * Versions
448 # * Categories
455 # * Categories
449 # * Issues
456 # * Issues
450 # * Members
457 # * Members
451 # * Queries
458 # * Queries
452 #
459 #
453 # Accepts an +options+ argument to specify what to copy
460 # Accepts an +options+ argument to specify what to copy
454 #
461 #
455 # Examples:
462 # Examples:
456 # project.copy(1) # => copies everything
463 # project.copy(1) # => copies everything
457 # project.copy(1, :only => 'members') # => copies members only
464 # project.copy(1, :only => 'members') # => copies members only
458 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
465 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
459 def copy(project, options={})
466 def copy(project, options={})
460 project = project.is_a?(Project) ? project : Project.find(project)
467 project = project.is_a?(Project) ? project : Project.find(project)
461
468
462 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
469 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
463 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
470 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
464
471
465 Project.transaction do
472 Project.transaction do
466 if save
473 if save
467 reload
474 reload
468 to_be_copied.each do |name|
475 to_be_copied.each do |name|
469 send "copy_#{name}", project
476 send "copy_#{name}", project
470 end
477 end
471 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
478 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
472 save
479 save
473 end
480 end
474 end
481 end
475 end
482 end
476
483
477
484
478 # Copies +project+ and returns the new instance. This will not save
485 # Copies +project+ and returns the new instance. This will not save
479 # the copy
486 # the copy
480 def self.copy_from(project)
487 def self.copy_from(project)
481 begin
488 begin
482 project = project.is_a?(Project) ? project : Project.find(project)
489 project = project.is_a?(Project) ? project : Project.find(project)
483 if project
490 if project
484 # clear unique attributes
491 # clear unique attributes
485 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
492 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
486 copy = Project.new(attributes)
493 copy = Project.new(attributes)
487 copy.enabled_modules = project.enabled_modules
494 copy.enabled_modules = project.enabled_modules
488 copy.trackers = project.trackers
495 copy.trackers = project.trackers
489 copy.custom_values = project.custom_values.collect {|v| v.clone}
496 copy.custom_values = project.custom_values.collect {|v| v.clone}
490 copy.issue_custom_fields = project.issue_custom_fields
497 copy.issue_custom_fields = project.issue_custom_fields
491 return copy
498 return copy
492 else
499 else
493 return nil
500 return nil
494 end
501 end
495 rescue ActiveRecord::RecordNotFound
502 rescue ActiveRecord::RecordNotFound
496 return nil
503 return nil
497 end
504 end
498 end
505 end
499
506
500 private
507 private
501
508
502 # Destroys children before destroying self
509 # Destroys children before destroying self
503 def destroy_children
510 def destroy_children
504 children.each do |child|
511 children.each do |child|
505 child.destroy
512 child.destroy
506 end
513 end
507 end
514 end
508
515
509 # Copies wiki from +project+
516 # Copies wiki from +project+
510 def copy_wiki(project)
517 def copy_wiki(project)
511 # Check that the source project has a wiki first
518 # Check that the source project has a wiki first
512 unless project.wiki.nil?
519 unless project.wiki.nil?
513 self.wiki ||= Wiki.new
520 self.wiki ||= Wiki.new
514 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
521 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
515 wiki_pages_map = {}
522 wiki_pages_map = {}
516 project.wiki.pages.each do |page|
523 project.wiki.pages.each do |page|
517 # Skip pages without content
524 # Skip pages without content
518 next if page.content.nil?
525 next if page.content.nil?
519 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
526 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
520 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
527 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
521 new_wiki_page.content = new_wiki_content
528 new_wiki_page.content = new_wiki_content
522 wiki.pages << new_wiki_page
529 wiki.pages << new_wiki_page
523 wiki_pages_map[page.id] = new_wiki_page
530 wiki_pages_map[page.id] = new_wiki_page
524 end
531 end
525 wiki.save
532 wiki.save
526 # Reproduce page hierarchy
533 # Reproduce page hierarchy
527 project.wiki.pages.each do |page|
534 project.wiki.pages.each do |page|
528 if page.parent_id && wiki_pages_map[page.id]
535 if page.parent_id && wiki_pages_map[page.id]
529 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
536 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
530 wiki_pages_map[page.id].save
537 wiki_pages_map[page.id].save
531 end
538 end
532 end
539 end
533 end
540 end
534 end
541 end
535
542
536 # Copies versions from +project+
543 # Copies versions from +project+
537 def copy_versions(project)
544 def copy_versions(project)
538 project.versions.each do |version|
545 project.versions.each do |version|
539 new_version = Version.new
546 new_version = Version.new
540 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
547 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
541 self.versions << new_version
548 self.versions << new_version
542 end
549 end
543 end
550 end
544
551
545 # Copies issue categories from +project+
552 # Copies issue categories from +project+
546 def copy_issue_categories(project)
553 def copy_issue_categories(project)
547 project.issue_categories.each do |issue_category|
554 project.issue_categories.each do |issue_category|
548 new_issue_category = IssueCategory.new
555 new_issue_category = IssueCategory.new
549 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
556 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
550 self.issue_categories << new_issue_category
557 self.issue_categories << new_issue_category
551 end
558 end
552 end
559 end
553
560
554 # Copies issues from +project+
561 # Copies issues from +project+
555 def copy_issues(project)
562 def copy_issues(project)
556 # Stores the source issue id as a key and the copied issues as the
563 # Stores the source issue id as a key and the copied issues as the
557 # value. Used to map the two togeather for issue relations.
564 # value. Used to map the two togeather for issue relations.
558 issues_map = {}
565 issues_map = {}
559
566
560 # Get issues sorted by root_id, lft so that parent issues
567 # Get issues sorted by root_id, lft so that parent issues
561 # get copied before their children
568 # get copied before their children
562 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
569 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
563 new_issue = Issue.new
570 new_issue = Issue.new
564 new_issue.copy_from(issue)
571 new_issue.copy_from(issue)
565 new_issue.project = self
572 new_issue.project = self
566 # Reassign fixed_versions by name, since names are unique per
573 # Reassign fixed_versions by name, since names are unique per
567 # project and the versions for self are not yet saved
574 # project and the versions for self are not yet saved
568 if issue.fixed_version
575 if issue.fixed_version
569 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
576 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
570 end
577 end
571 # Reassign the category by name, since names are unique per
578 # Reassign the category by name, since names are unique per
572 # project and the categories for self are not yet saved
579 # project and the categories for self are not yet saved
573 if issue.category
580 if issue.category
574 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
581 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
575 end
582 end
576 # Parent issue
583 # Parent issue
577 if issue.parent_id
584 if issue.parent_id
578 if copied_parent = issues_map[issue.parent_id]
585 if copied_parent = issues_map[issue.parent_id]
579 new_issue.parent_issue_id = copied_parent.id
586 new_issue.parent_issue_id = copied_parent.id
580 end
587 end
581 end
588 end
582
589
583 self.issues << new_issue
590 self.issues << new_issue
584 issues_map[issue.id] = new_issue
591 issues_map[issue.id] = new_issue
585 end
592 end
586
593
587 # Relations after in case issues related each other
594 # Relations after in case issues related each other
588 project.issues.each do |issue|
595 project.issues.each do |issue|
589 new_issue = issues_map[issue.id]
596 new_issue = issues_map[issue.id]
590
597
591 # Relations
598 # Relations
592 issue.relations_from.each do |source_relation|
599 issue.relations_from.each do |source_relation|
593 new_issue_relation = IssueRelation.new
600 new_issue_relation = IssueRelation.new
594 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
601 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
595 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
602 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
596 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
603 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
597 new_issue_relation.issue_to = source_relation.issue_to
604 new_issue_relation.issue_to = source_relation.issue_to
598 end
605 end
599 new_issue.relations_from << new_issue_relation
606 new_issue.relations_from << new_issue_relation
600 end
607 end
601
608
602 issue.relations_to.each do |source_relation|
609 issue.relations_to.each do |source_relation|
603 new_issue_relation = IssueRelation.new
610 new_issue_relation = IssueRelation.new
604 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
611 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
605 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
612 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
606 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
613 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
607 new_issue_relation.issue_from = source_relation.issue_from
614 new_issue_relation.issue_from = source_relation.issue_from
608 end
615 end
609 new_issue.relations_to << new_issue_relation
616 new_issue.relations_to << new_issue_relation
610 end
617 end
611 end
618 end
612 end
619 end
613
620
614 # Copies members from +project+
621 # Copies members from +project+
615 def copy_members(project)
622 def copy_members(project)
616 project.memberships.each do |member|
623 project.memberships.each do |member|
617 new_member = Member.new
624 new_member = Member.new
618 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
625 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
619 # only copy non inherited roles
626 # only copy non inherited roles
620 # inherited roles will be added when copying the group membership
627 # inherited roles will be added when copying the group membership
621 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
628 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
622 next if role_ids.empty?
629 next if role_ids.empty?
623 new_member.role_ids = role_ids
630 new_member.role_ids = role_ids
624 new_member.project = self
631 new_member.project = self
625 self.members << new_member
632 self.members << new_member
626 end
633 end
627 end
634 end
628
635
629 # Copies queries from +project+
636 # Copies queries from +project+
630 def copy_queries(project)
637 def copy_queries(project)
631 project.queries.each do |query|
638 project.queries.each do |query|
632 new_query = Query.new
639 new_query = Query.new
633 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
640 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
634 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
641 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
635 new_query.project = self
642 new_query.project = self
636 self.queries << new_query
643 self.queries << new_query
637 end
644 end
638 end
645 end
639
646
640 # Copies boards from +project+
647 # Copies boards from +project+
641 def copy_boards(project)
648 def copy_boards(project)
642 project.boards.each do |board|
649 project.boards.each do |board|
643 new_board = Board.new
650 new_board = Board.new
644 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
651 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
645 new_board.project = self
652 new_board.project = self
646 self.boards << new_board
653 self.boards << new_board
647 end
654 end
648 end
655 end
649
656
650 def allowed_permissions
657 def allowed_permissions
651 @allowed_permissions ||= begin
658 @allowed_permissions ||= begin
652 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
659 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
653 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
660 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
654 end
661 end
655 end
662 end
656
663
657 def allowed_actions
664 def allowed_actions
658 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
665 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
659 end
666 end
660
667
661 # Returns all the active Systemwide and project specific activities
668 # Returns all the active Systemwide and project specific activities
662 def active_activities
669 def active_activities
663 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
670 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
664
671
665 if overridden_activity_ids.empty?
672 if overridden_activity_ids.empty?
666 return TimeEntryActivity.shared.active
673 return TimeEntryActivity.shared.active
667 else
674 else
668 return system_activities_and_project_overrides
675 return system_activities_and_project_overrides
669 end
676 end
670 end
677 end
671
678
672 # Returns all the Systemwide and project specific activities
679 # Returns all the Systemwide and project specific activities
673 # (inactive and active)
680 # (inactive and active)
674 def all_activities
681 def all_activities
675 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
682 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
676
683
677 if overridden_activity_ids.empty?
684 if overridden_activity_ids.empty?
678 return TimeEntryActivity.shared
685 return TimeEntryActivity.shared
679 else
686 else
680 return system_activities_and_project_overrides(true)
687 return system_activities_and_project_overrides(true)
681 end
688 end
682 end
689 end
683
690
684 # Returns the systemwide active activities merged with the project specific overrides
691 # Returns the systemwide active activities merged with the project specific overrides
685 def system_activities_and_project_overrides(include_inactive=false)
692 def system_activities_and_project_overrides(include_inactive=false)
686 if include_inactive
693 if include_inactive
687 return TimeEntryActivity.shared.
694 return TimeEntryActivity.shared.
688 find(:all,
695 find(:all,
689 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
696 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
690 self.time_entry_activities
697 self.time_entry_activities
691 else
698 else
692 return TimeEntryActivity.shared.active.
699 return TimeEntryActivity.shared.active.
693 find(:all,
700 find(:all,
694 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
701 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
695 self.time_entry_activities.active
702 self.time_entry_activities.active
696 end
703 end
697 end
704 end
698
705
699 # Archives subprojects recursively
706 # Archives subprojects recursively
700 def archive!
707 def archive!
701 children.each do |subproject|
708 children.each do |subproject|
702 subproject.send :archive!
709 subproject.send :archive!
703 end
710 end
704 update_attribute :status, STATUS_ARCHIVED
711 update_attribute :status, STATUS_ARCHIVED
705 end
712 end
706 end
713 end
@@ -1,715 +1,717
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < ActionController::TestCase
24 class ProjectsControllerTest < ActionController::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments, :custom_fields, :custom_values, :time_entries
27 :attachments, :custom_fields, :custom_values, :time_entries
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index
37 def test_index
38 get :index
38 get :index
39 assert_response :success
39 assert_response :success
40 assert_template 'index'
40 assert_template 'index'
41 assert_not_nil assigns(:projects)
41 assert_not_nil assigns(:projects)
42
42
43 assert_tag :ul, :child => {:tag => 'li',
43 assert_tag :ul, :child => {:tag => 'li',
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
44 :descendant => {:tag => 'a', :content => 'eCookbook'},
45 :child => { :tag => 'ul',
45 :child => { :tag => 'ul',
46 :descendant => { :tag => 'a',
46 :descendant => { :tag => 'a',
47 :content => 'Child of private child'
47 :content => 'Child of private child'
48 }
48 }
49 }
49 }
50 }
50 }
51
51
52 assert_no_tag :a, :content => /Private child of eCookbook/
52 assert_no_tag :a, :content => /Private child of eCookbook/
53 end
53 end
54
54
55 def test_index_atom
55 def test_index_atom
56 get :index, :format => 'atom'
56 get :index, :format => 'atom'
57 assert_response :success
57 assert_response :success
58 assert_template 'common/feed.atom.rxml'
58 assert_template 'common/feed.atom.rxml'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
59 assert_select 'feed>title', :text => 'Redmine: Latest projects'
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
60 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
61 end
61 end
62
62
63 context "#add" do
63 context "#add" do
64 context "by admin user" do
64 context "by admin user" do
65 setup do
65 setup do
66 @request.session[:user_id] = 1
66 @request.session[:user_id] = 1
67 end
67 end
68
68
69 should "accept get" do
69 should "accept get" do
70 get :add
70 get :add
71 assert_response :success
71 assert_response :success
72 assert_template 'add'
72 assert_template 'add'
73 end
73 end
74
74
75 should "accept post" do
75 should "accept post" do
76 post :add, :project => { :name => "blog",
76 post :add, :project => { :name => "blog",
77 :description => "weblog",
77 :description => "weblog",
78 :identifier => "blog",
78 :identifier => "blog",
79 :is_public => 1,
79 :is_public => 1,
80 :custom_field_values => { '3' => 'Beta' }
80 :custom_field_values => { '3' => 'Beta' }
81 }
81 }
82 assert_redirected_to '/projects/blog/settings'
82 assert_redirected_to '/projects/blog/settings'
83
83
84 project = Project.find_by_name('blog')
84 project = Project.find_by_name('blog')
85 assert_kind_of Project, project
85 assert_kind_of Project, project
86 assert_equal 'weblog', project.description
86 assert_equal 'weblog', project.description
87 assert_equal true, project.is_public?
87 assert_equal true, project.is_public?
88 assert_nil project.parent
88 assert_nil project.parent
89 end
89 end
90
90
91 should "accept post with parent" do
91 should "accept post with parent" do
92 post :add, :project => { :name => "blog",
92 post :add, :project => { :name => "blog",
93 :description => "weblog",
93 :description => "weblog",
94 :identifier => "blog",
94 :identifier => "blog",
95 :is_public => 1,
95 :is_public => 1,
96 :custom_field_values => { '3' => 'Beta' },
96 :custom_field_values => { '3' => 'Beta' },
97 :parent_id => 1
97 :parent_id => 1
98 }
98 }
99 assert_redirected_to '/projects/blog/settings'
99 assert_redirected_to '/projects/blog/settings'
100
100
101 project = Project.find_by_name('blog')
101 project = Project.find_by_name('blog')
102 assert_kind_of Project, project
102 assert_kind_of Project, project
103 assert_equal Project.find(1), project.parent
103 assert_equal Project.find(1), project.parent
104 end
104 end
105 end
105 end
106
106
107 context "by non-admin user with add_project permission" do
107 context "by non-admin user with add_project permission" do
108 setup do
108 setup do
109 Role.non_member.add_permission! :add_project
109 Role.non_member.add_permission! :add_project
110 @request.session[:user_id] = 9
110 @request.session[:user_id] = 9
111 end
111 end
112
112
113 should "accept get" do
113 should "accept get" do
114 get :add
114 get :add
115 assert_response :success
115 assert_response :success
116 assert_template 'add'
116 assert_template 'add'
117 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
117 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}
118 end
118 end
119
119
120 should "accept post" do
120 should "accept post" do
121 post :add, :project => { :name => "blog",
121 post :add, :project => { :name => "blog",
122 :description => "weblog",
122 :description => "weblog",
123 :identifier => "blog",
123 :identifier => "blog",
124 :is_public => 1,
124 :is_public => 1,
125 :custom_field_values => { '3' => 'Beta' }
125 :custom_field_values => { '3' => 'Beta' }
126 }
126 }
127
127
128 assert_redirected_to '/projects/blog/settings'
128 assert_redirected_to '/projects/blog/settings'
129
129
130 project = Project.find_by_name('blog')
130 project = Project.find_by_name('blog')
131 assert_kind_of Project, project
131 assert_kind_of Project, project
132 assert_equal 'weblog', project.description
132 assert_equal 'weblog', project.description
133 assert_equal true, project.is_public?
133 assert_equal true, project.is_public?
134
134
135 # User should be added as a project member
135 # User should be added as a project member
136 assert User.find(9).member_of?(project)
136 assert User.find(9).member_of?(project)
137 assert_equal 1, project.members.size
137 assert_equal 1, project.members.size
138 end
138 end
139
139
140 should "fail with parent_id" do
140 should "fail with parent_id" do
141 assert_no_difference 'Project.count' do
141 assert_no_difference 'Project.count' do
142 post :add, :project => { :name => "blog",
142 post :add, :project => { :name => "blog",
143 :description => "weblog",
143 :description => "weblog",
144 :identifier => "blog",
144 :identifier => "blog",
145 :is_public => 1,
145 :is_public => 1,
146 :custom_field_values => { '3' => 'Beta' },
146 :custom_field_values => { '3' => 'Beta' },
147 :parent_id => 1
147 :parent_id => 1
148 }
148 }
149 end
149 end
150 assert_response :success
150 assert_response :success
151 project = assigns(:project)
151 project = assigns(:project)
152 assert_kind_of Project, project
152 assert_kind_of Project, project
153 assert_not_nil project.errors.on(:parent_id)
153 assert_not_nil project.errors.on(:parent_id)
154 end
154 end
155 end
155 end
156
156
157 context "by non-admin user with add_subprojects permission" do
157 context "by non-admin user with add_subprojects permission" do
158 setup do
158 setup do
159 Role.find(1).remove_permission! :add_project
159 Role.find(1).remove_permission! :add_project
160 Role.find(1).add_permission! :add_subprojects
160 Role.find(1).add_permission! :add_subprojects
161 @request.session[:user_id] = 2
161 @request.session[:user_id] = 2
162 end
162 end
163
163
164 should "accept get" do
164 should "accept get" do
165 get :add, :parent_id => 'ecookbook'
165 get :add, :parent_id => 'ecookbook'
166 assert_response :success
166 assert_response :success
167 assert_template 'add'
167 assert_template 'add'
168 # parent project selected
168 # parent project selected
169 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
169 assert_tag :select, :attributes => {:name => 'project[parent_id]'},
170 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
170 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
171 # no empty value
171 # no empty value
172 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
172 assert_no_tag :select, :attributes => {:name => 'project[parent_id]'},
173 :child => {:tag => 'option', :attributes => {:value => ''}}
173 :child => {:tag => 'option', :attributes => {:value => ''}}
174 end
174 end
175
175
176 should "accept post with parent_id" do
176 should "accept post with parent_id" do
177 post :add, :project => { :name => "blog",
177 post :add, :project => { :name => "blog",
178 :description => "weblog",
178 :description => "weblog",
179 :identifier => "blog",
179 :identifier => "blog",
180 :is_public => 1,
180 :is_public => 1,
181 :custom_field_values => { '3' => 'Beta' },
181 :custom_field_values => { '3' => 'Beta' },
182 :parent_id => 1
182 :parent_id => 1
183 }
183 }
184 assert_redirected_to '/projects/blog/settings'
184 assert_redirected_to '/projects/blog/settings'
185 project = Project.find_by_name('blog')
185 project = Project.find_by_name('blog')
186 end
186 end
187
187
188 should "fail without parent_id" do
188 should "fail without parent_id" do
189 assert_no_difference 'Project.count' do
189 assert_no_difference 'Project.count' do
190 post :add, :project => { :name => "blog",
190 post :add, :project => { :name => "blog",
191 :description => "weblog",
191 :description => "weblog",
192 :identifier => "blog",
192 :identifier => "blog",
193 :is_public => 1,
193 :is_public => 1,
194 :custom_field_values => { '3' => 'Beta' }
194 :custom_field_values => { '3' => 'Beta' }
195 }
195 }
196 end
196 end
197 assert_response :success
197 assert_response :success
198 project = assigns(:project)
198 project = assigns(:project)
199 assert_kind_of Project, project
199 assert_kind_of Project, project
200 assert_not_nil project.errors.on(:parent_id)
200 assert_not_nil project.errors.on(:parent_id)
201 end
201 end
202
202
203 should "fail with unauthorized parent_id" do
203 should "fail with unauthorized parent_id" do
204 assert !User.find(2).member_of?(Project.find(6))
204 assert !User.find(2).member_of?(Project.find(6))
205 assert_no_difference 'Project.count' do
205 assert_no_difference 'Project.count' do
206 post :add, :project => { :name => "blog",
206 post :add, :project => { :name => "blog",
207 :description => "weblog",
207 :description => "weblog",
208 :identifier => "blog",
208 :identifier => "blog",
209 :is_public => 1,
209 :is_public => 1,
210 :custom_field_values => { '3' => 'Beta' },
210 :custom_field_values => { '3' => 'Beta' },
211 :parent_id => 6
211 :parent_id => 6
212 }
212 }
213 end
213 end
214 assert_response :success
214 assert_response :success
215 project = assigns(:project)
215 project = assigns(:project)
216 assert_kind_of Project, project
216 assert_kind_of Project, project
217 assert_not_nil project.errors.on(:parent_id)
217 assert_not_nil project.errors.on(:parent_id)
218 end
218 end
219 end
219 end
220 end
220 end
221
221
222 def test_show_by_id
222 def test_show_by_id
223 get :show, :id => 1
223 get :show, :id => 1
224 assert_response :success
224 assert_response :success
225 assert_template 'show'
225 assert_template 'show'
226 assert_not_nil assigns(:project)
226 assert_not_nil assigns(:project)
227 end
227 end
228
228
229 def test_show_by_identifier
229 def test_show_by_identifier
230 get :show, :id => 'ecookbook'
230 get :show, :id => 'ecookbook'
231 assert_response :success
231 assert_response :success
232 assert_template 'show'
232 assert_template 'show'
233 assert_not_nil assigns(:project)
233 assert_not_nil assigns(:project)
234 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
234 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
235 end
235 end
236
236
237 def test_show_should_not_fail_when_custom_values_are_nil
237 def test_show_should_not_fail_when_custom_values_are_nil
238 project = Project.find_by_identifier('ecookbook')
238 project = Project.find_by_identifier('ecookbook')
239 project.custom_values.first.update_attribute(:value, nil)
239 project.custom_values.first.update_attribute(:value, nil)
240 get :show, :id => 'ecookbook'
240 get :show, :id => 'ecookbook'
241 assert_response :success
241 assert_response :success
242 assert_template 'show'
242 assert_template 'show'
243 assert_not_nil assigns(:project)
243 assert_not_nil assigns(:project)
244 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
244 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
245 end
245 end
246
246
247 def test_private_subprojects_hidden
247 def test_private_subprojects_hidden
248 get :show, :id => 'ecookbook'
248 get :show, :id => 'ecookbook'
249 assert_response :success
249 assert_response :success
250 assert_template 'show'
250 assert_template 'show'
251 assert_no_tag :tag => 'a', :content => /Private child/
251 assert_no_tag :tag => 'a', :content => /Private child/
252 end
252 end
253
253
254 def test_private_subprojects_visible
254 def test_private_subprojects_visible
255 @request.session[:user_id] = 2 # manager who is a member of the private subproject
255 @request.session[:user_id] = 2 # manager who is a member of the private subproject
256 get :show, :id => 'ecookbook'
256 get :show, :id => 'ecookbook'
257 assert_response :success
257 assert_response :success
258 assert_template 'show'
258 assert_template 'show'
259 assert_tag :tag => 'a', :content => /Private child/
259 assert_tag :tag => 'a', :content => /Private child/
260 end
260 end
261
261
262 def test_settings
262 def test_settings
263 @request.session[:user_id] = 2 # manager
263 @request.session[:user_id] = 2 # manager
264 get :settings, :id => 1
264 get :settings, :id => 1
265 assert_response :success
265 assert_response :success
266 assert_template 'settings'
266 assert_template 'settings'
267 end
267 end
268
268
269 def test_edit
269 def test_edit
270 @request.session[:user_id] = 2 # manager
270 @request.session[:user_id] = 2 # manager
271 post :edit, :id => 1, :project => {:name => 'Test changed name',
271 post :edit, :id => 1, :project => {:name => 'Test changed name',
272 :issue_custom_field_ids => ['']}
272 :issue_custom_field_ids => ['']}
273 assert_redirected_to 'projects/ecookbook/settings'
273 assert_redirected_to 'projects/ecookbook/settings'
274 project = Project.find(1)
274 project = Project.find(1)
275 assert_equal 'Test changed name', project.name
275 assert_equal 'Test changed name', project.name
276 end
276 end
277
277
278 def test_get_destroy
278 def test_get_destroy
279 @request.session[:user_id] = 1 # admin
279 @request.session[:user_id] = 1 # admin
280 get :destroy, :id => 1
280 get :destroy, :id => 1
281 assert_response :success
281 assert_response :success
282 assert_template 'destroy'
282 assert_template 'destroy'
283 assert_not_nil Project.find_by_id(1)
283 assert_not_nil Project.find_by_id(1)
284 end
284 end
285
285
286 def test_post_destroy
286 def test_post_destroy
287 @request.session[:user_id] = 1 # admin
287 @request.session[:user_id] = 1 # admin
288 post :destroy, :id => 1, :confirm => 1
288 post :destroy, :id => 1, :confirm => 1
289 assert_redirected_to 'admin/projects'
289 assert_redirected_to 'admin/projects'
290 assert_nil Project.find_by_id(1)
290 assert_nil Project.find_by_id(1)
291 end
291 end
292
292
293 def test_add_file
293 def test_add_file
294 set_tmp_attachments_directory
294 set_tmp_attachments_directory
295 @request.session[:user_id] = 2
295 @request.session[:user_id] = 2
296 Setting.notified_events = ['file_added']
296 Setting.notified_events = ['file_added']
297 ActionMailer::Base.deliveries.clear
297 ActionMailer::Base.deliveries.clear
298
298
299 assert_difference 'Attachment.count' do
299 assert_difference 'Attachment.count' do
300 post :add_file, :id => 1, :version_id => '',
300 post :add_file, :id => 1, :version_id => '',
301 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
301 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
302 end
302 end
303 assert_redirected_to 'projects/ecookbook/files'
303 assert_redirected_to 'projects/ecookbook/files'
304 a = Attachment.find(:first, :order => 'created_on DESC')
304 a = Attachment.find(:first, :order => 'created_on DESC')
305 assert_equal 'testfile.txt', a.filename
305 assert_equal 'testfile.txt', a.filename
306 assert_equal Project.find(1), a.container
306 assert_equal Project.find(1), a.container
307
307
308 mail = ActionMailer::Base.deliveries.last
308 mail = ActionMailer::Base.deliveries.last
309 assert_kind_of TMail::Mail, mail
309 assert_kind_of TMail::Mail, mail
310 assert_equal "[eCookbook] New file", mail.subject
310 assert_equal "[eCookbook] New file", mail.subject
311 assert mail.body.include?('testfile.txt')
311 assert mail.body.include?('testfile.txt')
312 end
312 end
313
313
314 def test_add_version_file
314 def test_add_version_file
315 set_tmp_attachments_directory
315 set_tmp_attachments_directory
316 @request.session[:user_id] = 2
316 @request.session[:user_id] = 2
317 Setting.notified_events = ['file_added']
317 Setting.notified_events = ['file_added']
318
318
319 assert_difference 'Attachment.count' do
319 assert_difference 'Attachment.count' do
320 post :add_file, :id => 1, :version_id => '2',
320 post :add_file, :id => 1, :version_id => '2',
321 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
321 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
322 end
322 end
323 assert_redirected_to 'projects/ecookbook/files'
323 assert_redirected_to 'projects/ecookbook/files'
324 a = Attachment.find(:first, :order => 'created_on DESC')
324 a = Attachment.find(:first, :order => 'created_on DESC')
325 assert_equal 'testfile.txt', a.filename
325 assert_equal 'testfile.txt', a.filename
326 assert_equal Version.find(2), a.container
326 assert_equal Version.find(2), a.container
327 end
327 end
328
328
329 def test_list_files
329 def test_list_files
330 get :list_files, :id => 1
330 get :list_files, :id => 1
331 assert_response :success
331 assert_response :success
332 assert_template 'list_files'
332 assert_template 'list_files'
333 assert_not_nil assigns(:containers)
333 assert_not_nil assigns(:containers)
334
334
335 # file attached to the project
335 # file attached to the project
336 assert_tag :a, :content => 'project_file.zip',
336 assert_tag :a, :content => 'project_file.zip',
337 :attributes => { :href => '/attachments/download/8/project_file.zip' }
337 :attributes => { :href => '/attachments/download/8/project_file.zip' }
338
338
339 # file attached to a project's version
339 # file attached to a project's version
340 assert_tag :a, :content => 'version_file.zip',
340 assert_tag :a, :content => 'version_file.zip',
341 :attributes => { :href => '/attachments/download/9/version_file.zip' }
341 :attributes => { :href => '/attachments/download/9/version_file.zip' }
342 end
342 end
343
343
344 def test_roadmap
344 def test_roadmap
345 get :roadmap, :id => 1
345 get :roadmap, :id => 1
346 assert_response :success
346 assert_response :success
347 assert_template 'roadmap'
347 assert_template 'roadmap'
348 assert_not_nil assigns(:versions)
348 assert_not_nil assigns(:versions)
349 # Version with no date set appears
349 # Version with no date set appears
350 assert assigns(:versions).include?(Version.find(3))
350 assert assigns(:versions).include?(Version.find(3))
351 # Completed version doesn't appear
351 # Completed version doesn't appear
352 assert !assigns(:versions).include?(Version.find(1))
352 assert !assigns(:versions).include?(Version.find(1))
353 end
353 end
354
354
355 def test_roadmap_with_completed_versions
355 def test_roadmap_with_completed_versions
356 get :roadmap, :id => 1, :completed => 1
356 get :roadmap, :id => 1, :completed => 1
357 assert_response :success
357 assert_response :success
358 assert_template 'roadmap'
358 assert_template 'roadmap'
359 assert_not_nil assigns(:versions)
359 assert_not_nil assigns(:versions)
360 # Version with no date set appears
360 # Version with no date set appears
361 assert assigns(:versions).include?(Version.find(3))
361 assert assigns(:versions).include?(Version.find(3))
362 # Completed version appears
362 # Completed version appears
363 assert assigns(:versions).include?(Version.find(1))
363 assert assigns(:versions).include?(Version.find(1))
364 end
364 end
365
365
366 def test_roadmap_showing_subprojects_versions
366 def test_roadmap_showing_subprojects_versions
367 @subproject_version = Version.generate!(:project => Project.find(3))
367 get :roadmap, :id => 1, :with_subprojects => 1
368 get :roadmap, :id => 1, :with_subprojects => 1
368 assert_response :success
369 assert_response :success
369 assert_template 'roadmap'
370 assert_template 'roadmap'
370 assert_not_nil assigns(:versions)
371 assert_not_nil assigns(:versions)
371 # Version on subproject appears
372
372 assert assigns(:versions).include?(Version.find(4))
373 assert assigns(:versions).include?(Version.find(4)), "Shared version not found"
374 assert assigns(:versions).include?(@subproject_version), "Subproject version not found"
373 end
375 end
374 def test_project_activity
376 def test_project_activity
375 get :activity, :id => 1, :with_subprojects => 0
377 get :activity, :id => 1, :with_subprojects => 0
376 assert_response :success
378 assert_response :success
377 assert_template 'activity'
379 assert_template 'activity'
378 assert_not_nil assigns(:events_by_day)
380 assert_not_nil assigns(:events_by_day)
379
381
380 assert_tag :tag => "h3",
382 assert_tag :tag => "h3",
381 :content => /#{2.days.ago.to_date.day}/,
383 :content => /#{2.days.ago.to_date.day}/,
382 :sibling => { :tag => "dl",
384 :sibling => { :tag => "dl",
383 :child => { :tag => "dt",
385 :child => { :tag => "dt",
384 :attributes => { :class => /issue-edit/ },
386 :attributes => { :class => /issue-edit/ },
385 :child => { :tag => "a",
387 :child => { :tag => "a",
386 :content => /(#{IssueStatus.find(2).name})/,
388 :content => /(#{IssueStatus.find(2).name})/,
387 }
389 }
388 }
390 }
389 }
391 }
390 end
392 end
391
393
392 def test_previous_project_activity
394 def test_previous_project_activity
393 get :activity, :id => 1, :from => 3.days.ago.to_date
395 get :activity, :id => 1, :from => 3.days.ago.to_date
394 assert_response :success
396 assert_response :success
395 assert_template 'activity'
397 assert_template 'activity'
396 assert_not_nil assigns(:events_by_day)
398 assert_not_nil assigns(:events_by_day)
397
399
398 assert_tag :tag => "h3",
400 assert_tag :tag => "h3",
399 :content => /#{3.day.ago.to_date.day}/,
401 :content => /#{3.day.ago.to_date.day}/,
400 :sibling => { :tag => "dl",
402 :sibling => { :tag => "dl",
401 :child => { :tag => "dt",
403 :child => { :tag => "dt",
402 :attributes => { :class => /issue/ },
404 :attributes => { :class => /issue/ },
403 :child => { :tag => "a",
405 :child => { :tag => "a",
404 :content => /#{Issue.find(1).subject}/,
406 :content => /#{Issue.find(1).subject}/,
405 }
407 }
406 }
408 }
407 }
409 }
408 end
410 end
409
411
410 def test_global_activity
412 def test_global_activity
411 get :activity
413 get :activity
412 assert_response :success
414 assert_response :success
413 assert_template 'activity'
415 assert_template 'activity'
414 assert_not_nil assigns(:events_by_day)
416 assert_not_nil assigns(:events_by_day)
415
417
416 assert_tag :tag => "h3",
418 assert_tag :tag => "h3",
417 :content => /#{5.day.ago.to_date.day}/,
419 :content => /#{5.day.ago.to_date.day}/,
418 :sibling => { :tag => "dl",
420 :sibling => { :tag => "dl",
419 :child => { :tag => "dt",
421 :child => { :tag => "dt",
420 :attributes => { :class => /issue/ },
422 :attributes => { :class => /issue/ },
421 :child => { :tag => "a",
423 :child => { :tag => "a",
422 :content => /#{Issue.find(5).subject}/,
424 :content => /#{Issue.find(5).subject}/,
423 }
425 }
424 }
426 }
425 }
427 }
426 end
428 end
427
429
428 def test_user_activity
430 def test_user_activity
429 get :activity, :user_id => 2
431 get :activity, :user_id => 2
430 assert_response :success
432 assert_response :success
431 assert_template 'activity'
433 assert_template 'activity'
432 assert_not_nil assigns(:events_by_day)
434 assert_not_nil assigns(:events_by_day)
433
435
434 assert_tag :tag => "h3",
436 assert_tag :tag => "h3",
435 :content => /#{3.day.ago.to_date.day}/,
437 :content => /#{3.day.ago.to_date.day}/,
436 :sibling => { :tag => "dl",
438 :sibling => { :tag => "dl",
437 :child => { :tag => "dt",
439 :child => { :tag => "dt",
438 :attributes => { :class => /issue/ },
440 :attributes => { :class => /issue/ },
439 :child => { :tag => "a",
441 :child => { :tag => "a",
440 :content => /#{Issue.find(1).subject}/,
442 :content => /#{Issue.find(1).subject}/,
441 }
443 }
442 }
444 }
443 }
445 }
444 end
446 end
445
447
446 def test_activity_atom_feed
448 def test_activity_atom_feed
447 get :activity, :format => 'atom'
449 get :activity, :format => 'atom'
448 assert_response :success
450 assert_response :success
449 assert_template 'common/feed.atom.rxml'
451 assert_template 'common/feed.atom.rxml'
450 assert_tag :tag => 'entry', :child => {
452 assert_tag :tag => 'entry', :child => {
451 :tag => 'link',
453 :tag => 'link',
452 :attributes => {:href => 'http://test.host/issues/11'}}
454 :attributes => {:href => 'http://test.host/issues/11'}}
453 end
455 end
454
456
455 def test_archive
457 def test_archive
456 @request.session[:user_id] = 1 # admin
458 @request.session[:user_id] = 1 # admin
457 post :archive, :id => 1
459 post :archive, :id => 1
458 assert_redirected_to 'admin/projects'
460 assert_redirected_to 'admin/projects'
459 assert !Project.find(1).active?
461 assert !Project.find(1).active?
460 end
462 end
461
463
462 def test_unarchive
464 def test_unarchive
463 @request.session[:user_id] = 1 # admin
465 @request.session[:user_id] = 1 # admin
464 Project.find(1).archive
466 Project.find(1).archive
465 post :unarchive, :id => 1
467 post :unarchive, :id => 1
466 assert_redirected_to 'admin/projects'
468 assert_redirected_to 'admin/projects'
467 assert Project.find(1).active?
469 assert Project.find(1).active?
468 end
470 end
469
471
470 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
472 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
471 CustomField.delete_all
473 CustomField.delete_all
472 parent = nil
474 parent = nil
473 6.times do |i|
475 6.times do |i|
474 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
476 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
475 p.set_parent!(parent)
477 p.set_parent!(parent)
476 get :show, :id => p
478 get :show, :id => p
477 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
479 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
478 :children => { :count => [i, 3].min,
480 :children => { :count => [i, 3].min,
479 :only => { :tag => 'a' } }
481 :only => { :tag => 'a' } }
480
482
481 parent = p
483 parent = p
482 end
484 end
483 end
485 end
484
486
485 def test_copy_with_project
487 def test_copy_with_project
486 @request.session[:user_id] = 1 # admin
488 @request.session[:user_id] = 1 # admin
487 get :copy, :id => 1
489 get :copy, :id => 1
488 assert_response :success
490 assert_response :success
489 assert_template 'copy'
491 assert_template 'copy'
490 assert assigns(:project)
492 assert assigns(:project)
491 assert_equal Project.find(1).description, assigns(:project).description
493 assert_equal Project.find(1).description, assigns(:project).description
492 assert_nil assigns(:project).id
494 assert_nil assigns(:project).id
493 end
495 end
494
496
495 def test_copy_without_project
497 def test_copy_without_project
496 @request.session[:user_id] = 1 # admin
498 @request.session[:user_id] = 1 # admin
497 get :copy
499 get :copy
498 assert_response :redirect
500 assert_response :redirect
499 assert_redirected_to :controller => 'admin', :action => 'projects'
501 assert_redirected_to :controller => 'admin', :action => 'projects'
500 end
502 end
501
503
502 def test_jump_should_redirect_to_active_tab
504 def test_jump_should_redirect_to_active_tab
503 get :show, :id => 1, :jump => 'issues'
505 get :show, :id => 1, :jump => 'issues'
504 assert_redirected_to 'projects/ecookbook/issues'
506 assert_redirected_to 'projects/ecookbook/issues'
505 end
507 end
506
508
507 def test_jump_should_not_redirect_to_inactive_tab
509 def test_jump_should_not_redirect_to_inactive_tab
508 get :show, :id => 3, :jump => 'documents'
510 get :show, :id => 3, :jump => 'documents'
509 assert_response :success
511 assert_response :success
510 assert_template 'show'
512 assert_template 'show'
511 end
513 end
512
514
513 def test_jump_should_not_redirect_to_unknown_tab
515 def test_jump_should_not_redirect_to_unknown_tab
514 get :show, :id => 3, :jump => 'foobar'
516 get :show, :id => 3, :jump => 'foobar'
515 assert_response :success
517 assert_response :success
516 assert_template 'show'
518 assert_template 'show'
517 end
519 end
518
520
519 def test_reset_activities
521 def test_reset_activities
520 @request.session[:user_id] = 2 # manager
522 @request.session[:user_id] = 2 # manager
521 project_activity = TimeEntryActivity.new({
523 project_activity = TimeEntryActivity.new({
522 :name => 'Project Specific',
524 :name => 'Project Specific',
523 :parent => TimeEntryActivity.find(:first),
525 :parent => TimeEntryActivity.find(:first),
524 :project => Project.find(1),
526 :project => Project.find(1),
525 :active => true
527 :active => true
526 })
528 })
527 assert project_activity.save
529 assert project_activity.save
528 project_activity_two = TimeEntryActivity.new({
530 project_activity_two = TimeEntryActivity.new({
529 :name => 'Project Specific Two',
531 :name => 'Project Specific Two',
530 :parent => TimeEntryActivity.find(:last),
532 :parent => TimeEntryActivity.find(:last),
531 :project => Project.find(1),
533 :project => Project.find(1),
532 :active => true
534 :active => true
533 })
535 })
534 assert project_activity_two.save
536 assert project_activity_two.save
535
537
536 delete :reset_activities, :id => 1
538 delete :reset_activities, :id => 1
537 assert_response :redirect
539 assert_response :redirect
538 assert_redirected_to 'projects/ecookbook/settings/activities'
540 assert_redirected_to 'projects/ecookbook/settings/activities'
539
541
540 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
542 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
541 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
543 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
542 end
544 end
543
545
544 def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
546 def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
545 @request.session[:user_id] = 2 # manager
547 @request.session[:user_id] = 2 # manager
546 project_activity = TimeEntryActivity.new({
548 project_activity = TimeEntryActivity.new({
547 :name => 'Project Specific Design',
549 :name => 'Project Specific Design',
548 :parent => TimeEntryActivity.find(9),
550 :parent => TimeEntryActivity.find(9),
549 :project => Project.find(1),
551 :project => Project.find(1),
550 :active => true
552 :active => true
551 })
553 })
552 assert project_activity.save
554 assert project_activity.save
553 assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
555 assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
554 assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
556 assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
555
557
556 delete :reset_activities, :id => 1
558 delete :reset_activities, :id => 1
557 assert_response :redirect
559 assert_response :redirect
558 assert_redirected_to 'projects/ecookbook/settings/activities'
560 assert_redirected_to 'projects/ecookbook/settings/activities'
559
561
560 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
562 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
561 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
563 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
562 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
564 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
563 end
565 end
564
566
565 def test_save_activities_to_override_system_activities
567 def test_save_activities_to_override_system_activities
566 @request.session[:user_id] = 2 # manager
568 @request.session[:user_id] = 2 # manager
567 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
569 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
568
570
569 post :save_activities, :id => 1, :enumerations => {
571 post :save_activities, :id => 1, :enumerations => {
570 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
572 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
571 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
573 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
572 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
574 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
573 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
575 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
574 }
576 }
575
577
576 assert_response :redirect
578 assert_response :redirect
577 assert_redirected_to 'projects/ecookbook/settings/activities'
579 assert_redirected_to 'projects/ecookbook/settings/activities'
578
580
579 # Created project specific activities...
581 # Created project specific activities...
580 project = Project.find('ecookbook')
582 project = Project.find('ecookbook')
581
583
582 # ... Design
584 # ... Design
583 design = project.time_entry_activities.find_by_name("Design")
585 design = project.time_entry_activities.find_by_name("Design")
584 assert design, "Project activity not found"
586 assert design, "Project activity not found"
585
587
586 assert_equal 9, design.parent_id # Relate to the system activity
588 assert_equal 9, design.parent_id # Relate to the system activity
587 assert_not_equal design.parent.id, design.id # Different records
589 assert_not_equal design.parent.id, design.id # Different records
588 assert_equal design.parent.name, design.name # Same name
590 assert_equal design.parent.name, design.name # Same name
589 assert !design.active?
591 assert !design.active?
590
592
591 # ... Development
593 # ... Development
592 development = project.time_entry_activities.find_by_name("Development")
594 development = project.time_entry_activities.find_by_name("Development")
593 assert development, "Project activity not found"
595 assert development, "Project activity not found"
594
596
595 assert_equal 10, development.parent_id # Relate to the system activity
597 assert_equal 10, development.parent_id # Relate to the system activity
596 assert_not_equal development.parent.id, development.id # Different records
598 assert_not_equal development.parent.id, development.id # Different records
597 assert_equal development.parent.name, development.name # Same name
599 assert_equal development.parent.name, development.name # Same name
598 assert development.active?
600 assert development.active?
599 assert_equal "0", development.custom_value_for(billable_field).value
601 assert_equal "0", development.custom_value_for(billable_field).value
600
602
601 # ... Inactive Activity
603 # ... Inactive Activity
602 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
604 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
603 assert previously_inactive, "Project activity not found"
605 assert previously_inactive, "Project activity not found"
604
606
605 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
607 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
606 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
608 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
607 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
609 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
608 assert previously_inactive.active?
610 assert previously_inactive.active?
609 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
611 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
610
612
611 # ... QA
613 # ... QA
612 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
614 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
613 end
615 end
614
616
615 def test_save_activities_will_update_project_specific_activities
617 def test_save_activities_will_update_project_specific_activities
616 @request.session[:user_id] = 2 # manager
618 @request.session[:user_id] = 2 # manager
617
619
618 project_activity = TimeEntryActivity.new({
620 project_activity = TimeEntryActivity.new({
619 :name => 'Project Specific',
621 :name => 'Project Specific',
620 :parent => TimeEntryActivity.find(:first),
622 :parent => TimeEntryActivity.find(:first),
621 :project => Project.find(1),
623 :project => Project.find(1),
622 :active => true
624 :active => true
623 })
625 })
624 assert project_activity.save
626 assert project_activity.save
625 project_activity_two = TimeEntryActivity.new({
627 project_activity_two = TimeEntryActivity.new({
626 :name => 'Project Specific Two',
628 :name => 'Project Specific Two',
627 :parent => TimeEntryActivity.find(:last),
629 :parent => TimeEntryActivity.find(:last),
628 :project => Project.find(1),
630 :project => Project.find(1),
629 :active => true
631 :active => true
630 })
632 })
631 assert project_activity_two.save
633 assert project_activity_two.save
632
634
633
635
634 post :save_activities, :id => 1, :enumerations => {
636 post :save_activities, :id => 1, :enumerations => {
635 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
637 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
636 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
638 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
637 }
639 }
638
640
639 assert_response :redirect
641 assert_response :redirect
640 assert_redirected_to 'projects/ecookbook/settings/activities'
642 assert_redirected_to 'projects/ecookbook/settings/activities'
641
643
642 # Created project specific activities...
644 # Created project specific activities...
643 project = Project.find('ecookbook')
645 project = Project.find('ecookbook')
644 assert_equal 2, project.time_entry_activities.count
646 assert_equal 2, project.time_entry_activities.count
645
647
646 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
648 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
647 assert activity_one, "Project activity not found"
649 assert activity_one, "Project activity not found"
648 assert_equal project_activity.id, activity_one.id
650 assert_equal project_activity.id, activity_one.id
649 assert !activity_one.active?
651 assert !activity_one.active?
650
652
651 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
653 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
652 assert activity_two, "Project activity not found"
654 assert activity_two, "Project activity not found"
653 assert_equal project_activity_two.id, activity_two.id
655 assert_equal project_activity_two.id, activity_two.id
654 assert !activity_two.active?
656 assert !activity_two.active?
655 end
657 end
656
658
657 def test_save_activities_when_creating_new_activities_will_convert_existing_data
659 def test_save_activities_when_creating_new_activities_will_convert_existing_data
658 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
660 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
659
661
660 @request.session[:user_id] = 2 # manager
662 @request.session[:user_id] = 2 # manager
661 post :save_activities, :id => 1, :enumerations => {
663 post :save_activities, :id => 1, :enumerations => {
662 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
664 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
663 }
665 }
664 assert_response :redirect
666 assert_response :redirect
665
667
666 # No more TimeEntries using the system activity
668 # No more TimeEntries using the system activity
667 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
669 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
668 # All TimeEntries using project activity
670 # All TimeEntries using project activity
669 project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
671 project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
670 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
672 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
671 end
673 end
672
674
673 def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
675 def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
674 # TODO: Need to cause an exception on create but these tests
676 # TODO: Need to cause an exception on create but these tests
675 # aren't setup for mocking. Just create a record now so the
677 # aren't setup for mocking. Just create a record now so the
676 # second one is a dupicate
678 # second one is a dupicate
677 parent = TimeEntryActivity.find(9)
679 parent = TimeEntryActivity.find(9)
678 TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
680 TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
679 TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
681 TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
680
682
681 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
683 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
682 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
684 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
683
685
684 @request.session[:user_id] = 2 # manager
686 @request.session[:user_id] = 2 # manager
685 post :save_activities, :id => 1, :enumerations => {
687 post :save_activities, :id => 1, :enumerations => {
686 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
688 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
687 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
689 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
688 }
690 }
689 assert_response :redirect
691 assert_response :redirect
690
692
691 # TimeEntries shouldn't have been reassigned on the failed record
693 # TimeEntries shouldn't have been reassigned on the failed record
692 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
694 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
693 # TimeEntries shouldn't have been reassigned on the saved record either
695 # TimeEntries shouldn't have been reassigned on the saved record either
694 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
696 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
695 end
697 end
696
698
697 # A hook that is manually registered later
699 # A hook that is manually registered later
698 class ProjectBasedTemplate < Redmine::Hook::ViewListener
700 class ProjectBasedTemplate < Redmine::Hook::ViewListener
699 def view_layouts_base_html_head(context)
701 def view_layouts_base_html_head(context)
700 # Adds a project stylesheet
702 # Adds a project stylesheet
701 stylesheet_link_tag(context[:project].identifier) if context[:project]
703 stylesheet_link_tag(context[:project].identifier) if context[:project]
702 end
704 end
703 end
705 end
704 # Don't use this hook now
706 # Don't use this hook now
705 Redmine::Hook.clear_listeners
707 Redmine::Hook.clear_listeners
706
708
707 def test_hook_response
709 def test_hook_response
708 Redmine::Hook.add_listener(ProjectBasedTemplate)
710 Redmine::Hook.add_listener(ProjectBasedTemplate)
709 get :show, :id => 1
711 get :show, :id => 1
710 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
712 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
711 :parent => {:tag => 'head'}
713 :parent => {:tag => 'head'}
712
714
713 Redmine::Hook.clear_listeners
715 Redmine::Hook.clear_listeners
714 end
716 end
715 end
717 end
@@ -1,792 +1,845
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 :all
21 fixtures :all
22
22
23 def setup
23 def setup
24 @ecookbook = Project.find(1)
24 @ecookbook = Project.find(1)
25 @ecookbook_sub1 = Project.find(3)
25 @ecookbook_sub1 = Project.find(3)
26 User.current = nil
26 User.current = nil
27 end
27 end
28
28
29 should_validate_presence_of :name
29 should_validate_presence_of :name
30 should_validate_presence_of :identifier
30 should_validate_presence_of :identifier
31
31
32 should_validate_uniqueness_of :name
32 should_validate_uniqueness_of :name
33 should_validate_uniqueness_of :identifier
33 should_validate_uniqueness_of :identifier
34
34
35 context "associations" do
35 context "associations" do
36 should_have_many :members
36 should_have_many :members
37 should_have_many :users, :through => :members
37 should_have_many :users, :through => :members
38 should_have_many :member_principals
38 should_have_many :member_principals
39 should_have_many :principals, :through => :member_principals
39 should_have_many :principals, :through => :member_principals
40 should_have_many :enabled_modules
40 should_have_many :enabled_modules
41 should_have_many :issues
41 should_have_many :issues
42 should_have_many :issue_changes, :through => :issues
42 should_have_many :issue_changes, :through => :issues
43 should_have_many :versions
43 should_have_many :versions
44 should_have_many :time_entries
44 should_have_many :time_entries
45 should_have_many :queries
45 should_have_many :queries
46 should_have_many :documents
46 should_have_many :documents
47 should_have_many :news
47 should_have_many :news
48 should_have_many :issue_categories
48 should_have_many :issue_categories
49 should_have_many :boards
49 should_have_many :boards
50 should_have_many :changesets, :through => :repository
50 should_have_many :changesets, :through => :repository
51
51
52 should_have_one :repository
52 should_have_one :repository
53 should_have_one :wiki
53 should_have_one :wiki
54
54
55 should_have_and_belong_to_many :trackers
55 should_have_and_belong_to_many :trackers
56 should_have_and_belong_to_many :issue_custom_fields
56 should_have_and_belong_to_many :issue_custom_fields
57 end
57 end
58
58
59 def test_truth
59 def test_truth
60 assert_kind_of Project, @ecookbook
60 assert_kind_of Project, @ecookbook
61 assert_equal "eCookbook", @ecookbook.name
61 assert_equal "eCookbook", @ecookbook.name
62 end
62 end
63
63
64 def test_update
64 def test_update
65 assert_equal "eCookbook", @ecookbook.name
65 assert_equal "eCookbook", @ecookbook.name
66 @ecookbook.name = "eCook"
66 @ecookbook.name = "eCook"
67 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
67 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
68 @ecookbook.reload
68 @ecookbook.reload
69 assert_equal "eCook", @ecookbook.name
69 assert_equal "eCook", @ecookbook.name
70 end
70 end
71
71
72 def test_validate_identifier
72 def test_validate_identifier
73 to_test = {"abc" => true,
73 to_test = {"abc" => true,
74 "ab12" => true,
74 "ab12" => true,
75 "ab-12" => true,
75 "ab-12" => true,
76 "12" => false,
76 "12" => false,
77 "new" => false}
77 "new" => false}
78
78
79 to_test.each do |identifier, valid|
79 to_test.each do |identifier, valid|
80 p = Project.new
80 p = Project.new
81 p.identifier = identifier
81 p.identifier = identifier
82 p.valid?
82 p.valid?
83 assert_equal valid, p.errors.on('identifier').nil?
83 assert_equal valid, p.errors.on('identifier').nil?
84 end
84 end
85 end
85 end
86
86
87 def test_members_should_be_active_users
87 def test_members_should_be_active_users
88 Project.all.each do |project|
88 Project.all.each do |project|
89 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
89 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
90 end
90 end
91 end
91 end
92
92
93 def test_users_should_be_active_users
93 def test_users_should_be_active_users
94 Project.all.each do |project|
94 Project.all.each do |project|
95 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
95 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
96 end
96 end
97 end
97 end
98
98
99 def test_archive
99 def test_archive
100 user = @ecookbook.members.first.user
100 user = @ecookbook.members.first.user
101 @ecookbook.archive
101 @ecookbook.archive
102 @ecookbook.reload
102 @ecookbook.reload
103
103
104 assert !@ecookbook.active?
104 assert !@ecookbook.active?
105 assert !user.projects.include?(@ecookbook)
105 assert !user.projects.include?(@ecookbook)
106 # Subproject are also archived
106 # Subproject are also archived
107 assert !@ecookbook.children.empty?
107 assert !@ecookbook.children.empty?
108 assert @ecookbook.descendants.active.empty?
108 assert @ecookbook.descendants.active.empty?
109 end
109 end
110
110
111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
112 # Assign an issue of a project to a version of a child project
112 # Assign an issue of a project to a version of a child project
113 Issue.find(4).update_attribute :fixed_version_id, 4
113 Issue.find(4).update_attribute :fixed_version_id, 4
114
114
115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 assert_equal false, @ecookbook.archive
116 assert_equal false, @ecookbook.archive
117 end
117 end
118 @ecookbook.reload
118 @ecookbook.reload
119 assert @ecookbook.active?
119 assert @ecookbook.active?
120 end
120 end
121
121
122 def test_unarchive
122 def test_unarchive
123 user = @ecookbook.members.first.user
123 user = @ecookbook.members.first.user
124 @ecookbook.archive
124 @ecookbook.archive
125 # A subproject of an archived project can not be unarchived
125 # A subproject of an archived project can not be unarchived
126 assert !@ecookbook_sub1.unarchive
126 assert !@ecookbook_sub1.unarchive
127
127
128 # Unarchive project
128 # Unarchive project
129 assert @ecookbook.unarchive
129 assert @ecookbook.unarchive
130 @ecookbook.reload
130 @ecookbook.reload
131 assert @ecookbook.active?
131 assert @ecookbook.active?
132 assert user.projects.include?(@ecookbook)
132 assert user.projects.include?(@ecookbook)
133 # Subproject can now be unarchived
133 # Subproject can now be unarchived
134 @ecookbook_sub1.reload
134 @ecookbook_sub1.reload
135 assert @ecookbook_sub1.unarchive
135 assert @ecookbook_sub1.unarchive
136 end
136 end
137
137
138 def test_destroy
138 def test_destroy
139 # 2 active members
139 # 2 active members
140 assert_equal 2, @ecookbook.members.size
140 assert_equal 2, @ecookbook.members.size
141 # and 1 is locked
141 # and 1 is locked
142 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
142 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
143 # some boards
143 # some boards
144 assert @ecookbook.boards.any?
144 assert @ecookbook.boards.any?
145
145
146 @ecookbook.destroy
146 @ecookbook.destroy
147 # make sure that the project non longer exists
147 # make sure that the project non longer exists
148 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
148 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
149 # make sure related data was removed
149 # make sure related data was removed
150 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
150 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
151 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
151 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
152 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
152 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
153 end
153 end
154
154
155 def test_move_an_orphan_project_to_a_root_project
155 def test_move_an_orphan_project_to_a_root_project
156 sub = Project.find(2)
156 sub = Project.find(2)
157 sub.set_parent! @ecookbook
157 sub.set_parent! @ecookbook
158 assert_equal @ecookbook.id, sub.parent.id
158 assert_equal @ecookbook.id, sub.parent.id
159 @ecookbook.reload
159 @ecookbook.reload
160 assert_equal 4, @ecookbook.children.size
160 assert_equal 4, @ecookbook.children.size
161 end
161 end
162
162
163 def test_move_an_orphan_project_to_a_subproject
163 def test_move_an_orphan_project_to_a_subproject
164 sub = Project.find(2)
164 sub = Project.find(2)
165 assert sub.set_parent!(@ecookbook_sub1)
165 assert sub.set_parent!(@ecookbook_sub1)
166 end
166 end
167
167
168 def test_move_a_root_project_to_a_project
168 def test_move_a_root_project_to_a_project
169 sub = @ecookbook
169 sub = @ecookbook
170 assert sub.set_parent!(Project.find(2))
170 assert sub.set_parent!(Project.find(2))
171 end
171 end
172
172
173 def test_should_not_move_a_project_to_its_children
173 def test_should_not_move_a_project_to_its_children
174 sub = @ecookbook
174 sub = @ecookbook
175 assert !(sub.set_parent!(Project.find(3)))
175 assert !(sub.set_parent!(Project.find(3)))
176 end
176 end
177
177
178 def test_set_parent_should_add_roots_in_alphabetical_order
178 def test_set_parent_should_add_roots_in_alphabetical_order
179 ProjectCustomField.delete_all
179 ProjectCustomField.delete_all
180 Project.delete_all
180 Project.delete_all
181 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
181 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
182 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
182 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
183 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
183 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
184 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
184 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
185
185
186 assert_equal 4, Project.count
186 assert_equal 4, Project.count
187 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
187 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
188 end
188 end
189
189
190 def test_set_parent_should_add_children_in_alphabetical_order
190 def test_set_parent_should_add_children_in_alphabetical_order
191 ProjectCustomField.delete_all
191 ProjectCustomField.delete_all
192 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
192 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
193 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
193 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
194 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
194 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
195 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
195 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
196 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
196 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
197
197
198 parent.reload
198 parent.reload
199 assert_equal 4, parent.children.size
199 assert_equal 4, parent.children.size
200 assert_equal parent.children.sort_by(&:name), parent.children
200 assert_equal parent.children.sort_by(&:name), parent.children
201 end
201 end
202
202
203 def test_rebuild_should_sort_children_alphabetically
203 def test_rebuild_should_sort_children_alphabetically
204 ProjectCustomField.delete_all
204 ProjectCustomField.delete_all
205 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
205 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
206 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
206 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
207 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
207 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
208 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
208 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
209 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
209 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
210
210
211 Project.update_all("lft = NULL, rgt = NULL")
211 Project.update_all("lft = NULL, rgt = NULL")
212 Project.rebuild!
212 Project.rebuild!
213
213
214 parent.reload
214 parent.reload
215 assert_equal 4, parent.children.size
215 assert_equal 4, parent.children.size
216 assert_equal parent.children.sort_by(&:name), parent.children
216 assert_equal parent.children.sort_by(&:name), parent.children
217 end
217 end
218
218
219
219
220 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
220 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
221 # Parent issue with a hierarchy project's fixed version
221 # Parent issue with a hierarchy project's fixed version
222 parent_issue = Issue.find(1)
222 parent_issue = Issue.find(1)
223 parent_issue.update_attribute(:fixed_version_id, 4)
223 parent_issue.update_attribute(:fixed_version_id, 4)
224 parent_issue.reload
224 parent_issue.reload
225 assert_equal 4, parent_issue.fixed_version_id
225 assert_equal 4, parent_issue.fixed_version_id
226
226
227 # Should keep fixed versions for the issues
227 # Should keep fixed versions for the issues
228 issue_with_local_fixed_version = Issue.find(5)
228 issue_with_local_fixed_version = Issue.find(5)
229 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
229 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
230 issue_with_local_fixed_version.reload
230 issue_with_local_fixed_version.reload
231 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
231 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
232
232
233 # Local issue with hierarchy fixed_version
233 # Local issue with hierarchy fixed_version
234 issue_with_hierarchy_fixed_version = Issue.find(13)
234 issue_with_hierarchy_fixed_version = Issue.find(13)
235 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
235 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
236 issue_with_hierarchy_fixed_version.reload
236 issue_with_hierarchy_fixed_version.reload
237 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
237 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
238
238
239 # Move project out of the issue's hierarchy
239 # Move project out of the issue's hierarchy
240 moved_project = Project.find(3)
240 moved_project = Project.find(3)
241 moved_project.set_parent!(Project.find(2))
241 moved_project.set_parent!(Project.find(2))
242 parent_issue.reload
242 parent_issue.reload
243 issue_with_local_fixed_version.reload
243 issue_with_local_fixed_version.reload
244 issue_with_hierarchy_fixed_version.reload
244 issue_with_hierarchy_fixed_version.reload
245
245
246 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
246 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
247 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
247 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
248 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
248 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
249 end
249 end
250
250
251 def test_parent
251 def test_parent
252 p = Project.find(6).parent
252 p = Project.find(6).parent
253 assert p.is_a?(Project)
253 assert p.is_a?(Project)
254 assert_equal 5, p.id
254 assert_equal 5, p.id
255 end
255 end
256
256
257 def test_ancestors
257 def test_ancestors
258 a = Project.find(6).ancestors
258 a = Project.find(6).ancestors
259 assert a.first.is_a?(Project)
259 assert a.first.is_a?(Project)
260 assert_equal [1, 5], a.collect(&:id)
260 assert_equal [1, 5], a.collect(&:id)
261 end
261 end
262
262
263 def test_root
263 def test_root
264 r = Project.find(6).root
264 r = Project.find(6).root
265 assert r.is_a?(Project)
265 assert r.is_a?(Project)
266 assert_equal 1, r.id
266 assert_equal 1, r.id
267 end
267 end
268
268
269 def test_children
269 def test_children
270 c = Project.find(1).children
270 c = Project.find(1).children
271 assert c.first.is_a?(Project)
271 assert c.first.is_a?(Project)
272 assert_equal [5, 3, 4], c.collect(&:id)
272 assert_equal [5, 3, 4], c.collect(&:id)
273 end
273 end
274
274
275 def test_descendants
275 def test_descendants
276 d = Project.find(1).descendants
276 d = Project.find(1).descendants
277 assert d.first.is_a?(Project)
277 assert d.first.is_a?(Project)
278 assert_equal [5, 6, 3, 4], d.collect(&:id)
278 assert_equal [5, 6, 3, 4], d.collect(&:id)
279 end
279 end
280
280
281 def test_allowed_parents_should_be_empty_for_non_member_user
281 def test_allowed_parents_should_be_empty_for_non_member_user
282 Role.non_member.add_permission!(:add_project)
282 Role.non_member.add_permission!(:add_project)
283 user = User.find(9)
283 user = User.find(9)
284 assert user.memberships.empty?
284 assert user.memberships.empty?
285 User.current = user
285 User.current = user
286 assert Project.new.allowed_parents.compact.empty?
286 assert Project.new.allowed_parents.compact.empty?
287 end
287 end
288
288
289 def test_allowed_parents_with_add_subprojects_permission
289 def test_allowed_parents_with_add_subprojects_permission
290 Role.find(1).remove_permission!(:add_project)
290 Role.find(1).remove_permission!(:add_project)
291 Role.find(1).add_permission!(:add_subprojects)
291 Role.find(1).add_permission!(:add_subprojects)
292 User.current = User.find(2)
292 User.current = User.find(2)
293 # new project
293 # new project
294 assert !Project.new.allowed_parents.include?(nil)
294 assert !Project.new.allowed_parents.include?(nil)
295 assert Project.new.allowed_parents.include?(Project.find(1))
295 assert Project.new.allowed_parents.include?(Project.find(1))
296 # existing root project
296 # existing root project
297 assert Project.find(1).allowed_parents.include?(nil)
297 assert Project.find(1).allowed_parents.include?(nil)
298 # existing child
298 # existing child
299 assert Project.find(3).allowed_parents.include?(Project.find(1))
299 assert Project.find(3).allowed_parents.include?(Project.find(1))
300 assert !Project.find(3).allowed_parents.include?(nil)
300 assert !Project.find(3).allowed_parents.include?(nil)
301 end
301 end
302
302
303 def test_allowed_parents_with_add_project_permission
303 def test_allowed_parents_with_add_project_permission
304 Role.find(1).add_permission!(:add_project)
304 Role.find(1).add_permission!(:add_project)
305 Role.find(1).remove_permission!(:add_subprojects)
305 Role.find(1).remove_permission!(:add_subprojects)
306 User.current = User.find(2)
306 User.current = User.find(2)
307 # new project
307 # new project
308 assert Project.new.allowed_parents.include?(nil)
308 assert Project.new.allowed_parents.include?(nil)
309 assert !Project.new.allowed_parents.include?(Project.find(1))
309 assert !Project.new.allowed_parents.include?(Project.find(1))
310 # existing root project
310 # existing root project
311 assert Project.find(1).allowed_parents.include?(nil)
311 assert Project.find(1).allowed_parents.include?(nil)
312 # existing child
312 # existing child
313 assert Project.find(3).allowed_parents.include?(Project.find(1))
313 assert Project.find(3).allowed_parents.include?(Project.find(1))
314 assert Project.find(3).allowed_parents.include?(nil)
314 assert Project.find(3).allowed_parents.include?(nil)
315 end
315 end
316
316
317 def test_allowed_parents_with_add_project_and_subprojects_permission
317 def test_allowed_parents_with_add_project_and_subprojects_permission
318 Role.find(1).add_permission!(:add_project)
318 Role.find(1).add_permission!(:add_project)
319 Role.find(1).add_permission!(:add_subprojects)
319 Role.find(1).add_permission!(:add_subprojects)
320 User.current = User.find(2)
320 User.current = User.find(2)
321 # new project
321 # new project
322 assert Project.new.allowed_parents.include?(nil)
322 assert Project.new.allowed_parents.include?(nil)
323 assert Project.new.allowed_parents.include?(Project.find(1))
323 assert Project.new.allowed_parents.include?(Project.find(1))
324 # existing root project
324 # existing root project
325 assert Project.find(1).allowed_parents.include?(nil)
325 assert Project.find(1).allowed_parents.include?(nil)
326 # existing child
326 # existing child
327 assert Project.find(3).allowed_parents.include?(Project.find(1))
327 assert Project.find(3).allowed_parents.include?(Project.find(1))
328 assert Project.find(3).allowed_parents.include?(nil)
328 assert Project.find(3).allowed_parents.include?(nil)
329 end
329 end
330
330
331 def test_users_by_role
331 def test_users_by_role
332 users_by_role = Project.find(1).users_by_role
332 users_by_role = Project.find(1).users_by_role
333 assert_kind_of Hash, users_by_role
333 assert_kind_of Hash, users_by_role
334 role = Role.find(1)
334 role = Role.find(1)
335 assert_kind_of Array, users_by_role[role]
335 assert_kind_of Array, users_by_role[role]
336 assert users_by_role[role].include?(User.find(2))
336 assert users_by_role[role].include?(User.find(2))
337 end
337 end
338
338
339 def test_rolled_up_trackers
339 def test_rolled_up_trackers
340 parent = Project.find(1)
340 parent = Project.find(1)
341 parent.trackers = Tracker.find([1,2])
341 parent.trackers = Tracker.find([1,2])
342 child = parent.children.find(3)
342 child = parent.children.find(3)
343
343
344 assert_equal [1, 2], parent.tracker_ids
344 assert_equal [1, 2], parent.tracker_ids
345 assert_equal [2, 3], child.trackers.collect(&:id)
345 assert_equal [2, 3], child.trackers.collect(&:id)
346
346
347 assert_kind_of Tracker, parent.rolled_up_trackers.first
347 assert_kind_of Tracker, parent.rolled_up_trackers.first
348 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
348 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
349
349
350 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
350 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
351 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
351 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
352 end
352 end
353
353
354 def test_rolled_up_trackers_should_ignore_archived_subprojects
354 def test_rolled_up_trackers_should_ignore_archived_subprojects
355 parent = Project.find(1)
355 parent = Project.find(1)
356 parent.trackers = Tracker.find([1,2])
356 parent.trackers = Tracker.find([1,2])
357 child = parent.children.find(3)
357 child = parent.children.find(3)
358 child.trackers = Tracker.find([1,3])
358 child.trackers = Tracker.find([1,3])
359 parent.children.each(&:archive)
359 parent.children.each(&:archive)
360
360
361 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
361 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
362 end
362 end
363
364 context "#rolled_up_versions" do
365 setup do
366 @project = Project.generate!
367 @parent_version_1 = Version.generate!(:project => @project)
368 @parent_version_2 = Version.generate!(:project => @project)
369 end
370
371 should "include the versions for the current project" do
372 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
373 end
374
375 should "include versions for a subproject" do
376 @subproject = Project.generate!
377 @subproject.set_parent!(@project)
378 @subproject_version = Version.generate!(:project => @subproject)
379
380 assert_same_elements [
381 @parent_version_1,
382 @parent_version_2,
383 @subproject_version
384 ], @project.rolled_up_versions
385 end
386
387 should "include versions for a sub-subproject" do
388 @subproject = Project.generate!
389 @subproject.set_parent!(@project)
390 @sub_subproject = Project.generate!
391 @sub_subproject.set_parent!(@subproject)
392 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
393
394 @project.reload
395
396 assert_same_elements [
397 @parent_version_1,
398 @parent_version_2,
399 @sub_subproject_version
400 ], @project.rolled_up_versions
401 end
402
403
404 should "only check active projects" do
405 @subproject = Project.generate!
406 @subproject.set_parent!(@project)
407 @subproject_version = Version.generate!(:project => @subproject)
408 assert @subproject.archive
409
410 @project.reload
411
412 assert !@subproject.active?
413 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
414 end
415 end
363
416
364 def test_shared_versions_none_sharing
417 def test_shared_versions_none_sharing
365 p = Project.find(5)
418 p = Project.find(5)
366 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
419 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
367 assert p.shared_versions.include?(v)
420 assert p.shared_versions.include?(v)
368 assert !p.children.first.shared_versions.include?(v)
421 assert !p.children.first.shared_versions.include?(v)
369 assert !p.root.shared_versions.include?(v)
422 assert !p.root.shared_versions.include?(v)
370 assert !p.siblings.first.shared_versions.include?(v)
423 assert !p.siblings.first.shared_versions.include?(v)
371 assert !p.root.siblings.first.shared_versions.include?(v)
424 assert !p.root.siblings.first.shared_versions.include?(v)
372 end
425 end
373
426
374 def test_shared_versions_descendants_sharing
427 def test_shared_versions_descendants_sharing
375 p = Project.find(5)
428 p = Project.find(5)
376 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
429 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
377 assert p.shared_versions.include?(v)
430 assert p.shared_versions.include?(v)
378 assert p.children.first.shared_versions.include?(v)
431 assert p.children.first.shared_versions.include?(v)
379 assert !p.root.shared_versions.include?(v)
432 assert !p.root.shared_versions.include?(v)
380 assert !p.siblings.first.shared_versions.include?(v)
433 assert !p.siblings.first.shared_versions.include?(v)
381 assert !p.root.siblings.first.shared_versions.include?(v)
434 assert !p.root.siblings.first.shared_versions.include?(v)
382 end
435 end
383
436
384 def test_shared_versions_hierarchy_sharing
437 def test_shared_versions_hierarchy_sharing
385 p = Project.find(5)
438 p = Project.find(5)
386 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
439 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
387 assert p.shared_versions.include?(v)
440 assert p.shared_versions.include?(v)
388 assert p.children.first.shared_versions.include?(v)
441 assert p.children.first.shared_versions.include?(v)
389 assert p.root.shared_versions.include?(v)
442 assert p.root.shared_versions.include?(v)
390 assert !p.siblings.first.shared_versions.include?(v)
443 assert !p.siblings.first.shared_versions.include?(v)
391 assert !p.root.siblings.first.shared_versions.include?(v)
444 assert !p.root.siblings.first.shared_versions.include?(v)
392 end
445 end
393
446
394 def test_shared_versions_tree_sharing
447 def test_shared_versions_tree_sharing
395 p = Project.find(5)
448 p = Project.find(5)
396 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
449 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
397 assert p.shared_versions.include?(v)
450 assert p.shared_versions.include?(v)
398 assert p.children.first.shared_versions.include?(v)
451 assert p.children.first.shared_versions.include?(v)
399 assert p.root.shared_versions.include?(v)
452 assert p.root.shared_versions.include?(v)
400 assert p.siblings.first.shared_versions.include?(v)
453 assert p.siblings.first.shared_versions.include?(v)
401 assert !p.root.siblings.first.shared_versions.include?(v)
454 assert !p.root.siblings.first.shared_versions.include?(v)
402 end
455 end
403
456
404 def test_shared_versions_system_sharing
457 def test_shared_versions_system_sharing
405 p = Project.find(5)
458 p = Project.find(5)
406 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
459 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
407 assert p.shared_versions.include?(v)
460 assert p.shared_versions.include?(v)
408 assert p.children.first.shared_versions.include?(v)
461 assert p.children.first.shared_versions.include?(v)
409 assert p.root.shared_versions.include?(v)
462 assert p.root.shared_versions.include?(v)
410 assert p.siblings.first.shared_versions.include?(v)
463 assert p.siblings.first.shared_versions.include?(v)
411 assert p.root.siblings.first.shared_versions.include?(v)
464 assert p.root.siblings.first.shared_versions.include?(v)
412 end
465 end
413
466
414 def test_shared_versions
467 def test_shared_versions
415 parent = Project.find(1)
468 parent = Project.find(1)
416 child = parent.children.find(3)
469 child = parent.children.find(3)
417 private_child = parent.children.find(5)
470 private_child = parent.children.find(5)
418
471
419 assert_equal [1,2,3], parent.version_ids.sort
472 assert_equal [1,2,3], parent.version_ids.sort
420 assert_equal [4], child.version_ids
473 assert_equal [4], child.version_ids
421 assert_equal [6], private_child.version_ids
474 assert_equal [6], private_child.version_ids
422 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
475 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
423
476
424 assert_equal 6, parent.shared_versions.size
477 assert_equal 6, parent.shared_versions.size
425 parent.shared_versions.each do |version|
478 parent.shared_versions.each do |version|
426 assert_kind_of Version, version
479 assert_kind_of Version, version
427 end
480 end
428
481
429 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
482 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
430 end
483 end
431
484
432 def test_shared_versions_should_ignore_archived_subprojects
485 def test_shared_versions_should_ignore_archived_subprojects
433 parent = Project.find(1)
486 parent = Project.find(1)
434 child = parent.children.find(3)
487 child = parent.children.find(3)
435 child.archive
488 child.archive
436 parent.reload
489 parent.reload
437
490
438 assert_equal [1,2,3], parent.version_ids.sort
491 assert_equal [1,2,3], parent.version_ids.sort
439 assert_equal [4], child.version_ids
492 assert_equal [4], child.version_ids
440 assert !parent.shared_versions.collect(&:id).include?(4)
493 assert !parent.shared_versions.collect(&:id).include?(4)
441 end
494 end
442
495
443 def test_shared_versions_visible_to_user
496 def test_shared_versions_visible_to_user
444 user = User.find(3)
497 user = User.find(3)
445 parent = Project.find(1)
498 parent = Project.find(1)
446 child = parent.children.find(5)
499 child = parent.children.find(5)
447
500
448 assert_equal [1,2,3], parent.version_ids.sort
501 assert_equal [1,2,3], parent.version_ids.sort
449 assert_equal [6], child.version_ids
502 assert_equal [6], child.version_ids
450
503
451 versions = parent.shared_versions.visible(user)
504 versions = parent.shared_versions.visible(user)
452
505
453 assert_equal 4, versions.size
506 assert_equal 4, versions.size
454 versions.each do |version|
507 versions.each do |version|
455 assert_kind_of Version, version
508 assert_kind_of Version, version
456 end
509 end
457
510
458 assert !versions.collect(&:id).include?(6)
511 assert !versions.collect(&:id).include?(6)
459 end
512 end
460
513
461
514
462 def test_next_identifier
515 def test_next_identifier
463 ProjectCustomField.delete_all
516 ProjectCustomField.delete_all
464 Project.create!(:name => 'last', :identifier => 'p2008040')
517 Project.create!(:name => 'last', :identifier => 'p2008040')
465 assert_equal 'p2008041', Project.next_identifier
518 assert_equal 'p2008041', Project.next_identifier
466 end
519 end
467
520
468 def test_next_identifier_first_project
521 def test_next_identifier_first_project
469 Project.delete_all
522 Project.delete_all
470 assert_nil Project.next_identifier
523 assert_nil Project.next_identifier
471 end
524 end
472
525
473
526
474 def test_enabled_module_names_should_not_recreate_enabled_modules
527 def test_enabled_module_names_should_not_recreate_enabled_modules
475 project = Project.find(1)
528 project = Project.find(1)
476 # Remove one module
529 # Remove one module
477 modules = project.enabled_modules.slice(0..-2)
530 modules = project.enabled_modules.slice(0..-2)
478 assert modules.any?
531 assert modules.any?
479 assert_difference 'EnabledModule.count', -1 do
532 assert_difference 'EnabledModule.count', -1 do
480 project.enabled_module_names = modules.collect(&:name)
533 project.enabled_module_names = modules.collect(&:name)
481 end
534 end
482 project.reload
535 project.reload
483 # Ids should be preserved
536 # Ids should be preserved
484 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
537 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
485 end
538 end
486
539
487 def test_copy_from_existing_project
540 def test_copy_from_existing_project
488 source_project = Project.find(1)
541 source_project = Project.find(1)
489 copied_project = Project.copy_from(1)
542 copied_project = Project.copy_from(1)
490
543
491 assert copied_project
544 assert copied_project
492 # Cleared attributes
545 # Cleared attributes
493 assert copied_project.id.blank?
546 assert copied_project.id.blank?
494 assert copied_project.name.blank?
547 assert copied_project.name.blank?
495 assert copied_project.identifier.blank?
548 assert copied_project.identifier.blank?
496
549
497 # Duplicated attributes
550 # Duplicated attributes
498 assert_equal source_project.description, copied_project.description
551 assert_equal source_project.description, copied_project.description
499 assert_equal source_project.enabled_modules, copied_project.enabled_modules
552 assert_equal source_project.enabled_modules, copied_project.enabled_modules
500 assert_equal source_project.trackers, copied_project.trackers
553 assert_equal source_project.trackers, copied_project.trackers
501
554
502 # Default attributes
555 # Default attributes
503 assert_equal 1, copied_project.status
556 assert_equal 1, copied_project.status
504 end
557 end
505
558
506 def test_activities_should_use_the_system_activities
559 def test_activities_should_use_the_system_activities
507 project = Project.find(1)
560 project = Project.find(1)
508 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
561 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
509 end
562 end
510
563
511
564
512 def test_activities_should_use_the_project_specific_activities
565 def test_activities_should_use_the_project_specific_activities
513 project = Project.find(1)
566 project = Project.find(1)
514 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
567 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
515 assert overridden_activity.save!
568 assert overridden_activity.save!
516
569
517 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
570 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
518 end
571 end
519
572
520 def test_activities_should_not_include_the_inactive_project_specific_activities
573 def test_activities_should_not_include_the_inactive_project_specific_activities
521 project = Project.find(1)
574 project = Project.find(1)
522 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
575 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
523 assert overridden_activity.save!
576 assert overridden_activity.save!
524
577
525 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
578 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
526 end
579 end
527
580
528 def test_activities_should_not_include_project_specific_activities_from_other_projects
581 def test_activities_should_not_include_project_specific_activities_from_other_projects
529 project = Project.find(1)
582 project = Project.find(1)
530 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
583 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
531 assert overridden_activity.save!
584 assert overridden_activity.save!
532
585
533 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
586 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
534 end
587 end
535
588
536 def test_activities_should_handle_nils
589 def test_activities_should_handle_nils
537 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
590 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
538 TimeEntryActivity.delete_all
591 TimeEntryActivity.delete_all
539
592
540 # No activities
593 # No activities
541 project = Project.find(1)
594 project = Project.find(1)
542 assert project.activities.empty?
595 assert project.activities.empty?
543
596
544 # No system, one overridden
597 # No system, one overridden
545 assert overridden_activity.save!
598 assert overridden_activity.save!
546 project.reload
599 project.reload
547 assert_equal [overridden_activity], project.activities
600 assert_equal [overridden_activity], project.activities
548 end
601 end
549
602
550 def test_activities_should_override_system_activities_with_project_activities
603 def test_activities_should_override_system_activities_with_project_activities
551 project = Project.find(1)
604 project = Project.find(1)
552 parent_activity = TimeEntryActivity.find(:first)
605 parent_activity = TimeEntryActivity.find(:first)
553 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
606 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
554 assert overridden_activity.save!
607 assert overridden_activity.save!
555
608
556 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
609 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
557 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
610 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
558 end
611 end
559
612
560 def test_activities_should_include_inactive_activities_if_specified
613 def test_activities_should_include_inactive_activities_if_specified
561 project = Project.find(1)
614 project = Project.find(1)
562 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
615 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
563 assert overridden_activity.save!
616 assert overridden_activity.save!
564
617
565 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
618 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
566 end
619 end
567
620
568 test 'activities should not include active System activities if the project has an override that is inactive' do
621 test 'activities should not include active System activities if the project has an override that is inactive' do
569 project = Project.find(1)
622 project = Project.find(1)
570 system_activity = TimeEntryActivity.find_by_name('Design')
623 system_activity = TimeEntryActivity.find_by_name('Design')
571 assert system_activity.active?
624 assert system_activity.active?
572 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
625 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
573 assert overridden_activity.save!
626 assert overridden_activity.save!
574
627
575 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
628 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
576 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
629 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
577 end
630 end
578
631
579 def test_close_completed_versions
632 def test_close_completed_versions
580 Version.update_all("status = 'open'")
633 Version.update_all("status = 'open'")
581 project = Project.find(1)
634 project = Project.find(1)
582 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
635 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
583 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
636 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
584 project.close_completed_versions
637 project.close_completed_versions
585 project.reload
638 project.reload
586 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
639 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
587 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
640 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
588 end
641 end
589
642
590 context "Project#copy" do
643 context "Project#copy" do
591 setup do
644 setup do
592 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
645 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
593 Project.destroy_all :identifier => "copy-test"
646 Project.destroy_all :identifier => "copy-test"
594 @source_project = Project.find(2)
647 @source_project = Project.find(2)
595 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
648 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
596 @project.trackers = @source_project.trackers
649 @project.trackers = @source_project.trackers
597 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
650 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
598 end
651 end
599
652
600 should "copy issues" do
653 should "copy issues" do
601 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
654 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
602 :subject => "copy issue status",
655 :subject => "copy issue status",
603 :tracker_id => 1,
656 :tracker_id => 1,
604 :assigned_to_id => 2,
657 :assigned_to_id => 2,
605 :project_id => @source_project.id)
658 :project_id => @source_project.id)
606 assert @project.valid?
659 assert @project.valid?
607 assert @project.issues.empty?
660 assert @project.issues.empty?
608 assert @project.copy(@source_project)
661 assert @project.copy(@source_project)
609
662
610 assert_equal @source_project.issues.size, @project.issues.size
663 assert_equal @source_project.issues.size, @project.issues.size
611 @project.issues.each do |issue|
664 @project.issues.each do |issue|
612 assert issue.valid?
665 assert issue.valid?
613 assert ! issue.assigned_to.blank?
666 assert ! issue.assigned_to.blank?
614 assert_equal @project, issue.project
667 assert_equal @project, issue.project
615 end
668 end
616
669
617 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
670 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
618 assert copied_issue
671 assert copied_issue
619 assert copied_issue.status
672 assert copied_issue.status
620 assert_equal "Closed", copied_issue.status.name
673 assert_equal "Closed", copied_issue.status.name
621 end
674 end
622
675
623 should "change the new issues to use the copied version" do
676 should "change the new issues to use the copied version" do
624 User.current = User.find(1)
677 User.current = User.find(1)
625 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
678 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
626 @source_project.versions << assigned_version
679 @source_project.versions << assigned_version
627 assert_equal 3, @source_project.versions.size
680 assert_equal 3, @source_project.versions.size
628 Issue.generate_for_project!(@source_project,
681 Issue.generate_for_project!(@source_project,
629 :fixed_version_id => assigned_version.id,
682 :fixed_version_id => assigned_version.id,
630 :subject => "change the new issues to use the copied version",
683 :subject => "change the new issues to use the copied version",
631 :tracker_id => 1,
684 :tracker_id => 1,
632 :project_id => @source_project.id)
685 :project_id => @source_project.id)
633
686
634 assert @project.copy(@source_project)
687 assert @project.copy(@source_project)
635 @project.reload
688 @project.reload
636 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
689 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
637
690
638 assert copied_issue
691 assert copied_issue
639 assert copied_issue.fixed_version
692 assert copied_issue.fixed_version
640 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
693 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
641 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
694 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
642 end
695 end
643
696
644 should "copy issue relations" do
697 should "copy issue relations" do
645 Setting.cross_project_issue_relations = '1'
698 Setting.cross_project_issue_relations = '1'
646
699
647 second_issue = Issue.generate!(:status_id => 5,
700 second_issue = Issue.generate!(:status_id => 5,
648 :subject => "copy issue relation",
701 :subject => "copy issue relation",
649 :tracker_id => 1,
702 :tracker_id => 1,
650 :assigned_to_id => 2,
703 :assigned_to_id => 2,
651 :project_id => @source_project.id)
704 :project_id => @source_project.id)
652 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
705 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
653 :issue_to => second_issue,
706 :issue_to => second_issue,
654 :relation_type => "relates")
707 :relation_type => "relates")
655 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
708 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
656 :issue_to => second_issue,
709 :issue_to => second_issue,
657 :relation_type => "duplicates")
710 :relation_type => "duplicates")
658
711
659 assert @project.copy(@source_project)
712 assert @project.copy(@source_project)
660 assert_equal @source_project.issues.count, @project.issues.count
713 assert_equal @source_project.issues.count, @project.issues.count
661 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
714 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
662 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
715 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
663
716
664 # First issue with a relation on project
717 # First issue with a relation on project
665 assert_equal 1, copied_issue.relations.size, "Relation not copied"
718 assert_equal 1, copied_issue.relations.size, "Relation not copied"
666 copied_relation = copied_issue.relations.first
719 copied_relation = copied_issue.relations.first
667 assert_equal "relates", copied_relation.relation_type
720 assert_equal "relates", copied_relation.relation_type
668 assert_equal copied_second_issue.id, copied_relation.issue_to_id
721 assert_equal copied_second_issue.id, copied_relation.issue_to_id
669 assert_not_equal source_relation.id, copied_relation.id
722 assert_not_equal source_relation.id, copied_relation.id
670
723
671 # Second issue with a cross project relation
724 # Second issue with a cross project relation
672 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
725 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
673 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
726 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
674 assert_equal "duplicates", copied_relation.relation_type
727 assert_equal "duplicates", copied_relation.relation_type
675 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
728 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
676 assert_not_equal source_relation_cross_project.id, copied_relation.id
729 assert_not_equal source_relation_cross_project.id, copied_relation.id
677 end
730 end
678
731
679 should "copy memberships" do
732 should "copy memberships" do
680 assert @project.valid?
733 assert @project.valid?
681 assert @project.members.empty?
734 assert @project.members.empty?
682 assert @project.copy(@source_project)
735 assert @project.copy(@source_project)
683
736
684 assert_equal @source_project.memberships.size, @project.memberships.size
737 assert_equal @source_project.memberships.size, @project.memberships.size
685 @project.memberships.each do |membership|
738 @project.memberships.each do |membership|
686 assert membership
739 assert membership
687 assert_equal @project, membership.project
740 assert_equal @project, membership.project
688 end
741 end
689 end
742 end
690
743
691 should "copy project specific queries" do
744 should "copy project specific queries" do
692 assert @project.valid?
745 assert @project.valid?
693 assert @project.queries.empty?
746 assert @project.queries.empty?
694 assert @project.copy(@source_project)
747 assert @project.copy(@source_project)
695
748
696 assert_equal @source_project.queries.size, @project.queries.size
749 assert_equal @source_project.queries.size, @project.queries.size
697 @project.queries.each do |query|
750 @project.queries.each do |query|
698 assert query
751 assert query
699 assert_equal @project, query.project
752 assert_equal @project, query.project
700 end
753 end
701 end
754 end
702
755
703 should "copy versions" do
756 should "copy versions" do
704 @source_project.versions << Version.generate!
757 @source_project.versions << Version.generate!
705 @source_project.versions << Version.generate!
758 @source_project.versions << Version.generate!
706
759
707 assert @project.versions.empty?
760 assert @project.versions.empty?
708 assert @project.copy(@source_project)
761 assert @project.copy(@source_project)
709
762
710 assert_equal @source_project.versions.size, @project.versions.size
763 assert_equal @source_project.versions.size, @project.versions.size
711 @project.versions.each do |version|
764 @project.versions.each do |version|
712 assert version
765 assert version
713 assert_equal @project, version.project
766 assert_equal @project, version.project
714 end
767 end
715 end
768 end
716
769
717 should "copy wiki" do
770 should "copy wiki" do
718 assert_difference 'Wiki.count' do
771 assert_difference 'Wiki.count' do
719 assert @project.copy(@source_project)
772 assert @project.copy(@source_project)
720 end
773 end
721
774
722 assert @project.wiki
775 assert @project.wiki
723 assert_not_equal @source_project.wiki, @project.wiki
776 assert_not_equal @source_project.wiki, @project.wiki
724 assert_equal "Start page", @project.wiki.start_page
777 assert_equal "Start page", @project.wiki.start_page
725 end
778 end
726
779
727 should "copy wiki pages and content with hierarchy" do
780 should "copy wiki pages and content with hierarchy" do
728 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
781 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
729 assert @project.copy(@source_project)
782 assert @project.copy(@source_project)
730 end
783 end
731
784
732 assert @project.wiki
785 assert @project.wiki
733 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
786 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
734
787
735 @project.wiki.pages.each do |wiki_page|
788 @project.wiki.pages.each do |wiki_page|
736 assert wiki_page.content
789 assert wiki_page.content
737 assert !@source_project.wiki.pages.include?(wiki_page)
790 assert !@source_project.wiki.pages.include?(wiki_page)
738 end
791 end
739
792
740 parent = @project.wiki.find_page('Parent_page')
793 parent = @project.wiki.find_page('Parent_page')
741 child1 = @project.wiki.find_page('Child_page_1')
794 child1 = @project.wiki.find_page('Child_page_1')
742 child2 = @project.wiki.find_page('Child_page_2')
795 child2 = @project.wiki.find_page('Child_page_2')
743 assert_equal parent, child1.parent
796 assert_equal parent, child1.parent
744 assert_equal parent, child2.parent
797 assert_equal parent, child2.parent
745 end
798 end
746
799
747 should "copy issue categories" do
800 should "copy issue categories" do
748 assert @project.copy(@source_project)
801 assert @project.copy(@source_project)
749
802
750 assert_equal 2, @project.issue_categories.size
803 assert_equal 2, @project.issue_categories.size
751 @project.issue_categories.each do |issue_category|
804 @project.issue_categories.each do |issue_category|
752 assert !@source_project.issue_categories.include?(issue_category)
805 assert !@source_project.issue_categories.include?(issue_category)
753 end
806 end
754 end
807 end
755
808
756 should "copy boards" do
809 should "copy boards" do
757 assert @project.copy(@source_project)
810 assert @project.copy(@source_project)
758
811
759 assert_equal 1, @project.boards.size
812 assert_equal 1, @project.boards.size
760 @project.boards.each do |board|
813 @project.boards.each do |board|
761 assert !@source_project.boards.include?(board)
814 assert !@source_project.boards.include?(board)
762 end
815 end
763 end
816 end
764
817
765 should "change the new issues to use the copied issue categories" do
818 should "change the new issues to use the copied issue categories" do
766 issue = Issue.find(4)
819 issue = Issue.find(4)
767 issue.update_attribute(:category_id, 3)
820 issue.update_attribute(:category_id, 3)
768
821
769 assert @project.copy(@source_project)
822 assert @project.copy(@source_project)
770
823
771 @project.issues.each do |issue|
824 @project.issues.each do |issue|
772 assert issue.category
825 assert issue.category
773 assert_equal "Stock management", issue.category.name # Same name
826 assert_equal "Stock management", issue.category.name # Same name
774 assert_not_equal IssueCategory.find(3), issue.category # Different record
827 assert_not_equal IssueCategory.find(3), issue.category # Different record
775 end
828 end
776 end
829 end
777
830
778 should "limit copy with :only option" do
831 should "limit copy with :only option" do
779 assert @project.members.empty?
832 assert @project.members.empty?
780 assert @project.issue_categories.empty?
833 assert @project.issue_categories.empty?
781 assert @source_project.issues.any?
834 assert @source_project.issues.any?
782
835
783 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
836 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
784
837
785 assert @project.members.any?
838 assert @project.members.any?
786 assert @project.issue_categories.any?
839 assert @project.issue_categories.any?
787 assert @project.issues.empty?
840 assert @project.issues.empty?
788 end
841 end
789
842
790 end
843 end
791
844
792 end
845 end
General Comments 0
You need to be logged in to leave comments. Login now