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