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