##// END OF EJS Templates
Moves project attributes default assignments from ProjectsController#new to the model (#6064)....
Jean-Philippe Lang -
r4346:9284a32c9ac7
parent child
Show More
@@ -1,272 +1,267
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 :roadmap, :only => :roadmap
20 menu_item :roadmap, :only => :roadmap
21 menu_item :settings, :only => :settings
21 menu_item :settings, :only => :settings
22
22
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 before_filter :authorize_global, :only => [:new, :create]
25 before_filter :authorize_global, :only => [:new, :create]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 accept_key_auth :index, :show, :create, :update, :destroy
27 accept_key_auth :index, :show, :create, :update, :destroy
28
28
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
29 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 if controller.request.post?
30 if controller.request.post?
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
31 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
32 end
32 end
33 end
33 end
34
34
35 # TODO: convert to PUT only
35 # TODO: convert to PUT only
36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
36 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
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.api {
55 format.api {
56 @projects = Project.visible.find(:all, :order => 'lft')
56 @projects = Project.visible.find(:all, :order => 'lft')
57 render :template => 'projects/index.apit'
57 render :template => 'projects/index.apit'
58 }
58 }
59 format.atom {
59 format.atom {
60 projects = Project.visible.find(:all, :order => 'created_on DESC',
60 projects = Project.visible.find(:all, :order => 'created_on DESC',
61 :limit => Setting.feeds_limit.to_i)
61 :limit => Setting.feeds_limit.to_i)
62 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
62 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
63 }
63 }
64 end
64 end
65 end
65 end
66
66
67 def new
67 def new
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
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 @project.trackers = Tracker.all
74 @project.is_public = Setting.default_projects_public?
75 @project.enabled_module_names = Setting.default_projects_modules
76 end
71 end
77
72
78 def create
73 def create
79 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
74 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
80 @trackers = Tracker.all
75 @trackers = Tracker.all
81 @project = Project.new(params[:project])
76 @project = Project.new(params[:project])
82
77
83 @project.enabled_module_names = params[:enabled_modules]
78 @project.enabled_module_names = params[:enabled_modules] if params[:enabled_modules]
84 if validate_parent_id && @project.save
79 if validate_parent_id && @project.save
85 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
80 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
86 # Add current user as a project member if he is not admin
81 # Add current user as a project member if he is not admin
87 unless User.current.admin?
82 unless User.current.admin?
88 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
83 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
89 m = Member.new(:user => User.current, :roles => [r])
84 m = Member.new(:user => User.current, :roles => [r])
90 @project.members << m
85 @project.members << m
91 end
86 end
92 respond_to do |format|
87 respond_to do |format|
93 format.html {
88 format.html {
94 flash[:notice] = l(:notice_successful_create)
89 flash[:notice] = l(:notice_successful_create)
95 redirect_to :controller => 'projects', :action => 'settings', :id => @project
90 redirect_to :controller => 'projects', :action => 'settings', :id => @project
96 }
91 }
97 format.api { render :template => 'projects/show.apit', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 format.api { render :template => 'projects/show.apit', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
98 end
93 end
99 else
94 else
100 respond_to do |format|
95 respond_to do |format|
101 format.html { render :action => 'new' }
96 format.html { render :action => 'new' }
102 format.api { render_validation_errors(@project) }
97 format.api { render_validation_errors(@project) }
103 end
98 end
104 end
99 end
105
100
106 end
101 end
107
102
108 def copy
103 def copy
109 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
104 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
110 @trackers = Tracker.all
105 @trackers = Tracker.all
111 @root_projects = Project.find(:all,
106 @root_projects = Project.find(:all,
112 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
107 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
113 :order => 'name')
108 :order => 'name')
114 @source_project = Project.find(params[:id])
109 @source_project = Project.find(params[:id])
115 if request.get?
110 if request.get?
116 @project = Project.copy_from(@source_project)
111 @project = Project.copy_from(@source_project)
117 if @project
112 if @project
118 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
113 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
119 else
114 else
120 redirect_to :controller => 'admin', :action => 'projects'
115 redirect_to :controller => 'admin', :action => 'projects'
121 end
116 end
122 else
117 else
123 Mailer.with_deliveries(params[:notifications] == '1') do
118 Mailer.with_deliveries(params[:notifications] == '1') do
124 @project = Project.new(params[:project])
119 @project = Project.new(params[:project])
125 @project.enabled_module_names = params[:enabled_modules]
120 @project.enabled_module_names = params[:enabled_modules]
126 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
121 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
127 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
128 flash[:notice] = l(:notice_successful_create)
123 flash[:notice] = l(:notice_successful_create)
129 redirect_to :controller => 'projects', :action => 'settings'
124 redirect_to :controller => 'projects', :action => 'settings'
130 elsif !@project.new_record?
125 elsif !@project.new_record?
131 # Project was created
126 # Project was created
132 # But some objects were not copied due to validation failures
127 # But some objects were not copied due to validation failures
133 # (eg. issues from disabled trackers)
128 # (eg. issues from disabled trackers)
134 # TODO: inform about that
129 # TODO: inform about that
135 redirect_to :controller => 'projects', :action => 'settings'
130 redirect_to :controller => 'projects', :action => 'settings'
136 end
131 end
137 end
132 end
138 end
133 end
139 rescue ActiveRecord::RecordNotFound
134 rescue ActiveRecord::RecordNotFound
140 redirect_to :controller => 'admin', :action => 'projects'
135 redirect_to :controller => 'admin', :action => 'projects'
141 end
136 end
142
137
143 # Show @project
138 # Show @project
144 def show
139 def show
145 if params[:jump]
140 if params[:jump]
146 # try to redirect to the requested menu item
141 # try to redirect to the requested menu item
147 redirect_to_project_menu_item(@project, params[:jump]) && return
142 redirect_to_project_menu_item(@project, params[:jump]) && return
148 end
143 end
149
144
150 @users_by_role = @project.users_by_role
145 @users_by_role = @project.users_by_role
151 @subprojects = @project.children.visible
146 @subprojects = @project.children.visible
152 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
147 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
153 @trackers = @project.rolled_up_trackers
148 @trackers = @project.rolled_up_trackers
154
149
155 cond = @project.project_condition(Setting.display_subprojects_issues?)
150 cond = @project.project_condition(Setting.display_subprojects_issues?)
156
151
157 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
152 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
158 :include => [:project, :status, :tracker],
153 :include => [:project, :status, :tracker],
159 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
154 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
160 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
155 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
161 :include => [:project, :status, :tracker],
156 :include => [:project, :status, :tracker],
162 :conditions => cond)
157 :conditions => cond)
163
158
164 TimeEntry.visible_by(User.current) do
159 TimeEntry.visible_by(User.current) do
165 @total_hours = TimeEntry.sum(:hours,
160 @total_hours = TimeEntry.sum(:hours,
166 :include => :project,
161 :include => :project,
167 :conditions => cond).to_f
162 :conditions => cond).to_f
168 end
163 end
169 @key = User.current.rss_key
164 @key = User.current.rss_key
170
165
171 respond_to do |format|
166 respond_to do |format|
172 format.html
167 format.html
173 format.api { render :template => 'projects/show.apit'}
168 format.api { render :template => 'projects/show.apit'}
174 end
169 end
175 end
170 end
176
171
177 def settings
172 def settings
178 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
173 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
179 @issue_category ||= IssueCategory.new
174 @issue_category ||= IssueCategory.new
180 @member ||= @project.members.new
175 @member ||= @project.members.new
181 @trackers = Tracker.all
176 @trackers = Tracker.all
182 @repository ||= @project.repository
177 @repository ||= @project.repository
183 @wiki ||= @project.wiki
178 @wiki ||= @project.wiki
184 end
179 end
185
180
186 def edit
181 def edit
187 end
182 end
188
183
189 def update
184 def update
190 @project.attributes = params[:project]
185 @project.attributes = params[:project]
191 if validate_parent_id && @project.save
186 if validate_parent_id && @project.save
192 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
187 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
193 respond_to do |format|
188 respond_to do |format|
194 format.html {
189 format.html {
195 flash[:notice] = l(:notice_successful_update)
190 flash[:notice] = l(:notice_successful_update)
196 redirect_to :action => 'settings', :id => @project
191 redirect_to :action => 'settings', :id => @project
197 }
192 }
198 format.api { head :ok }
193 format.api { head :ok }
199 end
194 end
200 else
195 else
201 respond_to do |format|
196 respond_to do |format|
202 format.html {
197 format.html {
203 settings
198 settings
204 render :action => 'settings'
199 render :action => 'settings'
205 }
200 }
206 format.api { render_validation_errors(@project) }
201 format.api { render_validation_errors(@project) }
207 end
202 end
208 end
203 end
209 end
204 end
210
205
211 def modules
206 def modules
212 @project.enabled_module_names = params[:enabled_modules]
207 @project.enabled_module_names = params[:enabled_modules]
213 flash[:notice] = l(:notice_successful_update)
208 flash[:notice] = l(:notice_successful_update)
214 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
209 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
215 end
210 end
216
211
217 def archive
212 def archive
218 if request.post?
213 if request.post?
219 unless @project.archive
214 unless @project.archive
220 flash[:error] = l(:error_can_not_archive_project)
215 flash[:error] = l(:error_can_not_archive_project)
221 end
216 end
222 end
217 end
223 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
218 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
224 end
219 end
225
220
226 def unarchive
221 def unarchive
227 @project.unarchive if request.post? && !@project.active?
222 @project.unarchive if request.post? && !@project.active?
228 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
223 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
229 end
224 end
230
225
231 # Delete @project
226 # Delete @project
232 def destroy
227 def destroy
233 @project_to_destroy = @project
228 @project_to_destroy = @project
234 if request.get?
229 if request.get?
235 # display confirmation view
230 # display confirmation view
236 else
231 else
237 if api_request? || params[:confirm]
232 if api_request? || params[:confirm]
238 @project_to_destroy.destroy
233 @project_to_destroy.destroy
239 respond_to do |format|
234 respond_to do |format|
240 format.html { redirect_to :controller => 'admin', :action => 'projects' }
235 format.html { redirect_to :controller => 'admin', :action => 'projects' }
241 format.api { head :ok }
236 format.api { head :ok }
242 end
237 end
243 end
238 end
244 end
239 end
245 # hide project in layout
240 # hide project in layout
246 @project = nil
241 @project = nil
247 end
242 end
248
243
249 private
244 private
250 def find_optional_project
245 def find_optional_project
251 return true unless params[:id]
246 return true unless params[:id]
252 @project = Project.find(params[:id])
247 @project = Project.find(params[:id])
253 authorize
248 authorize
254 rescue ActiveRecord::RecordNotFound
249 rescue ActiveRecord::RecordNotFound
255 render_404
250 render_404
256 end
251 end
257
252
258 # Validates parent_id param according to user's permissions
253 # Validates parent_id param according to user's permissions
259 # TODO: move it to Project model in a validation that depends on User.current
254 # TODO: move it to Project model in a validation that depends on User.current
260 def validate_parent_id
255 def validate_parent_id
261 return true if User.current.admin?
256 return true if User.current.admin?
262 parent_id = params[:project] && params[:project][:parent_id]
257 parent_id = params[:project] && params[:project][:parent_id]
263 if parent_id || @project.new_record?
258 if parent_id || @project.new_record?
264 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
259 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
265 unless @project.allowed_parents.include?(parent)
260 unless @project.allowed_parents.include?(parent)
266 @project.errors.add :parent_id, :invalid
261 @project.errors.add :parent_id, :invalid
267 return false
262 return false
268 end
263 end
269 end
264 end
270 true
265 true
271 end
266 end
272 end
267 end
@@ -1,794 +1,817
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 # Maximum length for project identifiers
23 # Maximum length for project identifiers
24 IDENTIFIER_MAX_LENGTH = 100
24 IDENTIFIER_MAX_LENGTH = 100
25
25
26 # Specific overidden Activities
26 # Specific overidden Activities
27 has_many :time_entry_activities
27 has_many :time_entry_activities
28 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
28 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
29 has_many :memberships, :class_name => 'Member'
29 has_many :memberships, :class_name => 'Member'
30 has_many :member_principals, :class_name => 'Member',
30 has_many :member_principals, :class_name => 'Member',
31 :include => :principal,
31 :include => :principal,
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 has_many :users, :through => :members
33 has_many :users, :through => :members
34 has_many :principals, :through => :member_principals, :source => :principal
34 has_many :principals, :through => :member_principals, :source => :principal
35
35
36 has_many :enabled_modules, :dependent => :delete_all
36 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 has_many :issue_changes, :through => :issues, :source => :journals
39 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :time_entries, :dependent => :delete_all
41 has_many :time_entries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
43 has_many :documents, :dependent => :destroy
43 has_many :documents, :dependent => :destroy
44 has_many :news, :dependent => :delete_all, :include => :author
44 has_many :news, :dependent => :delete_all, :include => :author
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_one :repository, :dependent => :destroy
47 has_one :repository, :dependent => :destroy
48 has_many :changesets, :through => :repository
48 has_many :changesets, :through => :repository
49 has_one :wiki, :dependent => :destroy
49 has_one :wiki, :dependent => :destroy
50 # Custom field for the project issues
50 # Custom field for the project issues
51 has_and_belongs_to_many :issue_custom_fields,
51 has_and_belongs_to_many :issue_custom_fields,
52 :class_name => 'IssueCustomField',
52 :class_name => 'IssueCustomField',
53 :order => "#{CustomField.table_name}.position",
53 :order => "#{CustomField.table_name}.position",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 :association_foreign_key => 'custom_field_id'
55 :association_foreign_key => 'custom_field_id'
56
56
57 acts_as_nested_set :order => 'name'
57 acts_as_nested_set :order => 'name'
58 acts_as_attachable :view_permission => :view_files,
58 acts_as_attachable :view_permission => :view_files,
59 :delete_permission => :manage_files
59 :delete_permission => :manage_files
60
60
61 acts_as_customizable
61 acts_as_customizable
62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
65 :author => nil
65 :author => nil
66
66
67 attr_protected :status, :enabled_module_names
67 attr_protected :status, :enabled_module_names
68
68
69 validates_presence_of :name, :identifier
69 validates_presence_of :name, :identifier
70 validates_uniqueness_of :identifier
70 validates_uniqueness_of :identifier
71 validates_associated :repository, :wiki
71 validates_associated :repository, :wiki
72 validates_length_of :name, :maximum => 255
72 validates_length_of :name, :maximum => 255
73 validates_length_of :homepage, :maximum => 255
73 validates_length_of :homepage, :maximum => 255
74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 # donwcase letters, digits, dashes but not digits only
75 # donwcase letters, digits, dashes but not digits only
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 # reserved words
77 # reserved words
78 validates_exclusion_of :identifier, :in => %w( new )
78 validates_exclusion_of :identifier, :in => %w( new )
79
79
80 before_destroy :delete_all_members, :destroy_children
80 before_destroy :delete_all_members, :destroy_children
81
81
82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 named_scope :all_public, { :conditions => { :is_public => true } }
84 named_scope :all_public, { :conditions => { :is_public => true } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86
86
87 def initialize(attributes = nil)
88 super
89
90 initialized = (attributes || {}).stringify_keys
91 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
92 self.identifier = Project.next_identifier
93 end
94 if !initialized.key?('is_public')
95 self.is_public = Setting.default_projects_public?
96 end
97 if !initialized.key?('enabled_module_names')
98 self.enabled_module_names = Setting.default_projects_modules
99 end
100 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
101 self.trackers = Tracker.all
102 end
103 end
104
87 def identifier=(identifier)
105 def identifier=(identifier)
88 super unless identifier_frozen?
106 super unless identifier_frozen?
89 end
107 end
90
108
91 def identifier_frozen?
109 def identifier_frozen?
92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
110 errors[:identifier].nil? && !(new_record? || identifier.blank?)
93 end
111 end
94
112
95 # returns latest created projects
113 # returns latest created projects
96 # non public projects will be returned only if user is a member of those
114 # non public projects will be returned only if user is a member of those
97 def self.latest(user=nil, count=5)
115 def self.latest(user=nil, count=5)
98 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
116 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
99 end
117 end
100
118
101 # Returns a SQL :conditions string used to find all active projects for the specified user.
119 # Returns a SQL :conditions string used to find all active projects for the specified user.
102 #
120 #
103 # Examples:
121 # Examples:
104 # Projects.visible_by(admin) => "projects.status = 1"
122 # Projects.visible_by(admin) => "projects.status = 1"
105 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
123 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
106 def self.visible_by(user=nil)
124 def self.visible_by(user=nil)
107 user ||= User.current
125 user ||= User.current
108 if user && user.admin?
126 if user && user.admin?
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
127 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
110 elsif user && user.memberships.any?
128 elsif user && user.memberships.any?
111 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(',')}))"
129 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(',')}))"
112 else
130 else
113 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
114 end
132 end
115 end
133 end
116
134
117 def self.allowed_to_condition(user, permission, options={})
135 def self.allowed_to_condition(user, permission, options={})
118 statements = []
136 statements = []
119 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
137 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
120 if perm = Redmine::AccessControl.permission(permission)
138 if perm = Redmine::AccessControl.permission(permission)
121 unless perm.project_module.nil?
139 unless perm.project_module.nil?
122 # If the permission belongs to a project module, make sure the module is enabled
140 # If the permission belongs to a project module, make sure the module is enabled
123 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
141 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
124 end
142 end
125 end
143 end
126 if options[:project]
144 if options[:project]
127 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
145 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
128 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
146 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
129 base_statement = "(#{project_statement}) AND (#{base_statement})"
147 base_statement = "(#{project_statement}) AND (#{base_statement})"
130 end
148 end
131 if user.admin?
149 if user.admin?
132 # no restriction
150 # no restriction
133 else
151 else
134 statements << "1=0"
152 statements << "1=0"
135 if user.logged?
153 if user.logged?
136 if Role.non_member.allowed_to?(permission) && !options[:member]
154 if Role.non_member.allowed_to?(permission) && !options[:member]
137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
155 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
138 end
156 end
139 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
157 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
140 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
158 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
141 else
159 else
142 if Role.anonymous.allowed_to?(permission) && !options[:member]
160 if Role.anonymous.allowed_to?(permission) && !options[:member]
143 # anonymous user allowed on public project
161 # anonymous user allowed on public project
144 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
162 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
145 end
163 end
146 end
164 end
147 end
165 end
148 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
166 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
149 end
167 end
150
168
151 # Returns the Systemwide and project specific activities
169 # Returns the Systemwide and project specific activities
152 def activities(include_inactive=false)
170 def activities(include_inactive=false)
153 if include_inactive
171 if include_inactive
154 return all_activities
172 return all_activities
155 else
173 else
156 return active_activities
174 return active_activities
157 end
175 end
158 end
176 end
159
177
160 # Will create a new Project specific Activity or update an existing one
178 # Will create a new Project specific Activity or update an existing one
161 #
179 #
162 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
180 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
163 # does not successfully save.
181 # does not successfully save.
164 def update_or_create_time_entry_activity(id, activity_hash)
182 def update_or_create_time_entry_activity(id, activity_hash)
165 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
183 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
166 self.create_time_entry_activity_if_needed(activity_hash)
184 self.create_time_entry_activity_if_needed(activity_hash)
167 else
185 else
168 activity = project.time_entry_activities.find_by_id(id.to_i)
186 activity = project.time_entry_activities.find_by_id(id.to_i)
169 activity.update_attributes(activity_hash) if activity
187 activity.update_attributes(activity_hash) if activity
170 end
188 end
171 end
189 end
172
190
173 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
191 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
174 #
192 #
175 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
193 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
176 # does not successfully save.
194 # does not successfully save.
177 def create_time_entry_activity_if_needed(activity)
195 def create_time_entry_activity_if_needed(activity)
178 if activity['parent_id']
196 if activity['parent_id']
179
197
180 parent_activity = TimeEntryActivity.find(activity['parent_id'])
198 parent_activity = TimeEntryActivity.find(activity['parent_id'])
181 activity['name'] = parent_activity.name
199 activity['name'] = parent_activity.name
182 activity['position'] = parent_activity.position
200 activity['position'] = parent_activity.position
183
201
184 if Enumeration.overridding_change?(activity, parent_activity)
202 if Enumeration.overridding_change?(activity, parent_activity)
185 project_activity = self.time_entry_activities.create(activity)
203 project_activity = self.time_entry_activities.create(activity)
186
204
187 if project_activity.new_record?
205 if project_activity.new_record?
188 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
206 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
189 else
207 else
190 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
208 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
191 end
209 end
192 end
210 end
193 end
211 end
194 end
212 end
195
213
196 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
214 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
197 #
215 #
198 # Examples:
216 # Examples:
199 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
217 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
200 # project.project_condition(false) => "projects.id = 1"
218 # project.project_condition(false) => "projects.id = 1"
201 def project_condition(with_subprojects)
219 def project_condition(with_subprojects)
202 cond = "#{Project.table_name}.id = #{id}"
220 cond = "#{Project.table_name}.id = #{id}"
203 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
221 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
204 cond
222 cond
205 end
223 end
206
224
207 def self.find(*args)
225 def self.find(*args)
208 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
226 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
209 project = find_by_identifier(*args)
227 project = find_by_identifier(*args)
210 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
228 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
211 project
229 project
212 else
230 else
213 super
231 super
214 end
232 end
215 end
233 end
216
234
217 def to_param
235 def to_param
218 # id is used for projects with a numeric identifier (compatibility)
236 # id is used for projects with a numeric identifier (compatibility)
219 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
237 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
220 end
238 end
221
239
222 def active?
240 def active?
223 self.status == STATUS_ACTIVE
241 self.status == STATUS_ACTIVE
224 end
242 end
225
243
226 def archived?
244 def archived?
227 self.status == STATUS_ARCHIVED
245 self.status == STATUS_ARCHIVED
228 end
246 end
229
247
230 # Archives the project and its descendants
248 # Archives the project and its descendants
231 def archive
249 def archive
232 # Check that there is no issue of a non descendant project that is assigned
250 # Check that there is no issue of a non descendant project that is assigned
233 # to one of the project or descendant versions
251 # to one of the project or descendant versions
234 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
252 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
235 if v_ids.any? && Issue.find(:first, :include => :project,
253 if v_ids.any? && Issue.find(:first, :include => :project,
236 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
254 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
237 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
255 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
238 return false
256 return false
239 end
257 end
240 Project.transaction do
258 Project.transaction do
241 archive!
259 archive!
242 end
260 end
243 true
261 true
244 end
262 end
245
263
246 # Unarchives the project
264 # Unarchives the project
247 # All its ancestors must be active
265 # All its ancestors must be active
248 def unarchive
266 def unarchive
249 return false if ancestors.detect {|a| !a.active?}
267 return false if ancestors.detect {|a| !a.active?}
250 update_attribute :status, STATUS_ACTIVE
268 update_attribute :status, STATUS_ACTIVE
251 end
269 end
252
270
253 # Returns an array of projects the project can be moved to
271 # Returns an array of projects the project can be moved to
254 # by the current user
272 # by the current user
255 def allowed_parents
273 def allowed_parents
256 return @allowed_parents if @allowed_parents
274 return @allowed_parents if @allowed_parents
257 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
275 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
258 @allowed_parents = @allowed_parents - self_and_descendants
276 @allowed_parents = @allowed_parents - self_and_descendants
259 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
277 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
260 @allowed_parents << nil
278 @allowed_parents << nil
261 end
279 end
262 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
280 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
263 @allowed_parents << parent
281 @allowed_parents << parent
264 end
282 end
265 @allowed_parents
283 @allowed_parents
266 end
284 end
267
285
268 # Sets the parent of the project with authorization check
286 # Sets the parent of the project with authorization check
269 def set_allowed_parent!(p)
287 def set_allowed_parent!(p)
270 unless p.nil? || p.is_a?(Project)
288 unless p.nil? || p.is_a?(Project)
271 if p.to_s.blank?
289 if p.to_s.blank?
272 p = nil
290 p = nil
273 else
291 else
274 p = Project.find_by_id(p)
292 p = Project.find_by_id(p)
275 return false unless p
293 return false unless p
276 end
294 end
277 end
295 end
278 if p.nil?
296 if p.nil?
279 if !new_record? && allowed_parents.empty?
297 if !new_record? && allowed_parents.empty?
280 return false
298 return false
281 end
299 end
282 elsif !allowed_parents.include?(p)
300 elsif !allowed_parents.include?(p)
283 return false
301 return false
284 end
302 end
285 set_parent!(p)
303 set_parent!(p)
286 end
304 end
287
305
288 # Sets the parent of the project
306 # Sets the parent of the project
289 # Argument can be either a Project, a String, a Fixnum or nil
307 # Argument can be either a Project, a String, a Fixnum or nil
290 def set_parent!(p)
308 def set_parent!(p)
291 unless p.nil? || p.is_a?(Project)
309 unless p.nil? || p.is_a?(Project)
292 if p.to_s.blank?
310 if p.to_s.blank?
293 p = nil
311 p = nil
294 else
312 else
295 p = Project.find_by_id(p)
313 p = Project.find_by_id(p)
296 return false unless p
314 return false unless p
297 end
315 end
298 end
316 end
299 if p == parent && !p.nil?
317 if p == parent && !p.nil?
300 # Nothing to do
318 # Nothing to do
301 true
319 true
302 elsif p.nil? || (p.active? && move_possible?(p))
320 elsif p.nil? || (p.active? && move_possible?(p))
303 # Insert the project so that target's children or root projects stay alphabetically sorted
321 # Insert the project so that target's children or root projects stay alphabetically sorted
304 sibs = (p.nil? ? self.class.roots : p.children)
322 sibs = (p.nil? ? self.class.roots : p.children)
305 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
323 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
306 if to_be_inserted_before
324 if to_be_inserted_before
307 move_to_left_of(to_be_inserted_before)
325 move_to_left_of(to_be_inserted_before)
308 elsif p.nil?
326 elsif p.nil?
309 if sibs.empty?
327 if sibs.empty?
310 # move_to_root adds the project in first (ie. left) position
328 # move_to_root adds the project in first (ie. left) position
311 move_to_root
329 move_to_root
312 else
330 else
313 move_to_right_of(sibs.last) unless self == sibs.last
331 move_to_right_of(sibs.last) unless self == sibs.last
314 end
332 end
315 else
333 else
316 # move_to_child_of adds the project in last (ie.right) position
334 # move_to_child_of adds the project in last (ie.right) position
317 move_to_child_of(p)
335 move_to_child_of(p)
318 end
336 end
319 Issue.update_versions_from_hierarchy_change(self)
337 Issue.update_versions_from_hierarchy_change(self)
320 true
338 true
321 else
339 else
322 # Can not move to the given target
340 # Can not move to the given target
323 false
341 false
324 end
342 end
325 end
343 end
326
344
327 # Returns an array of the trackers used by the project and its active sub projects
345 # Returns an array of the trackers used by the project and its active sub projects
328 def rolled_up_trackers
346 def rolled_up_trackers
329 @rolled_up_trackers ||=
347 @rolled_up_trackers ||=
330 Tracker.find(:all, :include => :projects,
348 Tracker.find(:all, :include => :projects,
331 :select => "DISTINCT #{Tracker.table_name}.*",
349 :select => "DISTINCT #{Tracker.table_name}.*",
332 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
350 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
333 :order => "#{Tracker.table_name}.position")
351 :order => "#{Tracker.table_name}.position")
334 end
352 end
335
353
336 # Closes open and locked project versions that are completed
354 # Closes open and locked project versions that are completed
337 def close_completed_versions
355 def close_completed_versions
338 Version.transaction do
356 Version.transaction do
339 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
357 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
340 if version.completed?
358 if version.completed?
341 version.update_attribute(:status, 'closed')
359 version.update_attribute(:status, 'closed')
342 end
360 end
343 end
361 end
344 end
362 end
345 end
363 end
346
364
347 # Returns a scope of the Versions on subprojects
365 # Returns a scope of the Versions on subprojects
348 def rolled_up_versions
366 def rolled_up_versions
349 @rolled_up_versions ||=
367 @rolled_up_versions ||=
350 Version.scoped(:include => :project,
368 Version.scoped(:include => :project,
351 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
352 end
370 end
353
371
354 # Returns a scope of the Versions used by the project
372 # Returns a scope of the Versions used by the project
355 def shared_versions
373 def shared_versions
356 @shared_versions ||=
374 @shared_versions ||=
357 Version.scoped(:include => :project,
375 Version.scoped(:include => :project,
358 :conditions => "#{Project.table_name}.id = #{id}" +
376 :conditions => "#{Project.table_name}.id = #{id}" +
359 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
377 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
360 " #{Version.table_name}.sharing = 'system'" +
378 " #{Version.table_name}.sharing = 'system'" +
361 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
379 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
362 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
380 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
363 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
381 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
364 "))")
382 "))")
365 end
383 end
366
384
367 # Returns a hash of project users grouped by role
385 # Returns a hash of project users grouped by role
368 def users_by_role
386 def users_by_role
369 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
387 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
370 m.roles.each do |r|
388 m.roles.each do |r|
371 h[r] ||= []
389 h[r] ||= []
372 h[r] << m.user
390 h[r] << m.user
373 end
391 end
374 h
392 h
375 end
393 end
376 end
394 end
377
395
378 # Deletes all project's members
396 # Deletes all project's members
379 def delete_all_members
397 def delete_all_members
380 me, mr = Member.table_name, MemberRole.table_name
398 me, mr = Member.table_name, MemberRole.table_name
381 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
399 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
382 Member.delete_all(['project_id = ?', id])
400 Member.delete_all(['project_id = ?', id])
383 end
401 end
384
402
385 # Users issues can be assigned to
403 # Users issues can be assigned to
386 def assignable_users
404 def assignable_users
387 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
405 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
388 end
406 end
389
407
390 # Returns the mail adresses of users that should be always notified on project events
408 # Returns the mail adresses of users that should be always notified on project events
391 def recipients
409 def recipients
392 notified_users.collect {|user| user.mail}
410 notified_users.collect {|user| user.mail}
393 end
411 end
394
412
395 # Returns the users that should be notified on project events
413 # Returns the users that should be notified on project events
396 def notified_users
414 def notified_users
397 # TODO: User part should be extracted to User#notify_about?
415 # TODO: User part should be extracted to User#notify_about?
398 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
416 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
399 end
417 end
400
418
401 # Returns an array of all custom fields enabled for project issues
419 # Returns an array of all custom fields enabled for project issues
402 # (explictly associated custom fields and custom fields enabled for all projects)
420 # (explictly associated custom fields and custom fields enabled for all projects)
403 def all_issue_custom_fields
421 def all_issue_custom_fields
404 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
422 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
405 end
423 end
406
424
407 def project
425 def project
408 self
426 self
409 end
427 end
410
428
411 def <=>(project)
429 def <=>(project)
412 name.downcase <=> project.name.downcase
430 name.downcase <=> project.name.downcase
413 end
431 end
414
432
415 def to_s
433 def to_s
416 name
434 name
417 end
435 end
418
436
419 # Returns a short description of the projects (first lines)
437 # Returns a short description of the projects (first lines)
420 def short_description(length = 255)
438 def short_description(length = 255)
421 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
439 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
422 end
440 end
423
441
424 def css_classes
442 def css_classes
425 s = 'project'
443 s = 'project'
426 s << ' root' if root?
444 s << ' root' if root?
427 s << ' child' if child?
445 s << ' child' if child?
428 s << (leaf? ? ' leaf' : ' parent')
446 s << (leaf? ? ' leaf' : ' parent')
429 s
447 s
430 end
448 end
431
449
432 # The earliest start date of a project, based on it's issues and versions
450 # The earliest start date of a project, based on it's issues and versions
433 def start_date
451 def start_date
434 if module_enabled?(:issue_tracking)
452 if module_enabled?(:issue_tracking)
435 [
453 [
436 issues.minimum('start_date'),
454 issues.minimum('start_date'),
437 shared_versions.collect(&:effective_date),
455 shared_versions.collect(&:effective_date),
438 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
456 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
439 ].flatten.compact.min
457 ].flatten.compact.min
440 end
458 end
441 end
459 end
442
460
443 # The latest due date of an issue or version
461 # The latest due date of an issue or version
444 def due_date
462 def due_date
445 if module_enabled?(:issue_tracking)
463 if module_enabled?(:issue_tracking)
446 [
464 [
447 issues.maximum('due_date'),
465 issues.maximum('due_date'),
448 shared_versions.collect(&:effective_date),
466 shared_versions.collect(&:effective_date),
449 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
467 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
450 ].flatten.compact.max
468 ].flatten.compact.max
451 end
469 end
452 end
470 end
453
471
454 def overdue?
472 def overdue?
455 active? && !due_date.nil? && (due_date < Date.today)
473 active? && !due_date.nil? && (due_date < Date.today)
456 end
474 end
457
475
458 # Returns the percent completed for this project, based on the
476 # Returns the percent completed for this project, based on the
459 # progress on it's versions.
477 # progress on it's versions.
460 def completed_percent(options={:include_subprojects => false})
478 def completed_percent(options={:include_subprojects => false})
461 if options.delete(:include_subprojects)
479 if options.delete(:include_subprojects)
462 total = self_and_descendants.collect(&:completed_percent).sum
480 total = self_and_descendants.collect(&:completed_percent).sum
463
481
464 total / self_and_descendants.count
482 total / self_and_descendants.count
465 else
483 else
466 if versions.count > 0
484 if versions.count > 0
467 total = versions.collect(&:completed_pourcent).sum
485 total = versions.collect(&:completed_pourcent).sum
468
486
469 total / versions.count
487 total / versions.count
470 else
488 else
471 100
489 100
472 end
490 end
473 end
491 end
474 end
492 end
475
493
476 # Return true if this project is allowed to do the specified action.
494 # Return true if this project is allowed to do the specified action.
477 # action can be:
495 # action can be:
478 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
496 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
479 # * a permission Symbol (eg. :edit_project)
497 # * a permission Symbol (eg. :edit_project)
480 def allows_to?(action)
498 def allows_to?(action)
481 if action.is_a? Hash
499 if action.is_a? Hash
482 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
500 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
483 else
501 else
484 allowed_permissions.include? action
502 allowed_permissions.include? action
485 end
503 end
486 end
504 end
487
505
488 def module_enabled?(module_name)
506 def module_enabled?(module_name)
489 module_name = module_name.to_s
507 module_name = module_name.to_s
490 enabled_modules.detect {|m| m.name == module_name}
508 enabled_modules.detect {|m| m.name == module_name}
491 end
509 end
492
510
493 def enabled_module_names=(module_names)
511 def enabled_module_names=(module_names)
494 if module_names && module_names.is_a?(Array)
512 if module_names && module_names.is_a?(Array)
495 module_names = module_names.collect(&:to_s)
513 module_names = module_names.collect(&:to_s).reject(&:blank?)
496 # remove disabled modules
514 # remove disabled modules
497 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
515 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
498 # add new modules
516 # add new modules
499 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
517 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
500 else
518 else
501 enabled_modules.clear
519 enabled_modules.clear
502 end
520 end
503 end
521 end
522
523 # Returns an array of the enabled modules names
524 def enabled_module_names
525 enabled_modules.collect(&:name)
526 end
504
527
505 # Returns an array of projects that are in this project's hierarchy
528 # Returns an array of projects that are in this project's hierarchy
506 #
529 #
507 # Example: parents, children, siblings
530 # Example: parents, children, siblings
508 def hierarchy
531 def hierarchy
509 parents = project.self_and_ancestors || []
532 parents = project.self_and_ancestors || []
510 descendants = project.descendants || []
533 descendants = project.descendants || []
511 project_hierarchy = parents | descendants # Set union
534 project_hierarchy = parents | descendants # Set union
512 end
535 end
513
536
514 # Returns an auto-generated project identifier based on the last identifier used
537 # Returns an auto-generated project identifier based on the last identifier used
515 def self.next_identifier
538 def self.next_identifier
516 p = Project.find(:first, :order => 'created_on DESC')
539 p = Project.find(:first, :order => 'created_on DESC')
517 p.nil? ? nil : p.identifier.to_s.succ
540 p.nil? ? nil : p.identifier.to_s.succ
518 end
541 end
519
542
520 # Copies and saves the Project instance based on the +project+.
543 # Copies and saves the Project instance based on the +project+.
521 # Duplicates the source project's:
544 # Duplicates the source project's:
522 # * Wiki
545 # * Wiki
523 # * Versions
546 # * Versions
524 # * Categories
547 # * Categories
525 # * Issues
548 # * Issues
526 # * Members
549 # * Members
527 # * Queries
550 # * Queries
528 #
551 #
529 # Accepts an +options+ argument to specify what to copy
552 # Accepts an +options+ argument to specify what to copy
530 #
553 #
531 # Examples:
554 # Examples:
532 # project.copy(1) # => copies everything
555 # project.copy(1) # => copies everything
533 # project.copy(1, :only => 'members') # => copies members only
556 # project.copy(1, :only => 'members') # => copies members only
534 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
557 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
535 def copy(project, options={})
558 def copy(project, options={})
536 project = project.is_a?(Project) ? project : Project.find(project)
559 project = project.is_a?(Project) ? project : Project.find(project)
537
560
538 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
561 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
539 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
562 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
540
563
541 Project.transaction do
564 Project.transaction do
542 if save
565 if save
543 reload
566 reload
544 to_be_copied.each do |name|
567 to_be_copied.each do |name|
545 send "copy_#{name}", project
568 send "copy_#{name}", project
546 end
569 end
547 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
570 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
548 save
571 save
549 end
572 end
550 end
573 end
551 end
574 end
552
575
553
576
554 # Copies +project+ and returns the new instance. This will not save
577 # Copies +project+ and returns the new instance. This will not save
555 # the copy
578 # the copy
556 def self.copy_from(project)
579 def self.copy_from(project)
557 begin
580 begin
558 project = project.is_a?(Project) ? project : Project.find(project)
581 project = project.is_a?(Project) ? project : Project.find(project)
559 if project
582 if project
560 # clear unique attributes
583 # clear unique attributes
561 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
584 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
562 copy = Project.new(attributes)
585 copy = Project.new(attributes)
563 copy.enabled_modules = project.enabled_modules
586 copy.enabled_modules = project.enabled_modules
564 copy.trackers = project.trackers
587 copy.trackers = project.trackers
565 copy.custom_values = project.custom_values.collect {|v| v.clone}
588 copy.custom_values = project.custom_values.collect {|v| v.clone}
566 copy.issue_custom_fields = project.issue_custom_fields
589 copy.issue_custom_fields = project.issue_custom_fields
567 return copy
590 return copy
568 else
591 else
569 return nil
592 return nil
570 end
593 end
571 rescue ActiveRecord::RecordNotFound
594 rescue ActiveRecord::RecordNotFound
572 return nil
595 return nil
573 end
596 end
574 end
597 end
575
598
576 # Yields the given block for each project with its level in the tree
599 # Yields the given block for each project with its level in the tree
577 def self.project_tree(projects, &block)
600 def self.project_tree(projects, &block)
578 ancestors = []
601 ancestors = []
579 projects.sort_by(&:lft).each do |project|
602 projects.sort_by(&:lft).each do |project|
580 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
603 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
581 ancestors.pop
604 ancestors.pop
582 end
605 end
583 yield project, ancestors.size
606 yield project, ancestors.size
584 ancestors << project
607 ancestors << project
585 end
608 end
586 end
609 end
587
610
588 private
611 private
589
612
590 # Destroys children before destroying self
613 # Destroys children before destroying self
591 def destroy_children
614 def destroy_children
592 children.each do |child|
615 children.each do |child|
593 child.destroy
616 child.destroy
594 end
617 end
595 end
618 end
596
619
597 # Copies wiki from +project+
620 # Copies wiki from +project+
598 def copy_wiki(project)
621 def copy_wiki(project)
599 # Check that the source project has a wiki first
622 # Check that the source project has a wiki first
600 unless project.wiki.nil?
623 unless project.wiki.nil?
601 self.wiki ||= Wiki.new
624 self.wiki ||= Wiki.new
602 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
625 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
603 wiki_pages_map = {}
626 wiki_pages_map = {}
604 project.wiki.pages.each do |page|
627 project.wiki.pages.each do |page|
605 # Skip pages without content
628 # Skip pages without content
606 next if page.content.nil?
629 next if page.content.nil?
607 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
630 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
608 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
631 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
609 new_wiki_page.content = new_wiki_content
632 new_wiki_page.content = new_wiki_content
610 wiki.pages << new_wiki_page
633 wiki.pages << new_wiki_page
611 wiki_pages_map[page.id] = new_wiki_page
634 wiki_pages_map[page.id] = new_wiki_page
612 end
635 end
613 wiki.save
636 wiki.save
614 # Reproduce page hierarchy
637 # Reproduce page hierarchy
615 project.wiki.pages.each do |page|
638 project.wiki.pages.each do |page|
616 if page.parent_id && wiki_pages_map[page.id]
639 if page.parent_id && wiki_pages_map[page.id]
617 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
640 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
618 wiki_pages_map[page.id].save
641 wiki_pages_map[page.id].save
619 end
642 end
620 end
643 end
621 end
644 end
622 end
645 end
623
646
624 # Copies versions from +project+
647 # Copies versions from +project+
625 def copy_versions(project)
648 def copy_versions(project)
626 project.versions.each do |version|
649 project.versions.each do |version|
627 new_version = Version.new
650 new_version = Version.new
628 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
651 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
629 self.versions << new_version
652 self.versions << new_version
630 end
653 end
631 end
654 end
632
655
633 # Copies issue categories from +project+
656 # Copies issue categories from +project+
634 def copy_issue_categories(project)
657 def copy_issue_categories(project)
635 project.issue_categories.each do |issue_category|
658 project.issue_categories.each do |issue_category|
636 new_issue_category = IssueCategory.new
659 new_issue_category = IssueCategory.new
637 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
660 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
638 self.issue_categories << new_issue_category
661 self.issue_categories << new_issue_category
639 end
662 end
640 end
663 end
641
664
642 # Copies issues from +project+
665 # Copies issues from +project+
643 def copy_issues(project)
666 def copy_issues(project)
644 # Stores the source issue id as a key and the copied issues as the
667 # Stores the source issue id as a key and the copied issues as the
645 # value. Used to map the two togeather for issue relations.
668 # value. Used to map the two togeather for issue relations.
646 issues_map = {}
669 issues_map = {}
647
670
648 # Get issues sorted by root_id, lft so that parent issues
671 # Get issues sorted by root_id, lft so that parent issues
649 # get copied before their children
672 # get copied before their children
650 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
673 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
651 new_issue = Issue.new
674 new_issue = Issue.new
652 new_issue.copy_from(issue)
675 new_issue.copy_from(issue)
653 new_issue.project = self
676 new_issue.project = self
654 # Reassign fixed_versions by name, since names are unique per
677 # Reassign fixed_versions by name, since names are unique per
655 # project and the versions for self are not yet saved
678 # project and the versions for self are not yet saved
656 if issue.fixed_version
679 if issue.fixed_version
657 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
680 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
658 end
681 end
659 # Reassign the category by name, since names are unique per
682 # Reassign the category by name, since names are unique per
660 # project and the categories for self are not yet saved
683 # project and the categories for self are not yet saved
661 if issue.category
684 if issue.category
662 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
685 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
663 end
686 end
664 # Parent issue
687 # Parent issue
665 if issue.parent_id
688 if issue.parent_id
666 if copied_parent = issues_map[issue.parent_id]
689 if copied_parent = issues_map[issue.parent_id]
667 new_issue.parent_issue_id = copied_parent.id
690 new_issue.parent_issue_id = copied_parent.id
668 end
691 end
669 end
692 end
670
693
671 self.issues << new_issue
694 self.issues << new_issue
672 issues_map[issue.id] = new_issue
695 issues_map[issue.id] = new_issue
673 end
696 end
674
697
675 # Relations after in case issues related each other
698 # Relations after in case issues related each other
676 project.issues.each do |issue|
699 project.issues.each do |issue|
677 new_issue = issues_map[issue.id]
700 new_issue = issues_map[issue.id]
678
701
679 # Relations
702 # Relations
680 issue.relations_from.each do |source_relation|
703 issue.relations_from.each do |source_relation|
681 new_issue_relation = IssueRelation.new
704 new_issue_relation = IssueRelation.new
682 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
705 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
683 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
706 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
684 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
707 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
685 new_issue_relation.issue_to = source_relation.issue_to
708 new_issue_relation.issue_to = source_relation.issue_to
686 end
709 end
687 new_issue.relations_from << new_issue_relation
710 new_issue.relations_from << new_issue_relation
688 end
711 end
689
712
690 issue.relations_to.each do |source_relation|
713 issue.relations_to.each do |source_relation|
691 new_issue_relation = IssueRelation.new
714 new_issue_relation = IssueRelation.new
692 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
715 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
693 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
716 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
694 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
717 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
695 new_issue_relation.issue_from = source_relation.issue_from
718 new_issue_relation.issue_from = source_relation.issue_from
696 end
719 end
697 new_issue.relations_to << new_issue_relation
720 new_issue.relations_to << new_issue_relation
698 end
721 end
699 end
722 end
700 end
723 end
701
724
702 # Copies members from +project+
725 # Copies members from +project+
703 def copy_members(project)
726 def copy_members(project)
704 project.memberships.each do |member|
727 project.memberships.each do |member|
705 new_member = Member.new
728 new_member = Member.new
706 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
729 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
707 # only copy non inherited roles
730 # only copy non inherited roles
708 # inherited roles will be added when copying the group membership
731 # inherited roles will be added when copying the group membership
709 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
732 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
710 next if role_ids.empty?
733 next if role_ids.empty?
711 new_member.role_ids = role_ids
734 new_member.role_ids = role_ids
712 new_member.project = self
735 new_member.project = self
713 self.members << new_member
736 self.members << new_member
714 end
737 end
715 end
738 end
716
739
717 # Copies queries from +project+
740 # Copies queries from +project+
718 def copy_queries(project)
741 def copy_queries(project)
719 project.queries.each do |query|
742 project.queries.each do |query|
720 new_query = Query.new
743 new_query = Query.new
721 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
744 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
722 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
745 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
723 new_query.project = self
746 new_query.project = self
724 self.queries << new_query
747 self.queries << new_query
725 end
748 end
726 end
749 end
727
750
728 # Copies boards from +project+
751 # Copies boards from +project+
729 def copy_boards(project)
752 def copy_boards(project)
730 project.boards.each do |board|
753 project.boards.each do |board|
731 new_board = Board.new
754 new_board = Board.new
732 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
755 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
733 new_board.project = self
756 new_board.project = self
734 self.boards << new_board
757 self.boards << new_board
735 end
758 end
736 end
759 end
737
760
738 def allowed_permissions
761 def allowed_permissions
739 @allowed_permissions ||= begin
762 @allowed_permissions ||= begin
740 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
763 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
741 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
764 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
742 end
765 end
743 end
766 end
744
767
745 def allowed_actions
768 def allowed_actions
746 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
769 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
747 end
770 end
748
771
749 # Returns all the active Systemwide and project specific activities
772 # Returns all the active Systemwide and project specific activities
750 def active_activities
773 def active_activities
751 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
774 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
752
775
753 if overridden_activity_ids.empty?
776 if overridden_activity_ids.empty?
754 return TimeEntryActivity.shared.active
777 return TimeEntryActivity.shared.active
755 else
778 else
756 return system_activities_and_project_overrides
779 return system_activities_and_project_overrides
757 end
780 end
758 end
781 end
759
782
760 # Returns all the Systemwide and project specific activities
783 # Returns all the Systemwide and project specific activities
761 # (inactive and active)
784 # (inactive and active)
762 def all_activities
785 def all_activities
763 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
786 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
764
787
765 if overridden_activity_ids.empty?
788 if overridden_activity_ids.empty?
766 return TimeEntryActivity.shared
789 return TimeEntryActivity.shared
767 else
790 else
768 return system_activities_and_project_overrides(true)
791 return system_activities_and_project_overrides(true)
769 end
792 end
770 end
793 end
771
794
772 # Returns the systemwide active activities merged with the project specific overrides
795 # Returns the systemwide active activities merged with the project specific overrides
773 def system_activities_and_project_overrides(include_inactive=false)
796 def system_activities_and_project_overrides(include_inactive=false)
774 if include_inactive
797 if include_inactive
775 return TimeEntryActivity.shared.
798 return TimeEntryActivity.shared.
776 find(:all,
799 find(:all,
777 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
800 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
778 self.time_entry_activities
801 self.time_entry_activities
779 else
802 else
780 return TimeEntryActivity.shared.active.
803 return TimeEntryActivity.shared.active.
781 find(:all,
804 find(:all,
782 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
805 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
783 self.time_entry_activities.active
806 self.time_entry_activities.active
784 end
807 end
785 end
808 end
786
809
787 # Archives subprojects recursively
810 # Archives subprojects recursively
788 def archive!
811 def archive!
789 children.each do |subproject|
812 children.each do |subproject|
790 subproject.send :archive!
813 subproject.send :archive!
791 end
814 end
792 update_attribute :status, STATUS_ARCHIVED
815 update_attribute :status, STATUS_ARCHIVED
793 end
816 end
794 end
817 end
@@ -1,17 +1,19
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
3 <% labelled_tabular_form_for :project, @project, :url => { :action => "create" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
5
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
6 <fieldset class="box"><legend><%= l(:label_module_plural) %></legend>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
7 <% Redmine::AccessControl.available_project_modules.each do |m| %>
8 <label class="floating">
8 <label class="floating">
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
9 <%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
10 <%= l_or_humanize(m, :prefix => "project_module_") %>
11 </label>
11 </label>
12 <% end %>
12 <% end %>
13 <%= hidden_field_tag 'enabled_modules[]', '' %>
14
13 </fieldset>
15 </fieldset>
14
16
15 <%= submit_tag l(:button_save) %>
17 <%= submit_tag l(:button_save) %>
16 <%= javascript_tag "Form.Element.focus('project_name');" %>
18 <%= javascript_tag "Form.Element.focus('project_name');" %>
17 <% end %>
19 <% end %>
@@ -1,214 +1,216
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 ApiTest::ProjectsTest < ActionController::IntegrationTest
20 class ApiTest::ProjectsTest < ActionController::IntegrationTest
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
21 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
22 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
23 :attachments, :custom_fields, :custom_values, :time_entries
23 :attachments, :custom_fields, :custom_values, :time_entries
24
24
25 def setup
25 def setup
26 Setting.rest_api_enabled = '1'
26 Setting.rest_api_enabled = '1'
27 end
27 end
28
28
29 context "GET /projects" do
29 context "GET /projects" do
30 context ".xml" do
30 context ".xml" do
31 should "return projects" do
31 should "return projects" do
32 get '/projects.xml'
32 get '/projects.xml'
33 assert_response :success
33 assert_response :success
34 assert_equal 'application/xml', @response.content_type
34 assert_equal 'application/xml', @response.content_type
35
35
36 assert_tag :tag => 'projects',
36 assert_tag :tag => 'projects',
37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
37 :child => {:tag => 'project', :child => {:tag => 'id', :content => '1'}}
38 end
38 end
39 end
39 end
40
40
41 context ".json" do
41 context ".json" do
42 should "return projects" do
42 should "return projects" do
43 get '/projects.json'
43 get '/projects.json'
44 assert_response :success
44 assert_response :success
45 assert_equal 'application/json', @response.content_type
45 assert_equal 'application/json', @response.content_type
46
46
47 json = ActiveSupport::JSON.decode(response.body)
47 json = ActiveSupport::JSON.decode(response.body)
48 assert_kind_of Hash, json
48 assert_kind_of Hash, json
49 assert_kind_of Array, json['projects']
49 assert_kind_of Array, json['projects']
50 assert_kind_of Hash, json['projects'].first
50 assert_kind_of Hash, json['projects'].first
51 assert json['projects'].first.has_key?('id')
51 assert json['projects'].first.has_key?('id')
52 end
52 end
53 end
53 end
54 end
54 end
55
55
56 context "GET /projects/:id" do
56 context "GET /projects/:id" do
57 context ".xml" do
57 context ".xml" do
58 # TODO: A private project is needed because should_allow_api_authentication
58 # TODO: A private project is needed because should_allow_api_authentication
59 # actually tests that authentication is *required*, not just allowed
59 # actually tests that authentication is *required*, not just allowed
60 should_allow_api_authentication(:get, "/projects/2.xml")
60 should_allow_api_authentication(:get, "/projects/2.xml")
61
61
62 should "return requested project" do
62 should "return requested project" do
63 get '/projects/1.xml'
63 get '/projects/1.xml'
64 assert_response :success
64 assert_response :success
65 assert_equal 'application/xml', @response.content_type
65 assert_equal 'application/xml', @response.content_type
66
66
67 assert_tag :tag => 'project',
67 assert_tag :tag => 'project',
68 :child => {:tag => 'id', :content => '1'}
68 :child => {:tag => 'id', :content => '1'}
69 assert_tag :tag => 'custom_field',
69 assert_tag :tag => 'custom_field',
70 :attributes => {:name => 'Development status'}, :content => 'Stable'
70 :attributes => {:name => 'Development status'}, :content => 'Stable'
71 end
71 end
72
72
73 context "with hidden custom fields" do
73 context "with hidden custom fields" do
74 setup do
74 setup do
75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
75 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
76 end
76 end
77
77
78 should "not display hidden custom fields" do
78 should "not display hidden custom fields" do
79 get '/projects/1.xml'
79 get '/projects/1.xml'
80 assert_response :success
80 assert_response :success
81 assert_equal 'application/xml', @response.content_type
81 assert_equal 'application/xml', @response.content_type
82
82
83 assert_no_tag 'custom_field',
83 assert_no_tag 'custom_field',
84 :attributes => {:name => 'Development status'}
84 :attributes => {:name => 'Development status'}
85 end
85 end
86 end
86 end
87 end
87 end
88
88
89 context ".json" do
89 context ".json" do
90 should_allow_api_authentication(:get, "/projects/2.json")
90 should_allow_api_authentication(:get, "/projects/2.json")
91
91
92 should "return requested project" do
92 should "return requested project" do
93 get '/projects/1.json'
93 get '/projects/1.json'
94
94
95 json = ActiveSupport::JSON.decode(response.body)
95 json = ActiveSupport::JSON.decode(response.body)
96 assert_kind_of Hash, json
96 assert_kind_of Hash, json
97 assert_kind_of Hash, json['project']
97 assert_kind_of Hash, json['project']
98 assert_equal 1, json['project']['id']
98 assert_equal 1, json['project']['id']
99 end
99 end
100 end
100 end
101 end
101 end
102
102
103 context "POST /projects" do
103 context "POST /projects" do
104 context "with valid parameters" do
104 context "with valid parameters" do
105 setup do
105 setup do
106 Setting.default_projects_modules = ['issue_tracking', 'repository']
106 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
107 @parameters = {:project => {:name => 'API test', :identifier => 'api-test'}}
107 end
108 end
108
109
109 context ".xml" do
110 context ".xml" do
110 should_allow_api_authentication(:post,
111 should_allow_api_authentication(:post,
111 '/projects.xml',
112 '/projects.xml',
112 {:project => {:name => 'API test', :identifier => 'api-test'}},
113 {:project => {:name => 'API test', :identifier => 'api-test'}},
113 {:success_code => :created})
114 {:success_code => :created})
114
115
115
116
116 should "create a project with the attributes" do
117 should "create a project with the attributes" do
117 assert_difference('Project.count') do
118 assert_difference('Project.count') do
118 post '/projects.xml', @parameters, :authorization => credentials('admin')
119 post '/projects.xml', @parameters, :authorization => credentials('admin')
119 end
120 end
120
121
121 project = Project.first(:order => 'id DESC')
122 project = Project.first(:order => 'id DESC')
122 assert_equal 'API test', project.name
123 assert_equal 'API test', project.name
123 assert_equal 'api-test', project.identifier
124 assert_equal 'api-test', project.identifier
125 assert_equal ['issue_tracking', 'repository'], project.enabled_module_names
124
126
125 assert_response :created
127 assert_response :created
126 assert_equal 'application/xml', @response.content_type
128 assert_equal 'application/xml', @response.content_type
127 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
129 assert_tag 'project', :child => {:tag => 'id', :content => project.id.to_s}
128 end
130 end
129 end
131 end
130 end
132 end
131
133
132 context "with invalid parameters" do
134 context "with invalid parameters" do
133 setup do
135 setup do
134 @parameters = {:project => {:name => 'API test'}}
136 @parameters = {:project => {:name => 'API test'}}
135 end
137 end
136
138
137 context ".xml" do
139 context ".xml" do
138 should "return errors" do
140 should "return errors" do
139 assert_no_difference('Project.count') do
141 assert_no_difference('Project.count') do
140 post '/projects.xml', @parameters, :authorization => credentials('admin')
142 post '/projects.xml', @parameters, :authorization => credentials('admin')
141 end
143 end
142
144
143 assert_response :unprocessable_entity
145 assert_response :unprocessable_entity
144 assert_equal 'application/xml', @response.content_type
146 assert_equal 'application/xml', @response.content_type
145 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
147 assert_tag 'errors', :child => {:tag => 'error', :content => "Identifier can't be blank"}
146 end
148 end
147 end
149 end
148 end
150 end
149 end
151 end
150
152
151 context "PUT /projects/:id" do
153 context "PUT /projects/:id" do
152 context "with valid parameters" do
154 context "with valid parameters" do
153 setup do
155 setup do
154 @parameters = {:project => {:name => 'API update'}}
156 @parameters = {:project => {:name => 'API update'}}
155 end
157 end
156
158
157 context ".xml" do
159 context ".xml" do
158 should_allow_api_authentication(:put,
160 should_allow_api_authentication(:put,
159 '/projects/2.xml',
161 '/projects/2.xml',
160 {:project => {:name => 'API update'}},
162 {:project => {:name => 'API update'}},
161 {:success_code => :ok})
163 {:success_code => :ok})
162
164
163 should "update the project" do
165 should "update the project" do
164 assert_no_difference 'Project.count' do
166 assert_no_difference 'Project.count' do
165 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
167 put '/projects/2.xml', @parameters, :authorization => credentials('jsmith')
166 end
168 end
167 assert_response :ok
169 assert_response :ok
168 assert_equal 'application/xml', @response.content_type
170 assert_equal 'application/xml', @response.content_type
169 project = Project.find(2)
171 project = Project.find(2)
170 assert_equal 'API update', project.name
172 assert_equal 'API update', project.name
171 end
173 end
172 end
174 end
173 end
175 end
174
176
175 context "with invalid parameters" do
177 context "with invalid parameters" do
176 setup do
178 setup do
177 @parameters = {:project => {:name => ''}}
179 @parameters = {:project => {:name => ''}}
178 end
180 end
179
181
180 context ".xml" do
182 context ".xml" do
181 should "return errors" do
183 should "return errors" do
182 assert_no_difference('Project.count') do
184 assert_no_difference('Project.count') do
183 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
185 put '/projects/2.xml', @parameters, :authorization => credentials('admin')
184 end
186 end
185
187
186 assert_response :unprocessable_entity
188 assert_response :unprocessable_entity
187 assert_equal 'application/xml', @response.content_type
189 assert_equal 'application/xml', @response.content_type
188 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
190 assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"}
189 end
191 end
190 end
192 end
191 end
193 end
192 end
194 end
193
195
194 context "DELETE /projects/:id" do
196 context "DELETE /projects/:id" do
195 context ".xml" do
197 context ".xml" do
196 should_allow_api_authentication(:delete,
198 should_allow_api_authentication(:delete,
197 '/projects/2.xml',
199 '/projects/2.xml',
198 {},
200 {},
199 {:success_code => :ok})
201 {:success_code => :ok})
200
202
201 should "delete the project" do
203 should "delete the project" do
202 assert_difference('Project.count',-1) do
204 assert_difference('Project.count',-1) do
203 delete '/projects/2.xml', {}, :authorization => credentials('admin')
205 delete '/projects/2.xml', {}, :authorization => credentials('admin')
204 end
206 end
205 assert_response :ok
207 assert_response :ok
206 assert_nil Project.find_by_id(2)
208 assert_nil Project.find_by_id(2)
207 end
209 end
208 end
210 end
209 end
211 end
210
212
211 def credentials(user, password=nil)
213 def credentials(user, password=nil)
212 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
214 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
213 end
215 end
214 end
216 end
@@ -1,1018 +1,1047
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 :identifier
32 should_validate_uniqueness_of :identifier
33
33
34 context "associations" do
34 context "associations" do
35 should_have_many :members
35 should_have_many :members
36 should_have_many :users, :through => :members
36 should_have_many :users, :through => :members
37 should_have_many :member_principals
37 should_have_many :member_principals
38 should_have_many :principals, :through => :member_principals
38 should_have_many :principals, :through => :member_principals
39 should_have_many :enabled_modules
39 should_have_many :enabled_modules
40 should_have_many :issues
40 should_have_many :issues
41 should_have_many :issue_changes, :through => :issues
41 should_have_many :issue_changes, :through => :issues
42 should_have_many :versions
42 should_have_many :versions
43 should_have_many :time_entries
43 should_have_many :time_entries
44 should_have_many :queries
44 should_have_many :queries
45 should_have_many :documents
45 should_have_many :documents
46 should_have_many :news
46 should_have_many :news
47 should_have_many :issue_categories
47 should_have_many :issue_categories
48 should_have_many :boards
48 should_have_many :boards
49 should_have_many :changesets, :through => :repository
49 should_have_many :changesets, :through => :repository
50
50
51 should_have_one :repository
51 should_have_one :repository
52 should_have_one :wiki
52 should_have_one :wiki
53
53
54 should_have_and_belong_to_many :trackers
54 should_have_and_belong_to_many :trackers
55 should_have_and_belong_to_many :issue_custom_fields
55 should_have_and_belong_to_many :issue_custom_fields
56 end
56 end
57
57
58 def test_truth
58 def test_truth
59 assert_kind_of Project, @ecookbook
59 assert_kind_of Project, @ecookbook
60 assert_equal "eCookbook", @ecookbook.name
60 assert_equal "eCookbook", @ecookbook.name
61 end
61 end
62
62
63 def test_default_attributes
64 with_settings :default_projects_public => '1' do
65 assert_equal true, Project.new.is_public
66 assert_equal false, Project.new(:is_public => false).is_public
67 end
68
69 with_settings :default_projects_public => '0' do
70 assert_equal false, Project.new.is_public
71 assert_equal true, Project.new(:is_public => true).is_public
72 end
73
74 with_settings :sequential_project_identifiers => '1' do
75 assert !Project.new.identifier.blank?
76 assert Project.new(:identifier => '').identifier.blank?
77 end
78
79 with_settings :sequential_project_identifiers => '0' do
80 assert Project.new.identifier.blank?
81 assert !Project.new(:identifier => 'test').blank?
82 end
83
84 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
85 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
86 end
87
88 assert_equal Tracker.all, Project.new.trackers
89 assert_equal Tracker.find(1, 3), Project.new(:tracker_ids => [1, 3]).trackers
90 end
91
63 def test_update
92 def test_update
64 assert_equal "eCookbook", @ecookbook.name
93 assert_equal "eCookbook", @ecookbook.name
65 @ecookbook.name = "eCook"
94 @ecookbook.name = "eCook"
66 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
95 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
67 @ecookbook.reload
96 @ecookbook.reload
68 assert_equal "eCook", @ecookbook.name
97 assert_equal "eCook", @ecookbook.name
69 end
98 end
70
99
71 def test_validate_identifier
100 def test_validate_identifier
72 to_test = {"abc" => true,
101 to_test = {"abc" => true,
73 "ab12" => true,
102 "ab12" => true,
74 "ab-12" => true,
103 "ab-12" => true,
75 "12" => false,
104 "12" => false,
76 "new" => false}
105 "new" => false}
77
106
78 to_test.each do |identifier, valid|
107 to_test.each do |identifier, valid|
79 p = Project.new
108 p = Project.new
80 p.identifier = identifier
109 p.identifier = identifier
81 p.valid?
110 p.valid?
82 assert_equal valid, p.errors.on('identifier').nil?
111 assert_equal valid, p.errors.on('identifier').nil?
83 end
112 end
84 end
113 end
85
114
86 def test_members_should_be_active_users
115 def test_members_should_be_active_users
87 Project.all.each do |project|
116 Project.all.each do |project|
88 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
117 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
89 end
118 end
90 end
119 end
91
120
92 def test_users_should_be_active_users
121 def test_users_should_be_active_users
93 Project.all.each do |project|
122 Project.all.each do |project|
94 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
123 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
95 end
124 end
96 end
125 end
97
126
98 def test_archive
127 def test_archive
99 user = @ecookbook.members.first.user
128 user = @ecookbook.members.first.user
100 @ecookbook.archive
129 @ecookbook.archive
101 @ecookbook.reload
130 @ecookbook.reload
102
131
103 assert !@ecookbook.active?
132 assert !@ecookbook.active?
104 assert @ecookbook.archived?
133 assert @ecookbook.archived?
105 assert !user.projects.include?(@ecookbook)
134 assert !user.projects.include?(@ecookbook)
106 # Subproject are also archived
135 # Subproject are also archived
107 assert !@ecookbook.children.empty?
136 assert !@ecookbook.children.empty?
108 assert @ecookbook.descendants.active.empty?
137 assert @ecookbook.descendants.active.empty?
109 end
138 end
110
139
111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
140 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
141 # Assign an issue of a project to a version of a child project
113 Issue.find(4).update_attribute :fixed_version_id, 4
142 Issue.find(4).update_attribute :fixed_version_id, 4
114
143
115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
144 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 assert_equal false, @ecookbook.archive
145 assert_equal false, @ecookbook.archive
117 end
146 end
118 @ecookbook.reload
147 @ecookbook.reload
119 assert @ecookbook.active?
148 assert @ecookbook.active?
120 end
149 end
121
150
122 def test_unarchive
151 def test_unarchive
123 user = @ecookbook.members.first.user
152 user = @ecookbook.members.first.user
124 @ecookbook.archive
153 @ecookbook.archive
125 # A subproject of an archived project can not be unarchived
154 # A subproject of an archived project can not be unarchived
126 assert !@ecookbook_sub1.unarchive
155 assert !@ecookbook_sub1.unarchive
127
156
128 # Unarchive project
157 # Unarchive project
129 assert @ecookbook.unarchive
158 assert @ecookbook.unarchive
130 @ecookbook.reload
159 @ecookbook.reload
131 assert @ecookbook.active?
160 assert @ecookbook.active?
132 assert !@ecookbook.archived?
161 assert !@ecookbook.archived?
133 assert user.projects.include?(@ecookbook)
162 assert user.projects.include?(@ecookbook)
134 # Subproject can now be unarchived
163 # Subproject can now be unarchived
135 @ecookbook_sub1.reload
164 @ecookbook_sub1.reload
136 assert @ecookbook_sub1.unarchive
165 assert @ecookbook_sub1.unarchive
137 end
166 end
138
167
139 def test_destroy
168 def test_destroy
140 # 2 active members
169 # 2 active members
141 assert_equal 2, @ecookbook.members.size
170 assert_equal 2, @ecookbook.members.size
142 # and 1 is locked
171 # and 1 is locked
143 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
172 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
144 # some boards
173 # some boards
145 assert @ecookbook.boards.any?
174 assert @ecookbook.boards.any?
146
175
147 @ecookbook.destroy
176 @ecookbook.destroy
148 # make sure that the project non longer exists
177 # make sure that the project non longer exists
149 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
178 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
150 # make sure related data was removed
179 # make sure related data was removed
151 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
180 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
152 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
181 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
153 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
182 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
154 end
183 end
155
184
156 def test_move_an_orphan_project_to_a_root_project
185 def test_move_an_orphan_project_to_a_root_project
157 sub = Project.find(2)
186 sub = Project.find(2)
158 sub.set_parent! @ecookbook
187 sub.set_parent! @ecookbook
159 assert_equal @ecookbook.id, sub.parent.id
188 assert_equal @ecookbook.id, sub.parent.id
160 @ecookbook.reload
189 @ecookbook.reload
161 assert_equal 4, @ecookbook.children.size
190 assert_equal 4, @ecookbook.children.size
162 end
191 end
163
192
164 def test_move_an_orphan_project_to_a_subproject
193 def test_move_an_orphan_project_to_a_subproject
165 sub = Project.find(2)
194 sub = Project.find(2)
166 assert sub.set_parent!(@ecookbook_sub1)
195 assert sub.set_parent!(@ecookbook_sub1)
167 end
196 end
168
197
169 def test_move_a_root_project_to_a_project
198 def test_move_a_root_project_to_a_project
170 sub = @ecookbook
199 sub = @ecookbook
171 assert sub.set_parent!(Project.find(2))
200 assert sub.set_parent!(Project.find(2))
172 end
201 end
173
202
174 def test_should_not_move_a_project_to_its_children
203 def test_should_not_move_a_project_to_its_children
175 sub = @ecookbook
204 sub = @ecookbook
176 assert !(sub.set_parent!(Project.find(3)))
205 assert !(sub.set_parent!(Project.find(3)))
177 end
206 end
178
207
179 def test_set_parent_should_add_roots_in_alphabetical_order
208 def test_set_parent_should_add_roots_in_alphabetical_order
180 ProjectCustomField.delete_all
209 ProjectCustomField.delete_all
181 Project.delete_all
210 Project.delete_all
182 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
211 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
183 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
212 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
184 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
213 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
185 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
214 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
186
215
187 assert_equal 4, Project.count
216 assert_equal 4, Project.count
188 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
217 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
189 end
218 end
190
219
191 def test_set_parent_should_add_children_in_alphabetical_order
220 def test_set_parent_should_add_children_in_alphabetical_order
192 ProjectCustomField.delete_all
221 ProjectCustomField.delete_all
193 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
222 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
194 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
223 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
195 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
224 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
196 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
225 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
197 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
226 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
198
227
199 parent.reload
228 parent.reload
200 assert_equal 4, parent.children.size
229 assert_equal 4, parent.children.size
201 assert_equal parent.children.sort_by(&:name), parent.children
230 assert_equal parent.children.sort_by(&:name), parent.children
202 end
231 end
203
232
204 def test_rebuild_should_sort_children_alphabetically
233 def test_rebuild_should_sort_children_alphabetically
205 ProjectCustomField.delete_all
234 ProjectCustomField.delete_all
206 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
235 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
207 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
236 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
208 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
237 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
209 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
238 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
210 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
239 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
211
240
212 Project.update_all("lft = NULL, rgt = NULL")
241 Project.update_all("lft = NULL, rgt = NULL")
213 Project.rebuild!
242 Project.rebuild!
214
243
215 parent.reload
244 parent.reload
216 assert_equal 4, parent.children.size
245 assert_equal 4, parent.children.size
217 assert_equal parent.children.sort_by(&:name), parent.children
246 assert_equal parent.children.sort_by(&:name), parent.children
218 end
247 end
219
248
220
249
221 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
250 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
222 # Parent issue with a hierarchy project's fixed version
251 # Parent issue with a hierarchy project's fixed version
223 parent_issue = Issue.find(1)
252 parent_issue = Issue.find(1)
224 parent_issue.update_attribute(:fixed_version_id, 4)
253 parent_issue.update_attribute(:fixed_version_id, 4)
225 parent_issue.reload
254 parent_issue.reload
226 assert_equal 4, parent_issue.fixed_version_id
255 assert_equal 4, parent_issue.fixed_version_id
227
256
228 # Should keep fixed versions for the issues
257 # Should keep fixed versions for the issues
229 issue_with_local_fixed_version = Issue.find(5)
258 issue_with_local_fixed_version = Issue.find(5)
230 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
259 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
231 issue_with_local_fixed_version.reload
260 issue_with_local_fixed_version.reload
232 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
261 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
233
262
234 # Local issue with hierarchy fixed_version
263 # Local issue with hierarchy fixed_version
235 issue_with_hierarchy_fixed_version = Issue.find(13)
264 issue_with_hierarchy_fixed_version = Issue.find(13)
236 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
265 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
237 issue_with_hierarchy_fixed_version.reload
266 issue_with_hierarchy_fixed_version.reload
238 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
267 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
239
268
240 # Move project out of the issue's hierarchy
269 # Move project out of the issue's hierarchy
241 moved_project = Project.find(3)
270 moved_project = Project.find(3)
242 moved_project.set_parent!(Project.find(2))
271 moved_project.set_parent!(Project.find(2))
243 parent_issue.reload
272 parent_issue.reload
244 issue_with_local_fixed_version.reload
273 issue_with_local_fixed_version.reload
245 issue_with_hierarchy_fixed_version.reload
274 issue_with_hierarchy_fixed_version.reload
246
275
247 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
276 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
248 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"
277 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"
249 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
278 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
250 end
279 end
251
280
252 def test_parent
281 def test_parent
253 p = Project.find(6).parent
282 p = Project.find(6).parent
254 assert p.is_a?(Project)
283 assert p.is_a?(Project)
255 assert_equal 5, p.id
284 assert_equal 5, p.id
256 end
285 end
257
286
258 def test_ancestors
287 def test_ancestors
259 a = Project.find(6).ancestors
288 a = Project.find(6).ancestors
260 assert a.first.is_a?(Project)
289 assert a.first.is_a?(Project)
261 assert_equal [1, 5], a.collect(&:id)
290 assert_equal [1, 5], a.collect(&:id)
262 end
291 end
263
292
264 def test_root
293 def test_root
265 r = Project.find(6).root
294 r = Project.find(6).root
266 assert r.is_a?(Project)
295 assert r.is_a?(Project)
267 assert_equal 1, r.id
296 assert_equal 1, r.id
268 end
297 end
269
298
270 def test_children
299 def test_children
271 c = Project.find(1).children
300 c = Project.find(1).children
272 assert c.first.is_a?(Project)
301 assert c.first.is_a?(Project)
273 assert_equal [5, 3, 4], c.collect(&:id)
302 assert_equal [5, 3, 4], c.collect(&:id)
274 end
303 end
275
304
276 def test_descendants
305 def test_descendants
277 d = Project.find(1).descendants
306 d = Project.find(1).descendants
278 assert d.first.is_a?(Project)
307 assert d.first.is_a?(Project)
279 assert_equal [5, 6, 3, 4], d.collect(&:id)
308 assert_equal [5, 6, 3, 4], d.collect(&:id)
280 end
309 end
281
310
282 def test_allowed_parents_should_be_empty_for_non_member_user
311 def test_allowed_parents_should_be_empty_for_non_member_user
283 Role.non_member.add_permission!(:add_project)
312 Role.non_member.add_permission!(:add_project)
284 user = User.find(9)
313 user = User.find(9)
285 assert user.memberships.empty?
314 assert user.memberships.empty?
286 User.current = user
315 User.current = user
287 assert Project.new.allowed_parents.compact.empty?
316 assert Project.new.allowed_parents.compact.empty?
288 end
317 end
289
318
290 def test_allowed_parents_with_add_subprojects_permission
319 def test_allowed_parents_with_add_subprojects_permission
291 Role.find(1).remove_permission!(:add_project)
320 Role.find(1).remove_permission!(:add_project)
292 Role.find(1).add_permission!(:add_subprojects)
321 Role.find(1).add_permission!(:add_subprojects)
293 User.current = User.find(2)
322 User.current = User.find(2)
294 # new project
323 # new project
295 assert !Project.new.allowed_parents.include?(nil)
324 assert !Project.new.allowed_parents.include?(nil)
296 assert Project.new.allowed_parents.include?(Project.find(1))
325 assert Project.new.allowed_parents.include?(Project.find(1))
297 # existing root project
326 # existing root project
298 assert Project.find(1).allowed_parents.include?(nil)
327 assert Project.find(1).allowed_parents.include?(nil)
299 # existing child
328 # existing child
300 assert Project.find(3).allowed_parents.include?(Project.find(1))
329 assert Project.find(3).allowed_parents.include?(Project.find(1))
301 assert !Project.find(3).allowed_parents.include?(nil)
330 assert !Project.find(3).allowed_parents.include?(nil)
302 end
331 end
303
332
304 def test_allowed_parents_with_add_project_permission
333 def test_allowed_parents_with_add_project_permission
305 Role.find(1).add_permission!(:add_project)
334 Role.find(1).add_permission!(:add_project)
306 Role.find(1).remove_permission!(:add_subprojects)
335 Role.find(1).remove_permission!(:add_subprojects)
307 User.current = User.find(2)
336 User.current = User.find(2)
308 # new project
337 # new project
309 assert Project.new.allowed_parents.include?(nil)
338 assert Project.new.allowed_parents.include?(nil)
310 assert !Project.new.allowed_parents.include?(Project.find(1))
339 assert !Project.new.allowed_parents.include?(Project.find(1))
311 # existing root project
340 # existing root project
312 assert Project.find(1).allowed_parents.include?(nil)
341 assert Project.find(1).allowed_parents.include?(nil)
313 # existing child
342 # existing child
314 assert Project.find(3).allowed_parents.include?(Project.find(1))
343 assert Project.find(3).allowed_parents.include?(Project.find(1))
315 assert Project.find(3).allowed_parents.include?(nil)
344 assert Project.find(3).allowed_parents.include?(nil)
316 end
345 end
317
346
318 def test_allowed_parents_with_add_project_and_subprojects_permission
347 def test_allowed_parents_with_add_project_and_subprojects_permission
319 Role.find(1).add_permission!(:add_project)
348 Role.find(1).add_permission!(:add_project)
320 Role.find(1).add_permission!(:add_subprojects)
349 Role.find(1).add_permission!(:add_subprojects)
321 User.current = User.find(2)
350 User.current = User.find(2)
322 # new project
351 # new project
323 assert Project.new.allowed_parents.include?(nil)
352 assert Project.new.allowed_parents.include?(nil)
324 assert Project.new.allowed_parents.include?(Project.find(1))
353 assert Project.new.allowed_parents.include?(Project.find(1))
325 # existing root project
354 # existing root project
326 assert Project.find(1).allowed_parents.include?(nil)
355 assert Project.find(1).allowed_parents.include?(nil)
327 # existing child
356 # existing child
328 assert Project.find(3).allowed_parents.include?(Project.find(1))
357 assert Project.find(3).allowed_parents.include?(Project.find(1))
329 assert Project.find(3).allowed_parents.include?(nil)
358 assert Project.find(3).allowed_parents.include?(nil)
330 end
359 end
331
360
332 def test_users_by_role
361 def test_users_by_role
333 users_by_role = Project.find(1).users_by_role
362 users_by_role = Project.find(1).users_by_role
334 assert_kind_of Hash, users_by_role
363 assert_kind_of Hash, users_by_role
335 role = Role.find(1)
364 role = Role.find(1)
336 assert_kind_of Array, users_by_role[role]
365 assert_kind_of Array, users_by_role[role]
337 assert users_by_role[role].include?(User.find(2))
366 assert users_by_role[role].include?(User.find(2))
338 end
367 end
339
368
340 def test_rolled_up_trackers
369 def test_rolled_up_trackers
341 parent = Project.find(1)
370 parent = Project.find(1)
342 parent.trackers = Tracker.find([1,2])
371 parent.trackers = Tracker.find([1,2])
343 child = parent.children.find(3)
372 child = parent.children.find(3)
344
373
345 assert_equal [1, 2], parent.tracker_ids
374 assert_equal [1, 2], parent.tracker_ids
346 assert_equal [2, 3], child.trackers.collect(&:id)
375 assert_equal [2, 3], child.trackers.collect(&:id)
347
376
348 assert_kind_of Tracker, parent.rolled_up_trackers.first
377 assert_kind_of Tracker, parent.rolled_up_trackers.first
349 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
378 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
350
379
351 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
380 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
352 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
381 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
353 end
382 end
354
383
355 def test_rolled_up_trackers_should_ignore_archived_subprojects
384 def test_rolled_up_trackers_should_ignore_archived_subprojects
356 parent = Project.find(1)
385 parent = Project.find(1)
357 parent.trackers = Tracker.find([1,2])
386 parent.trackers = Tracker.find([1,2])
358 child = parent.children.find(3)
387 child = parent.children.find(3)
359 child.trackers = Tracker.find([1,3])
388 child.trackers = Tracker.find([1,3])
360 parent.children.each(&:archive)
389 parent.children.each(&:archive)
361
390
362 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
391 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
363 end
392 end
364
393
365 context "#rolled_up_versions" do
394 context "#rolled_up_versions" do
366 setup do
395 setup do
367 @project = Project.generate!
396 @project = Project.generate!
368 @parent_version_1 = Version.generate!(:project => @project)
397 @parent_version_1 = Version.generate!(:project => @project)
369 @parent_version_2 = Version.generate!(:project => @project)
398 @parent_version_2 = Version.generate!(:project => @project)
370 end
399 end
371
400
372 should "include the versions for the current project" do
401 should "include the versions for the current project" do
373 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
402 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
374 end
403 end
375
404
376 should "include versions for a subproject" do
405 should "include versions for a subproject" do
377 @subproject = Project.generate!
406 @subproject = Project.generate!
378 @subproject.set_parent!(@project)
407 @subproject.set_parent!(@project)
379 @subproject_version = Version.generate!(:project => @subproject)
408 @subproject_version = Version.generate!(:project => @subproject)
380
409
381 assert_same_elements [
410 assert_same_elements [
382 @parent_version_1,
411 @parent_version_1,
383 @parent_version_2,
412 @parent_version_2,
384 @subproject_version
413 @subproject_version
385 ], @project.rolled_up_versions
414 ], @project.rolled_up_versions
386 end
415 end
387
416
388 should "include versions for a sub-subproject" do
417 should "include versions for a sub-subproject" do
389 @subproject = Project.generate!
418 @subproject = Project.generate!
390 @subproject.set_parent!(@project)
419 @subproject.set_parent!(@project)
391 @sub_subproject = Project.generate!
420 @sub_subproject = Project.generate!
392 @sub_subproject.set_parent!(@subproject)
421 @sub_subproject.set_parent!(@subproject)
393 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
422 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
394
423
395 @project.reload
424 @project.reload
396
425
397 assert_same_elements [
426 assert_same_elements [
398 @parent_version_1,
427 @parent_version_1,
399 @parent_version_2,
428 @parent_version_2,
400 @sub_subproject_version
429 @sub_subproject_version
401 ], @project.rolled_up_versions
430 ], @project.rolled_up_versions
402 end
431 end
403
432
404
433
405 should "only check active projects" do
434 should "only check active projects" do
406 @subproject = Project.generate!
435 @subproject = Project.generate!
407 @subproject.set_parent!(@project)
436 @subproject.set_parent!(@project)
408 @subproject_version = Version.generate!(:project => @subproject)
437 @subproject_version = Version.generate!(:project => @subproject)
409 assert @subproject.archive
438 assert @subproject.archive
410
439
411 @project.reload
440 @project.reload
412
441
413 assert !@subproject.active?
442 assert !@subproject.active?
414 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
443 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
415 end
444 end
416 end
445 end
417
446
418 def test_shared_versions_none_sharing
447 def test_shared_versions_none_sharing
419 p = Project.find(5)
448 p = Project.find(5)
420 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
449 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
421 assert p.shared_versions.include?(v)
450 assert p.shared_versions.include?(v)
422 assert !p.children.first.shared_versions.include?(v)
451 assert !p.children.first.shared_versions.include?(v)
423 assert !p.root.shared_versions.include?(v)
452 assert !p.root.shared_versions.include?(v)
424 assert !p.siblings.first.shared_versions.include?(v)
453 assert !p.siblings.first.shared_versions.include?(v)
425 assert !p.root.siblings.first.shared_versions.include?(v)
454 assert !p.root.siblings.first.shared_versions.include?(v)
426 end
455 end
427
456
428 def test_shared_versions_descendants_sharing
457 def test_shared_versions_descendants_sharing
429 p = Project.find(5)
458 p = Project.find(5)
430 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
459 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
431 assert p.shared_versions.include?(v)
460 assert p.shared_versions.include?(v)
432 assert p.children.first.shared_versions.include?(v)
461 assert p.children.first.shared_versions.include?(v)
433 assert !p.root.shared_versions.include?(v)
462 assert !p.root.shared_versions.include?(v)
434 assert !p.siblings.first.shared_versions.include?(v)
463 assert !p.siblings.first.shared_versions.include?(v)
435 assert !p.root.siblings.first.shared_versions.include?(v)
464 assert !p.root.siblings.first.shared_versions.include?(v)
436 end
465 end
437
466
438 def test_shared_versions_hierarchy_sharing
467 def test_shared_versions_hierarchy_sharing
439 p = Project.find(5)
468 p = Project.find(5)
440 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
469 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
441 assert p.shared_versions.include?(v)
470 assert p.shared_versions.include?(v)
442 assert p.children.first.shared_versions.include?(v)
471 assert p.children.first.shared_versions.include?(v)
443 assert p.root.shared_versions.include?(v)
472 assert p.root.shared_versions.include?(v)
444 assert !p.siblings.first.shared_versions.include?(v)
473 assert !p.siblings.first.shared_versions.include?(v)
445 assert !p.root.siblings.first.shared_versions.include?(v)
474 assert !p.root.siblings.first.shared_versions.include?(v)
446 end
475 end
447
476
448 def test_shared_versions_tree_sharing
477 def test_shared_versions_tree_sharing
449 p = Project.find(5)
478 p = Project.find(5)
450 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
479 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
451 assert p.shared_versions.include?(v)
480 assert p.shared_versions.include?(v)
452 assert p.children.first.shared_versions.include?(v)
481 assert p.children.first.shared_versions.include?(v)
453 assert p.root.shared_versions.include?(v)
482 assert p.root.shared_versions.include?(v)
454 assert p.siblings.first.shared_versions.include?(v)
483 assert p.siblings.first.shared_versions.include?(v)
455 assert !p.root.siblings.first.shared_versions.include?(v)
484 assert !p.root.siblings.first.shared_versions.include?(v)
456 end
485 end
457
486
458 def test_shared_versions_system_sharing
487 def test_shared_versions_system_sharing
459 p = Project.find(5)
488 p = Project.find(5)
460 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
489 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
461 assert p.shared_versions.include?(v)
490 assert p.shared_versions.include?(v)
462 assert p.children.first.shared_versions.include?(v)
491 assert p.children.first.shared_versions.include?(v)
463 assert p.root.shared_versions.include?(v)
492 assert p.root.shared_versions.include?(v)
464 assert p.siblings.first.shared_versions.include?(v)
493 assert p.siblings.first.shared_versions.include?(v)
465 assert p.root.siblings.first.shared_versions.include?(v)
494 assert p.root.siblings.first.shared_versions.include?(v)
466 end
495 end
467
496
468 def test_shared_versions
497 def test_shared_versions
469 parent = Project.find(1)
498 parent = Project.find(1)
470 child = parent.children.find(3)
499 child = parent.children.find(3)
471 private_child = parent.children.find(5)
500 private_child = parent.children.find(5)
472
501
473 assert_equal [1,2,3], parent.version_ids.sort
502 assert_equal [1,2,3], parent.version_ids.sort
474 assert_equal [4], child.version_ids
503 assert_equal [4], child.version_ids
475 assert_equal [6], private_child.version_ids
504 assert_equal [6], private_child.version_ids
476 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
505 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
477
506
478 assert_equal 6, parent.shared_versions.size
507 assert_equal 6, parent.shared_versions.size
479 parent.shared_versions.each do |version|
508 parent.shared_versions.each do |version|
480 assert_kind_of Version, version
509 assert_kind_of Version, version
481 end
510 end
482
511
483 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
512 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
484 end
513 end
485
514
486 def test_shared_versions_should_ignore_archived_subprojects
515 def test_shared_versions_should_ignore_archived_subprojects
487 parent = Project.find(1)
516 parent = Project.find(1)
488 child = parent.children.find(3)
517 child = parent.children.find(3)
489 child.archive
518 child.archive
490 parent.reload
519 parent.reload
491
520
492 assert_equal [1,2,3], parent.version_ids.sort
521 assert_equal [1,2,3], parent.version_ids.sort
493 assert_equal [4], child.version_ids
522 assert_equal [4], child.version_ids
494 assert !parent.shared_versions.collect(&:id).include?(4)
523 assert !parent.shared_versions.collect(&:id).include?(4)
495 end
524 end
496
525
497 def test_shared_versions_visible_to_user
526 def test_shared_versions_visible_to_user
498 user = User.find(3)
527 user = User.find(3)
499 parent = Project.find(1)
528 parent = Project.find(1)
500 child = parent.children.find(5)
529 child = parent.children.find(5)
501
530
502 assert_equal [1,2,3], parent.version_ids.sort
531 assert_equal [1,2,3], parent.version_ids.sort
503 assert_equal [6], child.version_ids
532 assert_equal [6], child.version_ids
504
533
505 versions = parent.shared_versions.visible(user)
534 versions = parent.shared_versions.visible(user)
506
535
507 assert_equal 4, versions.size
536 assert_equal 4, versions.size
508 versions.each do |version|
537 versions.each do |version|
509 assert_kind_of Version, version
538 assert_kind_of Version, version
510 end
539 end
511
540
512 assert !versions.collect(&:id).include?(6)
541 assert !versions.collect(&:id).include?(6)
513 end
542 end
514
543
515
544
516 def test_next_identifier
545 def test_next_identifier
517 ProjectCustomField.delete_all
546 ProjectCustomField.delete_all
518 Project.create!(:name => 'last', :identifier => 'p2008040')
547 Project.create!(:name => 'last', :identifier => 'p2008040')
519 assert_equal 'p2008041', Project.next_identifier
548 assert_equal 'p2008041', Project.next_identifier
520 end
549 end
521
550
522 def test_next_identifier_first_project
551 def test_next_identifier_first_project
523 Project.delete_all
552 Project.delete_all
524 assert_nil Project.next_identifier
553 assert_nil Project.next_identifier
525 end
554 end
526
555
527
556
528 def test_enabled_module_names_should_not_recreate_enabled_modules
557 def test_enabled_module_names_should_not_recreate_enabled_modules
529 project = Project.find(1)
558 project = Project.find(1)
530 # Remove one module
559 # Remove one module
531 modules = project.enabled_modules.slice(0..-2)
560 modules = project.enabled_modules.slice(0..-2)
532 assert modules.any?
561 assert modules.any?
533 assert_difference 'EnabledModule.count', -1 do
562 assert_difference 'EnabledModule.count', -1 do
534 project.enabled_module_names = modules.collect(&:name)
563 project.enabled_module_names = modules.collect(&:name)
535 end
564 end
536 project.reload
565 project.reload
537 # Ids should be preserved
566 # Ids should be preserved
538 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
567 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
539 end
568 end
540
569
541 def test_copy_from_existing_project
570 def test_copy_from_existing_project
542 source_project = Project.find(1)
571 source_project = Project.find(1)
543 copied_project = Project.copy_from(1)
572 copied_project = Project.copy_from(1)
544
573
545 assert copied_project
574 assert copied_project
546 # Cleared attributes
575 # Cleared attributes
547 assert copied_project.id.blank?
576 assert copied_project.id.blank?
548 assert copied_project.name.blank?
577 assert copied_project.name.blank?
549 assert copied_project.identifier.blank?
578 assert copied_project.identifier.blank?
550
579
551 # Duplicated attributes
580 # Duplicated attributes
552 assert_equal source_project.description, copied_project.description
581 assert_equal source_project.description, copied_project.description
553 assert_equal source_project.enabled_modules, copied_project.enabled_modules
582 assert_equal source_project.enabled_modules, copied_project.enabled_modules
554 assert_equal source_project.trackers, copied_project.trackers
583 assert_equal source_project.trackers, copied_project.trackers
555
584
556 # Default attributes
585 # Default attributes
557 assert_equal 1, copied_project.status
586 assert_equal 1, copied_project.status
558 end
587 end
559
588
560 def test_activities_should_use_the_system_activities
589 def test_activities_should_use_the_system_activities
561 project = Project.find(1)
590 project = Project.find(1)
562 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
591 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
563 end
592 end
564
593
565
594
566 def test_activities_should_use_the_project_specific_activities
595 def test_activities_should_use_the_project_specific_activities
567 project = Project.find(1)
596 project = Project.find(1)
568 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
597 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
569 assert overridden_activity.save!
598 assert overridden_activity.save!
570
599
571 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
600 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
572 end
601 end
573
602
574 def test_activities_should_not_include_the_inactive_project_specific_activities
603 def test_activities_should_not_include_the_inactive_project_specific_activities
575 project = Project.find(1)
604 project = Project.find(1)
576 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
605 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
577 assert overridden_activity.save!
606 assert overridden_activity.save!
578
607
579 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
608 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
580 end
609 end
581
610
582 def test_activities_should_not_include_project_specific_activities_from_other_projects
611 def test_activities_should_not_include_project_specific_activities_from_other_projects
583 project = Project.find(1)
612 project = Project.find(1)
584 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
613 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
585 assert overridden_activity.save!
614 assert overridden_activity.save!
586
615
587 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
616 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
588 end
617 end
589
618
590 def test_activities_should_handle_nils
619 def test_activities_should_handle_nils
591 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
620 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
592 TimeEntryActivity.delete_all
621 TimeEntryActivity.delete_all
593
622
594 # No activities
623 # No activities
595 project = Project.find(1)
624 project = Project.find(1)
596 assert project.activities.empty?
625 assert project.activities.empty?
597
626
598 # No system, one overridden
627 # No system, one overridden
599 assert overridden_activity.save!
628 assert overridden_activity.save!
600 project.reload
629 project.reload
601 assert_equal [overridden_activity], project.activities
630 assert_equal [overridden_activity], project.activities
602 end
631 end
603
632
604 def test_activities_should_override_system_activities_with_project_activities
633 def test_activities_should_override_system_activities_with_project_activities
605 project = Project.find(1)
634 project = Project.find(1)
606 parent_activity = TimeEntryActivity.find(:first)
635 parent_activity = TimeEntryActivity.find(:first)
607 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
636 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
608 assert overridden_activity.save!
637 assert overridden_activity.save!
609
638
610 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
639 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
611 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
640 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
612 end
641 end
613
642
614 def test_activities_should_include_inactive_activities_if_specified
643 def test_activities_should_include_inactive_activities_if_specified
615 project = Project.find(1)
644 project = Project.find(1)
616 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
645 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
617 assert overridden_activity.save!
646 assert overridden_activity.save!
618
647
619 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
648 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
620 end
649 end
621
650
622 test 'activities should not include active System activities if the project has an override that is inactive' do
651 test 'activities should not include active System activities if the project has an override that is inactive' do
623 project = Project.find(1)
652 project = Project.find(1)
624 system_activity = TimeEntryActivity.find_by_name('Design')
653 system_activity = TimeEntryActivity.find_by_name('Design')
625 assert system_activity.active?
654 assert system_activity.active?
626 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
655 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
627 assert overridden_activity.save!
656 assert overridden_activity.save!
628
657
629 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
658 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
630 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
659 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
631 end
660 end
632
661
633 def test_close_completed_versions
662 def test_close_completed_versions
634 Version.update_all("status = 'open'")
663 Version.update_all("status = 'open'")
635 project = Project.find(1)
664 project = Project.find(1)
636 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
665 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
637 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
666 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
638 project.close_completed_versions
667 project.close_completed_versions
639 project.reload
668 project.reload
640 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
669 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
641 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
670 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
642 end
671 end
643
672
644 context "Project#copy" do
673 context "Project#copy" do
645 setup do
674 setup do
646 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
675 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
647 Project.destroy_all :identifier => "copy-test"
676 Project.destroy_all :identifier => "copy-test"
648 @source_project = Project.find(2)
677 @source_project = Project.find(2)
649 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
678 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
650 @project.trackers = @source_project.trackers
679 @project.trackers = @source_project.trackers
651 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
680 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
652 end
681 end
653
682
654 should "copy issues" do
683 should "copy issues" do
655 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
684 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
656 :subject => "copy issue status",
685 :subject => "copy issue status",
657 :tracker_id => 1,
686 :tracker_id => 1,
658 :assigned_to_id => 2,
687 :assigned_to_id => 2,
659 :project_id => @source_project.id)
688 :project_id => @source_project.id)
660 assert @project.valid?
689 assert @project.valid?
661 assert @project.issues.empty?
690 assert @project.issues.empty?
662 assert @project.copy(@source_project)
691 assert @project.copy(@source_project)
663
692
664 assert_equal @source_project.issues.size, @project.issues.size
693 assert_equal @source_project.issues.size, @project.issues.size
665 @project.issues.each do |issue|
694 @project.issues.each do |issue|
666 assert issue.valid?
695 assert issue.valid?
667 assert ! issue.assigned_to.blank?
696 assert ! issue.assigned_to.blank?
668 assert_equal @project, issue.project
697 assert_equal @project, issue.project
669 end
698 end
670
699
671 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
700 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
672 assert copied_issue
701 assert copied_issue
673 assert copied_issue.status
702 assert copied_issue.status
674 assert_equal "Closed", copied_issue.status.name
703 assert_equal "Closed", copied_issue.status.name
675 end
704 end
676
705
677 should "change the new issues to use the copied version" do
706 should "change the new issues to use the copied version" do
678 User.current = User.find(1)
707 User.current = User.find(1)
679 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
708 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
680 @source_project.versions << assigned_version
709 @source_project.versions << assigned_version
681 assert_equal 3, @source_project.versions.size
710 assert_equal 3, @source_project.versions.size
682 Issue.generate_for_project!(@source_project,
711 Issue.generate_for_project!(@source_project,
683 :fixed_version_id => assigned_version.id,
712 :fixed_version_id => assigned_version.id,
684 :subject => "change the new issues to use the copied version",
713 :subject => "change the new issues to use the copied version",
685 :tracker_id => 1,
714 :tracker_id => 1,
686 :project_id => @source_project.id)
715 :project_id => @source_project.id)
687
716
688 assert @project.copy(@source_project)
717 assert @project.copy(@source_project)
689 @project.reload
718 @project.reload
690 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
719 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
691
720
692 assert copied_issue
721 assert copied_issue
693 assert copied_issue.fixed_version
722 assert copied_issue.fixed_version
694 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
723 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
695 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
724 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
696 end
725 end
697
726
698 should "copy issue relations" do
727 should "copy issue relations" do
699 Setting.cross_project_issue_relations = '1'
728 Setting.cross_project_issue_relations = '1'
700
729
701 second_issue = Issue.generate!(:status_id => 5,
730 second_issue = Issue.generate!(:status_id => 5,
702 :subject => "copy issue relation",
731 :subject => "copy issue relation",
703 :tracker_id => 1,
732 :tracker_id => 1,
704 :assigned_to_id => 2,
733 :assigned_to_id => 2,
705 :project_id => @source_project.id)
734 :project_id => @source_project.id)
706 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
735 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
707 :issue_to => second_issue,
736 :issue_to => second_issue,
708 :relation_type => "relates")
737 :relation_type => "relates")
709 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
738 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
710 :issue_to => second_issue,
739 :issue_to => second_issue,
711 :relation_type => "duplicates")
740 :relation_type => "duplicates")
712
741
713 assert @project.copy(@source_project)
742 assert @project.copy(@source_project)
714 assert_equal @source_project.issues.count, @project.issues.count
743 assert_equal @source_project.issues.count, @project.issues.count
715 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
744 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
716 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
745 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
717
746
718 # First issue with a relation on project
747 # First issue with a relation on project
719 assert_equal 1, copied_issue.relations.size, "Relation not copied"
748 assert_equal 1, copied_issue.relations.size, "Relation not copied"
720 copied_relation = copied_issue.relations.first
749 copied_relation = copied_issue.relations.first
721 assert_equal "relates", copied_relation.relation_type
750 assert_equal "relates", copied_relation.relation_type
722 assert_equal copied_second_issue.id, copied_relation.issue_to_id
751 assert_equal copied_second_issue.id, copied_relation.issue_to_id
723 assert_not_equal source_relation.id, copied_relation.id
752 assert_not_equal source_relation.id, copied_relation.id
724
753
725 # Second issue with a cross project relation
754 # Second issue with a cross project relation
726 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
755 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
727 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
756 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
728 assert_equal "duplicates", copied_relation.relation_type
757 assert_equal "duplicates", copied_relation.relation_type
729 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
758 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
730 assert_not_equal source_relation_cross_project.id, copied_relation.id
759 assert_not_equal source_relation_cross_project.id, copied_relation.id
731 end
760 end
732
761
733 should "copy memberships" do
762 should "copy memberships" do
734 assert @project.valid?
763 assert @project.valid?
735 assert @project.members.empty?
764 assert @project.members.empty?
736 assert @project.copy(@source_project)
765 assert @project.copy(@source_project)
737
766
738 assert_equal @source_project.memberships.size, @project.memberships.size
767 assert_equal @source_project.memberships.size, @project.memberships.size
739 @project.memberships.each do |membership|
768 @project.memberships.each do |membership|
740 assert membership
769 assert membership
741 assert_equal @project, membership.project
770 assert_equal @project, membership.project
742 end
771 end
743 end
772 end
744
773
745 should "copy project specific queries" do
774 should "copy project specific queries" do
746 assert @project.valid?
775 assert @project.valid?
747 assert @project.queries.empty?
776 assert @project.queries.empty?
748 assert @project.copy(@source_project)
777 assert @project.copy(@source_project)
749
778
750 assert_equal @source_project.queries.size, @project.queries.size
779 assert_equal @source_project.queries.size, @project.queries.size
751 @project.queries.each do |query|
780 @project.queries.each do |query|
752 assert query
781 assert query
753 assert_equal @project, query.project
782 assert_equal @project, query.project
754 end
783 end
755 end
784 end
756
785
757 should "copy versions" do
786 should "copy versions" do
758 @source_project.versions << Version.generate!
787 @source_project.versions << Version.generate!
759 @source_project.versions << Version.generate!
788 @source_project.versions << Version.generate!
760
789
761 assert @project.versions.empty?
790 assert @project.versions.empty?
762 assert @project.copy(@source_project)
791 assert @project.copy(@source_project)
763
792
764 assert_equal @source_project.versions.size, @project.versions.size
793 assert_equal @source_project.versions.size, @project.versions.size
765 @project.versions.each do |version|
794 @project.versions.each do |version|
766 assert version
795 assert version
767 assert_equal @project, version.project
796 assert_equal @project, version.project
768 end
797 end
769 end
798 end
770
799
771 should "copy wiki" do
800 should "copy wiki" do
772 assert_difference 'Wiki.count' do
801 assert_difference 'Wiki.count' do
773 assert @project.copy(@source_project)
802 assert @project.copy(@source_project)
774 end
803 end
775
804
776 assert @project.wiki
805 assert @project.wiki
777 assert_not_equal @source_project.wiki, @project.wiki
806 assert_not_equal @source_project.wiki, @project.wiki
778 assert_equal "Start page", @project.wiki.start_page
807 assert_equal "Start page", @project.wiki.start_page
779 end
808 end
780
809
781 should "copy wiki pages and content with hierarchy" do
810 should "copy wiki pages and content with hierarchy" do
782 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
811 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
783 assert @project.copy(@source_project)
812 assert @project.copy(@source_project)
784 end
813 end
785
814
786 assert @project.wiki
815 assert @project.wiki
787 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
816 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
788
817
789 @project.wiki.pages.each do |wiki_page|
818 @project.wiki.pages.each do |wiki_page|
790 assert wiki_page.content
819 assert wiki_page.content
791 assert !@source_project.wiki.pages.include?(wiki_page)
820 assert !@source_project.wiki.pages.include?(wiki_page)
792 end
821 end
793
822
794 parent = @project.wiki.find_page('Parent_page')
823 parent = @project.wiki.find_page('Parent_page')
795 child1 = @project.wiki.find_page('Child_page_1')
824 child1 = @project.wiki.find_page('Child_page_1')
796 child2 = @project.wiki.find_page('Child_page_2')
825 child2 = @project.wiki.find_page('Child_page_2')
797 assert_equal parent, child1.parent
826 assert_equal parent, child1.parent
798 assert_equal parent, child2.parent
827 assert_equal parent, child2.parent
799 end
828 end
800
829
801 should "copy issue categories" do
830 should "copy issue categories" do
802 assert @project.copy(@source_project)
831 assert @project.copy(@source_project)
803
832
804 assert_equal 2, @project.issue_categories.size
833 assert_equal 2, @project.issue_categories.size
805 @project.issue_categories.each do |issue_category|
834 @project.issue_categories.each do |issue_category|
806 assert !@source_project.issue_categories.include?(issue_category)
835 assert !@source_project.issue_categories.include?(issue_category)
807 end
836 end
808 end
837 end
809
838
810 should "copy boards" do
839 should "copy boards" do
811 assert @project.copy(@source_project)
840 assert @project.copy(@source_project)
812
841
813 assert_equal 1, @project.boards.size
842 assert_equal 1, @project.boards.size
814 @project.boards.each do |board|
843 @project.boards.each do |board|
815 assert !@source_project.boards.include?(board)
844 assert !@source_project.boards.include?(board)
816 end
845 end
817 end
846 end
818
847
819 should "change the new issues to use the copied issue categories" do
848 should "change the new issues to use the copied issue categories" do
820 issue = Issue.find(4)
849 issue = Issue.find(4)
821 issue.update_attribute(:category_id, 3)
850 issue.update_attribute(:category_id, 3)
822
851
823 assert @project.copy(@source_project)
852 assert @project.copy(@source_project)
824
853
825 @project.issues.each do |issue|
854 @project.issues.each do |issue|
826 assert issue.category
855 assert issue.category
827 assert_equal "Stock management", issue.category.name # Same name
856 assert_equal "Stock management", issue.category.name # Same name
828 assert_not_equal IssueCategory.find(3), issue.category # Different record
857 assert_not_equal IssueCategory.find(3), issue.category # Different record
829 end
858 end
830 end
859 end
831
860
832 should "limit copy with :only option" do
861 should "limit copy with :only option" do
833 assert @project.members.empty?
862 assert @project.members.empty?
834 assert @project.issue_categories.empty?
863 assert @project.issue_categories.empty?
835 assert @source_project.issues.any?
864 assert @source_project.issues.any?
836
865
837 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
866 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
838
867
839 assert @project.members.any?
868 assert @project.members.any?
840 assert @project.issue_categories.any?
869 assert @project.issue_categories.any?
841 assert @project.issues.empty?
870 assert @project.issues.empty?
842 end
871 end
843
872
844 end
873 end
845
874
846 context "#start_date" do
875 context "#start_date" do
847 setup do
876 setup do
848 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
877 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
849 @project = Project.generate!(:identifier => 'test0')
878 @project = Project.generate!(:identifier => 'test0')
850 @project.trackers << Tracker.generate!
879 @project.trackers << Tracker.generate!
851 end
880 end
852
881
853 should "be nil if there are no issues on the project" do
882 should "be nil if there are no issues on the project" do
854 assert_nil @project.start_date
883 assert_nil @project.start_date
855 end
884 end
856
885
857 should "be nil if issue tracking is disabled" do
886 should "be nil if issue tracking is disabled" do
858 Issue.generate_for_project!(@project, :start_date => Date.today)
887 Issue.generate_for_project!(@project, :start_date => Date.today)
859 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
888 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
860 @project.reload
889 @project.reload
861
890
862 assert_nil @project.start_date
891 assert_nil @project.start_date
863 end
892 end
864
893
865 should "be tested when issues have no start date"
894 should "be tested when issues have no start date"
866
895
867 should "be the earliest start date of it's issues" do
896 should "be the earliest start date of it's issues" do
868 early = 7.days.ago.to_date
897 early = 7.days.ago.to_date
869 Issue.generate_for_project!(@project, :start_date => Date.today)
898 Issue.generate_for_project!(@project, :start_date => Date.today)
870 Issue.generate_for_project!(@project, :start_date => early)
899 Issue.generate_for_project!(@project, :start_date => early)
871
900
872 assert_equal early, @project.start_date
901 assert_equal early, @project.start_date
873 end
902 end
874
903
875 end
904 end
876
905
877 context "#due_date" do
906 context "#due_date" do
878 setup do
907 setup do
879 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
908 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
880 @project = Project.generate!(:identifier => 'test0')
909 @project = Project.generate!(:identifier => 'test0')
881 @project.trackers << Tracker.generate!
910 @project.trackers << Tracker.generate!
882 end
911 end
883
912
884 should "be nil if there are no issues on the project" do
913 should "be nil if there are no issues on the project" do
885 assert_nil @project.due_date
914 assert_nil @project.due_date
886 end
915 end
887
916
888 should "be nil if issue tracking is disabled" do
917 should "be nil if issue tracking is disabled" do
889 Issue.generate_for_project!(@project, :due_date => Date.today)
918 Issue.generate_for_project!(@project, :due_date => Date.today)
890 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
919 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
891 @project.reload
920 @project.reload
892
921
893 assert_nil @project.due_date
922 assert_nil @project.due_date
894 end
923 end
895
924
896 should "be tested when issues have no due date"
925 should "be tested when issues have no due date"
897
926
898 should "be the latest due date of it's issues" do
927 should "be the latest due date of it's issues" do
899 future = 7.days.from_now.to_date
928 future = 7.days.from_now.to_date
900 Issue.generate_for_project!(@project, :due_date => future)
929 Issue.generate_for_project!(@project, :due_date => future)
901 Issue.generate_for_project!(@project, :due_date => Date.today)
930 Issue.generate_for_project!(@project, :due_date => Date.today)
902
931
903 assert_equal future, @project.due_date
932 assert_equal future, @project.due_date
904 end
933 end
905
934
906 should "be the latest due date of it's versions" do
935 should "be the latest due date of it's versions" do
907 future = 7.days.from_now.to_date
936 future = 7.days.from_now.to_date
908 @project.versions << Version.generate!(:effective_date => future)
937 @project.versions << Version.generate!(:effective_date => future)
909 @project.versions << Version.generate!(:effective_date => Date.today)
938 @project.versions << Version.generate!(:effective_date => Date.today)
910
939
911
940
912 assert_equal future, @project.due_date
941 assert_equal future, @project.due_date
913
942
914 end
943 end
915
944
916 should "pick the latest date from it's issues and versions" do
945 should "pick the latest date from it's issues and versions" do
917 future = 7.days.from_now.to_date
946 future = 7.days.from_now.to_date
918 far_future = 14.days.from_now.to_date
947 far_future = 14.days.from_now.to_date
919 Issue.generate_for_project!(@project, :due_date => far_future)
948 Issue.generate_for_project!(@project, :due_date => far_future)
920 @project.versions << Version.generate!(:effective_date => future)
949 @project.versions << Version.generate!(:effective_date => future)
921
950
922 assert_equal far_future, @project.due_date
951 assert_equal far_future, @project.due_date
923 end
952 end
924
953
925 end
954 end
926
955
927 context "Project#completed_percent" do
956 context "Project#completed_percent" do
928 setup do
957 setup do
929 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
958 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
930 @project = Project.generate!(:identifier => 'test0')
959 @project = Project.generate!(:identifier => 'test0')
931 @project.trackers << Tracker.generate!
960 @project.trackers << Tracker.generate!
932 end
961 end
933
962
934 context "no versions" do
963 context "no versions" do
935 should "be 100" do
964 should "be 100" do
936 assert_equal 100, @project.completed_percent
965 assert_equal 100, @project.completed_percent
937 end
966 end
938 end
967 end
939
968
940 context "with versions" do
969 context "with versions" do
941 should "return 0 if the versions have no issues" do
970 should "return 0 if the versions have no issues" do
942 Version.generate!(:project => @project)
971 Version.generate!(:project => @project)
943 Version.generate!(:project => @project)
972 Version.generate!(:project => @project)
944
973
945 assert_equal 0, @project.completed_percent
974 assert_equal 0, @project.completed_percent
946 end
975 end
947
976
948 should "return 100 if the version has only closed issues" do
977 should "return 100 if the version has only closed issues" do
949 v1 = Version.generate!(:project => @project)
978 v1 = Version.generate!(:project => @project)
950 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
979 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
951 v2 = Version.generate!(:project => @project)
980 v2 = Version.generate!(:project => @project)
952 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
981 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
953
982
954 assert_equal 100, @project.completed_percent
983 assert_equal 100, @project.completed_percent
955 end
984 end
956
985
957 should "return the averaged completed percent of the versions (not weighted)" do
986 should "return the averaged completed percent of the versions (not weighted)" do
958 v1 = Version.generate!(:project => @project)
987 v1 = Version.generate!(:project => @project)
959 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
988 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
960 v2 = Version.generate!(:project => @project)
989 v2 = Version.generate!(:project => @project)
961 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
990 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
962
991
963 assert_equal 50, @project.completed_percent
992 assert_equal 50, @project.completed_percent
964 end
993 end
965
994
966 end
995 end
967 end
996 end
968
997
969 context "#notified_users" do
998 context "#notified_users" do
970 setup do
999 setup do
971 @project = Project.generate!
1000 @project = Project.generate!
972 @role = Role.generate!
1001 @role = Role.generate!
973
1002
974 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1003 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
975 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1004 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
976
1005
977 @all_events_user = User.generate!(:mail_notification => 'all')
1006 @all_events_user = User.generate!(:mail_notification => 'all')
978 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1007 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
979
1008
980 @no_events_user = User.generate!(:mail_notification => 'none')
1009 @no_events_user = User.generate!(:mail_notification => 'none')
981 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1010 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
982
1011
983 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1012 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
984 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1013 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
985
1014
986 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1015 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
987 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1016 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
988
1017
989 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1018 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
990 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1019 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
991 end
1020 end
992
1021
993 should "include members with a mail notification" do
1022 should "include members with a mail notification" do
994 assert @project.notified_users.include?(@user_with_membership_notification)
1023 assert @project.notified_users.include?(@user_with_membership_notification)
995 end
1024 end
996
1025
997 should "include users with the 'all' notification option" do
1026 should "include users with the 'all' notification option" do
998 assert @project.notified_users.include?(@all_events_user)
1027 assert @project.notified_users.include?(@all_events_user)
999 end
1028 end
1000
1029
1001 should "not include users with the 'none' notification option" do
1030 should "not include users with the 'none' notification option" do
1002 assert !@project.notified_users.include?(@no_events_user)
1031 assert !@project.notified_users.include?(@no_events_user)
1003 end
1032 end
1004
1033
1005 should "not include users with the 'only_my_events' notification option" do
1034 should "not include users with the 'only_my_events' notification option" do
1006 assert !@project.notified_users.include?(@only_my_events_user)
1035 assert !@project.notified_users.include?(@only_my_events_user)
1007 end
1036 end
1008
1037
1009 should "not include users with the 'only_assigned' notification option" do
1038 should "not include users with the 'only_assigned' notification option" do
1010 assert !@project.notified_users.include?(@only_assigned_user)
1039 assert !@project.notified_users.include?(@only_assigned_user)
1011 end
1040 end
1012
1041
1013 should "not include users with the 'only_owner' notification option" do
1042 should "not include users with the 'only_owner' notification option" do
1014 assert !@project.notified_users.include?(@only_owned_user)
1043 assert !@project.notified_users.include?(@only_owned_user)
1015 end
1044 end
1016 end
1045 end
1017
1046
1018 end
1047 end
General Comments 0
You need to be logged in to leave comments. Login now