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