##// END OF EJS Templates
Enable global time logging at /time_entries/new (#10020)....
Jean-Philippe Lang -
r8571:41eab6615b31
parent child
Show More
@@ -1,330 +1,337
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 before_filter :find_project, :only => [:new, :create]
20
21 before_filter :find_project, :only => [:create]
21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :authorize, :except => [:index, :report]
24 before_filter :authorize, :except => [:new, :index, :report]
24 before_filter :find_optional_project, :only => [:index, :report]
25
26 before_filter :find_optional_project, :only => [:new, :index, :report]
27 before_filter :authorize_global, :only => [:new, :index, :report]
28
25 accept_rss_auth :index
29 accept_rss_auth :index
26 accept_api_auth :index, :show, :create, :update, :destroy
30 accept_api_auth :index, :show, :create, :update, :destroy
27
31
28 helper :sort
32 helper :sort
29 include SortHelper
33 include SortHelper
30 helper :issues
34 helper :issues
31 include TimelogHelper
35 include TimelogHelper
32 helper :custom_fields
36 helper :custom_fields
33 include CustomFieldsHelper
37 include CustomFieldsHelper
34
38
35 def index
39 def index
36 sort_init 'spent_on', 'desc'
40 sort_init 'spent_on', 'desc'
37 sort_update 'spent_on' => 'spent_on',
41 sort_update 'spent_on' => 'spent_on',
38 'user' => 'user_id',
42 'user' => 'user_id',
39 'activity' => 'activity_id',
43 'activity' => 'activity_id',
40 'project' => "#{Project.table_name}.name",
44 'project' => "#{Project.table_name}.name",
41 'issue' => 'issue_id',
45 'issue' => 'issue_id',
42 'hours' => 'hours'
46 'hours' => 'hours'
43
47
44 retrieve_date_range
48 retrieve_date_range
45
49
46 scope = TimeEntry.visible.spent_between(@from, @to)
50 scope = TimeEntry.visible.spent_between(@from, @to)
47 if @issue
51 if @issue
48 scope = scope.on_issue(@issue)
52 scope = scope.on_issue(@issue)
49 elsif @project
53 elsif @project
50 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
54 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
51 end
55 end
52
56
53 respond_to do |format|
57 respond_to do |format|
54 format.html {
58 format.html {
55 # Paginate results
59 # Paginate results
56 @entry_count = scope.count
60 @entry_count = scope.count
57 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
61 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
58 @entries = scope.all(
62 @entries = scope.all(
59 :include => [:project, :activity, :user, {:issue => :tracker}],
63 :include => [:project, :activity, :user, {:issue => :tracker}],
60 :order => sort_clause,
64 :order => sort_clause,
61 :limit => @entry_pages.items_per_page,
65 :limit => @entry_pages.items_per_page,
62 :offset => @entry_pages.current.offset
66 :offset => @entry_pages.current.offset
63 )
67 )
64 @total_hours = scope.sum(:hours).to_f
68 @total_hours = scope.sum(:hours).to_f
65
69
66 render :layout => !request.xhr?
70 render :layout => !request.xhr?
67 }
71 }
68 format.api {
72 format.api {
69 @entry_count = scope.count
73 @entry_count = scope.count
70 @offset, @limit = api_offset_and_limit
74 @offset, @limit = api_offset_and_limit
71 @entries = scope.all(
75 @entries = scope.all(
72 :include => [:project, :activity, :user, {:issue => :tracker}],
76 :include => [:project, :activity, :user, {:issue => :tracker}],
73 :order => sort_clause,
77 :order => sort_clause,
74 :limit => @limit,
78 :limit => @limit,
75 :offset => @offset
79 :offset => @offset
76 )
80 )
77 }
81 }
78 format.atom {
82 format.atom {
79 entries = scope.all(
83 entries = scope.all(
80 :include => [:project, :activity, :user, {:issue => :tracker}],
84 :include => [:project, :activity, :user, {:issue => :tracker}],
81 :order => "#{TimeEntry.table_name}.created_on DESC",
85 :order => "#{TimeEntry.table_name}.created_on DESC",
82 :limit => Setting.feeds_limit.to_i
86 :limit => Setting.feeds_limit.to_i
83 )
87 )
84 render_feed(entries, :title => l(:label_spent_time))
88 render_feed(entries, :title => l(:label_spent_time))
85 }
89 }
86 format.csv {
90 format.csv {
87 # Export all entries
91 # Export all entries
88 @entries = scope.all(
92 @entries = scope.all(
89 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
93 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
90 :order => sort_clause
94 :order => sort_clause
91 )
95 )
92 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
96 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
93 }
97 }
94 end
98 end
95 end
99 end
96
100
97 def report
101 def report
98 retrieve_date_range
102 retrieve_date_range
99 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
103 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
100
104
101 respond_to do |format|
105 respond_to do |format|
102 format.html { render :layout => !request.xhr? }
106 format.html { render :layout => !request.xhr? }
103 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
107 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
104 end
108 end
105 end
109 end
106
110
107 def show
111 def show
108 respond_to do |format|
112 respond_to do |format|
109 # TODO: Implement html response
113 # TODO: Implement html response
110 format.html { render :nothing => true, :status => 406 }
114 format.html { render :nothing => true, :status => 406 }
111 format.api
115 format.api
112 end
116 end
113 end
117 end
114
118
115 def new
119 def new
116 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
120 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
117 @time_entry.attributes = params[:time_entry]
121 @time_entry.attributes = params[:time_entry]
118 end
122 end
119
123
120 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
124 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
121 def create
125 def create
122 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
126 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
123 @time_entry.attributes = params[:time_entry]
127 @time_entry.attributes = params[:time_entry]
124
128
125 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
129 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
126
130
127 if @time_entry.save
131 if @time_entry.save
128 respond_to do |format|
132 respond_to do |format|
129 format.html {
133 format.html {
130 flash[:notice] = l(:notice_successful_create)
134 flash[:notice] = l(:notice_successful_create)
131 if params[:continue]
135 if params[:continue]
132 redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue
136 if params[:project_id]
137 redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue
138 else
139 redirect_to :action => 'new'
140 end
133 else
141 else
134 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
142 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
135 end
143 end
136 }
144 }
137 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
145 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
138 end
146 end
139 else
147 else
140 respond_to do |format|
148 respond_to do |format|
141 format.html { render :action => 'edit' }
149 format.html { render :action => 'new' }
142 format.api { render_validation_errors(@time_entry) }
150 format.api { render_validation_errors(@time_entry) }
143 end
151 end
144 end
152 end
145 end
153 end
146
154
147 def edit
155 def edit
148 @time_entry.attributes = params[:time_entry]
156 @time_entry.attributes = params[:time_entry]
149 end
157 end
150
158
151 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
159 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
152 def update
160 def update
153 @time_entry.attributes = params[:time_entry]
161 @time_entry.attributes = params[:time_entry]
154
162
155 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
163 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
156
164
157 if @time_entry.save
165 if @time_entry.save
158 respond_to do |format|
166 respond_to do |format|
159 format.html {
167 format.html {
160 flash[:notice] = l(:notice_successful_update)
168 flash[:notice] = l(:notice_successful_update)
161 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
169 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
162 }
170 }
163 format.api { head :ok }
171 format.api { head :ok }
164 end
172 end
165 else
173 else
166 respond_to do |format|
174 respond_to do |format|
167 format.html { render :action => 'edit' }
175 format.html { render :action => 'edit' }
168 format.api { render_validation_errors(@time_entry) }
176 format.api { render_validation_errors(@time_entry) }
169 end
177 end
170 end
178 end
171 end
179 end
172
180
173 def bulk_edit
181 def bulk_edit
174 @available_activities = TimeEntryActivity.shared.active
182 @available_activities = TimeEntryActivity.shared.active
175 @custom_fields = TimeEntry.first.available_custom_fields
183 @custom_fields = TimeEntry.first.available_custom_fields
176 end
184 end
177
185
178 def bulk_update
186 def bulk_update
179 attributes = parse_params_for_bulk_time_entry_attributes(params)
187 attributes = parse_params_for_bulk_time_entry_attributes(params)
180
188
181 unsaved_time_entry_ids = []
189 unsaved_time_entry_ids = []
182 @time_entries.each do |time_entry|
190 @time_entries.each do |time_entry|
183 time_entry.reload
191 time_entry.reload
184 time_entry.attributes = attributes
192 time_entry.attributes = attributes
185 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
193 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
186 unless time_entry.save
194 unless time_entry.save
187 # Keep unsaved time_entry ids to display them in flash error
195 # Keep unsaved time_entry ids to display them in flash error
188 unsaved_time_entry_ids << time_entry.id
196 unsaved_time_entry_ids << time_entry.id
189 end
197 end
190 end
198 end
191 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
199 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
192 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
200 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
193 end
201 end
194
202
195 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
203 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
196 def destroy
204 def destroy
197 @time_entries.each do |t|
205 @time_entries.each do |t|
198 begin
206 begin
199 unless t.destroy && t.destroyed?
207 unless t.destroy && t.destroyed?
200 respond_to do |format|
208 respond_to do |format|
201 format.html {
209 format.html {
202 flash[:error] = l(:notice_unable_delete_time_entry)
210 flash[:error] = l(:notice_unable_delete_time_entry)
203 redirect_to :back
211 redirect_to :back
204 }
212 }
205 format.api { render_validation_errors(t) }
213 format.api { render_validation_errors(t) }
206 end
214 end
207 return
215 return
208 end
216 end
209 rescue ::ActionController::RedirectBackError
217 rescue ::ActionController::RedirectBackError
210 redirect_to :action => 'index', :project_id => @projects.first
218 redirect_to :action => 'index', :project_id => @projects.first
211 return
219 return
212 end
220 end
213 end
221 end
214
222
215 respond_to do |format|
223 respond_to do |format|
216 format.html {
224 format.html {
217 flash[:notice] = l(:notice_successful_delete)
225 flash[:notice] = l(:notice_successful_delete)
218 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
226 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
219 }
227 }
220 format.api { head :ok }
228 format.api { head :ok }
221 end
229 end
222 end
230 end
223
231
224 private
232 private
225 def find_time_entry
233 def find_time_entry
226 @time_entry = TimeEntry.find(params[:id])
234 @time_entry = TimeEntry.find(params[:id])
227 unless @time_entry.editable_by?(User.current)
235 unless @time_entry.editable_by?(User.current)
228 render_403
236 render_403
229 return false
237 return false
230 end
238 end
231 @project = @time_entry.project
239 @project = @time_entry.project
232 rescue ActiveRecord::RecordNotFound
240 rescue ActiveRecord::RecordNotFound
233 render_404
241 render_404
234 end
242 end
235
243
236 def find_time_entries
244 def find_time_entries
237 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
245 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
238 raise ActiveRecord::RecordNotFound if @time_entries.empty?
246 raise ActiveRecord::RecordNotFound if @time_entries.empty?
239 @projects = @time_entries.collect(&:project).compact.uniq
247 @projects = @time_entries.collect(&:project).compact.uniq
240 @project = @projects.first if @projects.size == 1
248 @project = @projects.first if @projects.size == 1
241 rescue ActiveRecord::RecordNotFound
249 rescue ActiveRecord::RecordNotFound
242 render_404
250 render_404
243 end
251 end
244
252
245 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
253 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
246 if unsaved_time_entry_ids.empty?
254 if unsaved_time_entry_ids.empty?
247 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
255 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
248 else
256 else
249 flash[:error] = l(:notice_failed_to_save_time_entries,
257 flash[:error] = l(:notice_failed_to_save_time_entries,
250 :count => unsaved_time_entry_ids.size,
258 :count => unsaved_time_entry_ids.size,
251 :total => time_entries.size,
259 :total => time_entries.size,
252 :ids => '#' + unsaved_time_entry_ids.join(', #'))
260 :ids => '#' + unsaved_time_entry_ids.join(', #'))
253 end
261 end
254 end
262 end
255
263
256 def find_project
264 def find_project
257 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
265 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
258 @issue = Issue.find(issue_id)
266 @issue = Issue.find(issue_id)
259 @project = @issue.project
267 @project = @issue.project
260 elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
268 elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
261 @project = Project.find(project_id)
269 @project = Project.find(project_id)
262 else
270 else
263 render_404
271 render_404
264 return false
272 return false
265 end
273 end
266 rescue ActiveRecord::RecordNotFound
274 rescue ActiveRecord::RecordNotFound
267 render_404
275 render_404
268 end
276 end
269
277
270 def find_optional_project
278 def find_optional_project
271 if !params[:issue_id].blank?
279 if !params[:issue_id].blank?
272 @issue = Issue.find(params[:issue_id])
280 @issue = Issue.find(params[:issue_id])
273 @project = @issue.project
281 @project = @issue.project
274 elsif !params[:project_id].blank?
282 elsif !params[:project_id].blank?
275 @project = Project.find(params[:project_id])
283 @project = Project.find(params[:project_id])
276 end
284 end
277 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
278 end
285 end
279
286
280 # Retrieves the date range based on predefined ranges or specific from/to param dates
287 # Retrieves the date range based on predefined ranges or specific from/to param dates
281 def retrieve_date_range
288 def retrieve_date_range
282 @free_period = false
289 @free_period = false
283 @from, @to = nil, nil
290 @from, @to = nil, nil
284
291
285 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
292 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
286 case params[:period].to_s
293 case params[:period].to_s
287 when 'today'
294 when 'today'
288 @from = @to = Date.today
295 @from = @to = Date.today
289 when 'yesterday'
296 when 'yesterday'
290 @from = @to = Date.today - 1
297 @from = @to = Date.today - 1
291 when 'current_week'
298 when 'current_week'
292 @from = Date.today - (Date.today.cwday - 1)%7
299 @from = Date.today - (Date.today.cwday - 1)%7
293 @to = @from + 6
300 @to = @from + 6
294 when 'last_week'
301 when 'last_week'
295 @from = Date.today - 7 - (Date.today.cwday - 1)%7
302 @from = Date.today - 7 - (Date.today.cwday - 1)%7
296 @to = @from + 6
303 @to = @from + 6
297 when '7_days'
304 when '7_days'
298 @from = Date.today - 7
305 @from = Date.today - 7
299 @to = Date.today
306 @to = Date.today
300 when 'current_month'
307 when 'current_month'
301 @from = Date.civil(Date.today.year, Date.today.month, 1)
308 @from = Date.civil(Date.today.year, Date.today.month, 1)
302 @to = (@from >> 1) - 1
309 @to = (@from >> 1) - 1
303 when 'last_month'
310 when 'last_month'
304 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
311 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
305 @to = (@from >> 1) - 1
312 @to = (@from >> 1) - 1
306 when '30_days'
313 when '30_days'
307 @from = Date.today - 30
314 @from = Date.today - 30
308 @to = Date.today
315 @to = Date.today
309 when 'current_year'
316 when 'current_year'
310 @from = Date.civil(Date.today.year, 1, 1)
317 @from = Date.civil(Date.today.year, 1, 1)
311 @to = Date.civil(Date.today.year, 12, 31)
318 @to = Date.civil(Date.today.year, 12, 31)
312 end
319 end
313 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
320 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
314 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
321 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
315 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
322 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
316 @free_period = true
323 @free_period = true
317 else
324 else
318 # default
325 # default
319 end
326 end
320
327
321 @from, @to = @to, @from if @from && @to && @from > @to
328 @from, @to = @to, @from if @from && @to && @from > @to
322 end
329 end
323
330
324 def parse_params_for_bulk_time_entry_attributes(params)
331 def parse_params_for_bulk_time_entry_attributes(params)
325 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
332 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
326 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
333 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
327 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
334 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
328 attributes
335 attributes
329 end
336 end
330 end
337 end
@@ -1,889 +1,900
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author
46 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :conditions => ["is_default = ?", true]
49 has_one :repository, :conditions => ["is_default = ?", true]
50 has_many :repositories, :dependent => :destroy
50 has_many :repositories, :dependent => :destroy
51 has_many :changesets, :through => :repository
51 has_many :changesets, :through => :repository
52 has_one :wiki, :dependent => :destroy
52 has_one :wiki, :dependent => :destroy
53 # Custom field for the project issues
53 # Custom field for the project issues
54 has_and_belongs_to_many :issue_custom_fields,
54 has_and_belongs_to_many :issue_custom_fields,
55 :class_name => 'IssueCustomField',
55 :class_name => 'IssueCustomField',
56 :order => "#{CustomField.table_name}.position",
56 :order => "#{CustomField.table_name}.position",
57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :association_foreign_key => 'custom_field_id'
58 :association_foreign_key => 'custom_field_id'
59
59
60 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_attachable :view_permission => :view_files,
61 acts_as_attachable :view_permission => :view_files,
62 :delete_permission => :manage_files
62 :delete_permission => :manage_files
63
63
64 acts_as_customizable
64 acts_as_customizable
65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :author => nil
68 :author => nil
69
69
70 attr_protected :status
70 attr_protected :status
71
71
72 validates_presence_of :name, :identifier
72 validates_presence_of :name, :identifier
73 validates_uniqueness_of :identifier
73 validates_uniqueness_of :identifier
74 validates_associated :repository, :wiki
74 validates_associated :repository, :wiki
75 validates_length_of :name, :maximum => 255
75 validates_length_of :name, :maximum => 255
76 validates_length_of :homepage, :maximum => 255
76 validates_length_of :homepage, :maximum => 255
77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 # donwcase letters, digits, dashes but not digits only
78 # donwcase letters, digits, dashes but not digits only
79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
80 # reserved words
80 # reserved words
81 validates_exclusion_of :identifier, :in => %w( new )
81 validates_exclusion_of :identifier, :in => %w( new )
82
82
83 before_destroy :delete_all_members
83 before_destroy :delete_all_members
84
84
85 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
87 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
88 named_scope :all_public, { :conditions => { :is_public => true } }
88 named_scope :all_public, { :conditions => { :is_public => true } }
89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
89 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
90 named_scope :allowed_to, lambda {|*args|
91 user = User.current
92 permission = nil
93 if args.first.is_a?(Symbol)
94 permission = args.shift
95 else
96 user = args.shift
97 permission = args.shift
98 end
99 { :conditions => Project.allowed_to_condition(user, permission, *args) }
100 }
90 named_scope :like, lambda {|arg|
101 named_scope :like, lambda {|arg|
91 if arg.blank?
102 if arg.blank?
92 {}
103 {}
93 else
104 else
94 pattern = "%#{arg.to_s.strip.downcase}%"
105 pattern = "%#{arg.to_s.strip.downcase}%"
95 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
106 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
96 end
107 end
97 }
108 }
98
109
99 def initialize(attributes=nil, *args)
110 def initialize(attributes=nil, *args)
100 super
111 super
101
112
102 initialized = (attributes || {}).stringify_keys
113 initialized = (attributes || {}).stringify_keys
103 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
114 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
104 self.identifier = Project.next_identifier
115 self.identifier = Project.next_identifier
105 end
116 end
106 if !initialized.key?('is_public')
117 if !initialized.key?('is_public')
107 self.is_public = Setting.default_projects_public?
118 self.is_public = Setting.default_projects_public?
108 end
119 end
109 if !initialized.key?('enabled_module_names')
120 if !initialized.key?('enabled_module_names')
110 self.enabled_module_names = Setting.default_projects_modules
121 self.enabled_module_names = Setting.default_projects_modules
111 end
122 end
112 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
123 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
113 self.trackers = Tracker.all
124 self.trackers = Tracker.all
114 end
125 end
115 end
126 end
116
127
117 def identifier=(identifier)
128 def identifier=(identifier)
118 super unless identifier_frozen?
129 super unless identifier_frozen?
119 end
130 end
120
131
121 def identifier_frozen?
132 def identifier_frozen?
122 errors[:identifier].nil? && !(new_record? || identifier.blank?)
133 errors[:identifier].nil? && !(new_record? || identifier.blank?)
123 end
134 end
124
135
125 # returns latest created projects
136 # returns latest created projects
126 # non public projects will be returned only if user is a member of those
137 # non public projects will be returned only if user is a member of those
127 def self.latest(user=nil, count=5)
138 def self.latest(user=nil, count=5)
128 visible(user).find(:all, :limit => count, :order => "created_on DESC")
139 visible(user).find(:all, :limit => count, :order => "created_on DESC")
129 end
140 end
130
141
131 # Returns true if the project is visible to +user+ or to the current user.
142 # Returns true if the project is visible to +user+ or to the current user.
132 def visible?(user=User.current)
143 def visible?(user=User.current)
133 user.allowed_to?(:view_project, self)
144 user.allowed_to?(:view_project, self)
134 end
145 end
135
146
136 # Returns a SQL conditions string used to find all projects visible by the specified user.
147 # Returns a SQL conditions string used to find all projects visible by the specified user.
137 #
148 #
138 # Examples:
149 # Examples:
139 # Project.visible_condition(admin) => "projects.status = 1"
150 # Project.visible_condition(admin) => "projects.status = 1"
140 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
151 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
141 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
152 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
142 def self.visible_condition(user, options={})
153 def self.visible_condition(user, options={})
143 allowed_to_condition(user, :view_project, options)
154 allowed_to_condition(user, :view_project, options)
144 end
155 end
145
156
146 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
157 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
147 #
158 #
148 # Valid options:
159 # Valid options:
149 # * :project => limit the condition to project
160 # * :project => limit the condition to project
150 # * :with_subprojects => limit the condition to project and its subprojects
161 # * :with_subprojects => limit the condition to project and its subprojects
151 # * :member => limit the condition to the user projects
162 # * :member => limit the condition to the user projects
152 def self.allowed_to_condition(user, permission, options={})
163 def self.allowed_to_condition(user, permission, options={})
153 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
164 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
154 if perm = Redmine::AccessControl.permission(permission)
165 if perm = Redmine::AccessControl.permission(permission)
155 unless perm.project_module.nil?
166 unless perm.project_module.nil?
156 # If the permission belongs to a project module, make sure the module is enabled
167 # If the permission belongs to a project module, make sure the module is enabled
157 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
168 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
158 end
169 end
159 end
170 end
160 if options[:project]
171 if options[:project]
161 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
172 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
162 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
173 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
163 base_statement = "(#{project_statement}) AND (#{base_statement})"
174 base_statement = "(#{project_statement}) AND (#{base_statement})"
164 end
175 end
165
176
166 if user.admin?
177 if user.admin?
167 base_statement
178 base_statement
168 else
179 else
169 statement_by_role = {}
180 statement_by_role = {}
170 unless options[:member]
181 unless options[:member]
171 role = user.logged? ? Role.non_member : Role.anonymous
182 role = user.logged? ? Role.non_member : Role.anonymous
172 if role.allowed_to?(permission)
183 if role.allowed_to?(permission)
173 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
184 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
174 end
185 end
175 end
186 end
176 if user.logged?
187 if user.logged?
177 user.projects_by_role.each do |role, projects|
188 user.projects_by_role.each do |role, projects|
178 if role.allowed_to?(permission)
189 if role.allowed_to?(permission)
179 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
190 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
180 end
191 end
181 end
192 end
182 end
193 end
183 if statement_by_role.empty?
194 if statement_by_role.empty?
184 "1=0"
195 "1=0"
185 else
196 else
186 if block_given?
197 if block_given?
187 statement_by_role.each do |role, statement|
198 statement_by_role.each do |role, statement|
188 if s = yield(role, user)
199 if s = yield(role, user)
189 statement_by_role[role] = "(#{statement} AND (#{s}))"
200 statement_by_role[role] = "(#{statement} AND (#{s}))"
190 end
201 end
191 end
202 end
192 end
203 end
193 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
204 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
194 end
205 end
195 end
206 end
196 end
207 end
197
208
198 # Returns the Systemwide and project specific activities
209 # Returns the Systemwide and project specific activities
199 def activities(include_inactive=false)
210 def activities(include_inactive=false)
200 if include_inactive
211 if include_inactive
201 return all_activities
212 return all_activities
202 else
213 else
203 return active_activities
214 return active_activities
204 end
215 end
205 end
216 end
206
217
207 # Will create a new Project specific Activity or update an existing one
218 # Will create a new Project specific Activity or update an existing one
208 #
219 #
209 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
220 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
210 # does not successfully save.
221 # does not successfully save.
211 def update_or_create_time_entry_activity(id, activity_hash)
222 def update_or_create_time_entry_activity(id, activity_hash)
212 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
223 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
213 self.create_time_entry_activity_if_needed(activity_hash)
224 self.create_time_entry_activity_if_needed(activity_hash)
214 else
225 else
215 activity = project.time_entry_activities.find_by_id(id.to_i)
226 activity = project.time_entry_activities.find_by_id(id.to_i)
216 activity.update_attributes(activity_hash) if activity
227 activity.update_attributes(activity_hash) if activity
217 end
228 end
218 end
229 end
219
230
220 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
231 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
221 #
232 #
222 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
233 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
223 # does not successfully save.
234 # does not successfully save.
224 def create_time_entry_activity_if_needed(activity)
235 def create_time_entry_activity_if_needed(activity)
225 if activity['parent_id']
236 if activity['parent_id']
226
237
227 parent_activity = TimeEntryActivity.find(activity['parent_id'])
238 parent_activity = TimeEntryActivity.find(activity['parent_id'])
228 activity['name'] = parent_activity.name
239 activity['name'] = parent_activity.name
229 activity['position'] = parent_activity.position
240 activity['position'] = parent_activity.position
230
241
231 if Enumeration.overridding_change?(activity, parent_activity)
242 if Enumeration.overridding_change?(activity, parent_activity)
232 project_activity = self.time_entry_activities.create(activity)
243 project_activity = self.time_entry_activities.create(activity)
233
244
234 if project_activity.new_record?
245 if project_activity.new_record?
235 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
246 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
236 else
247 else
237 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
248 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
238 end
249 end
239 end
250 end
240 end
251 end
241 end
252 end
242
253
243 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
254 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
244 #
255 #
245 # Examples:
256 # Examples:
246 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
257 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
247 # project.project_condition(false) => "projects.id = 1"
258 # project.project_condition(false) => "projects.id = 1"
248 def project_condition(with_subprojects)
259 def project_condition(with_subprojects)
249 cond = "#{Project.table_name}.id = #{id}"
260 cond = "#{Project.table_name}.id = #{id}"
250 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
261 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
251 cond
262 cond
252 end
263 end
253
264
254 def self.find(*args)
265 def self.find(*args)
255 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
266 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
256 project = find_by_identifier(*args)
267 project = find_by_identifier(*args)
257 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
268 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
258 project
269 project
259 else
270 else
260 super
271 super
261 end
272 end
262 end
273 end
263
274
264 def to_param
275 def to_param
265 # id is used for projects with a numeric identifier (compatibility)
276 # id is used for projects with a numeric identifier (compatibility)
266 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
277 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
267 end
278 end
268
279
269 def active?
280 def active?
270 self.status == STATUS_ACTIVE
281 self.status == STATUS_ACTIVE
271 end
282 end
272
283
273 def archived?
284 def archived?
274 self.status == STATUS_ARCHIVED
285 self.status == STATUS_ARCHIVED
275 end
286 end
276
287
277 # Archives the project and its descendants
288 # Archives the project and its descendants
278 def archive
289 def archive
279 # Check that there is no issue of a non descendant project that is assigned
290 # Check that there is no issue of a non descendant project that is assigned
280 # to one of the project or descendant versions
291 # to one of the project or descendant versions
281 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
292 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
282 if v_ids.any? && Issue.find(:first, :include => :project,
293 if v_ids.any? && Issue.find(:first, :include => :project,
283 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
294 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
284 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
295 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
285 return false
296 return false
286 end
297 end
287 Project.transaction do
298 Project.transaction do
288 archive!
299 archive!
289 end
300 end
290 true
301 true
291 end
302 end
292
303
293 # Unarchives the project
304 # Unarchives the project
294 # All its ancestors must be active
305 # All its ancestors must be active
295 def unarchive
306 def unarchive
296 return false if ancestors.detect {|a| !a.active?}
307 return false if ancestors.detect {|a| !a.active?}
297 update_attribute :status, STATUS_ACTIVE
308 update_attribute :status, STATUS_ACTIVE
298 end
309 end
299
310
300 # Returns an array of projects the project can be moved to
311 # Returns an array of projects the project can be moved to
301 # by the current user
312 # by the current user
302 def allowed_parents
313 def allowed_parents
303 return @allowed_parents if @allowed_parents
314 return @allowed_parents if @allowed_parents
304 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
315 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
305 @allowed_parents = @allowed_parents - self_and_descendants
316 @allowed_parents = @allowed_parents - self_and_descendants
306 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
317 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
307 @allowed_parents << nil
318 @allowed_parents << nil
308 end
319 end
309 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
320 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
310 @allowed_parents << parent
321 @allowed_parents << parent
311 end
322 end
312 @allowed_parents
323 @allowed_parents
313 end
324 end
314
325
315 # Sets the parent of the project with authorization check
326 # Sets the parent of the project with authorization check
316 def set_allowed_parent!(p)
327 def set_allowed_parent!(p)
317 unless p.nil? || p.is_a?(Project)
328 unless p.nil? || p.is_a?(Project)
318 if p.to_s.blank?
329 if p.to_s.blank?
319 p = nil
330 p = nil
320 else
331 else
321 p = Project.find_by_id(p)
332 p = Project.find_by_id(p)
322 return false unless p
333 return false unless p
323 end
334 end
324 end
335 end
325 if p.nil?
336 if p.nil?
326 if !new_record? && allowed_parents.empty?
337 if !new_record? && allowed_parents.empty?
327 return false
338 return false
328 end
339 end
329 elsif !allowed_parents.include?(p)
340 elsif !allowed_parents.include?(p)
330 return false
341 return false
331 end
342 end
332 set_parent!(p)
343 set_parent!(p)
333 end
344 end
334
345
335 # Sets the parent of the project
346 # Sets the parent of the project
336 # Argument can be either a Project, a String, a Fixnum or nil
347 # Argument can be either a Project, a String, a Fixnum or nil
337 def set_parent!(p)
348 def set_parent!(p)
338 unless p.nil? || p.is_a?(Project)
349 unless p.nil? || p.is_a?(Project)
339 if p.to_s.blank?
350 if p.to_s.blank?
340 p = nil
351 p = nil
341 else
352 else
342 p = Project.find_by_id(p)
353 p = Project.find_by_id(p)
343 return false unless p
354 return false unless p
344 end
355 end
345 end
356 end
346 if p == parent && !p.nil?
357 if p == parent && !p.nil?
347 # Nothing to do
358 # Nothing to do
348 true
359 true
349 elsif p.nil? || (p.active? && move_possible?(p))
360 elsif p.nil? || (p.active? && move_possible?(p))
350 # Insert the project so that target's children or root projects stay alphabetically sorted
361 # Insert the project so that target's children or root projects stay alphabetically sorted
351 sibs = (p.nil? ? self.class.roots : p.children)
362 sibs = (p.nil? ? self.class.roots : p.children)
352 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
363 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
353 if to_be_inserted_before
364 if to_be_inserted_before
354 move_to_left_of(to_be_inserted_before)
365 move_to_left_of(to_be_inserted_before)
355 elsif p.nil?
366 elsif p.nil?
356 if sibs.empty?
367 if sibs.empty?
357 # move_to_root adds the project in first (ie. left) position
368 # move_to_root adds the project in first (ie. left) position
358 move_to_root
369 move_to_root
359 else
370 else
360 move_to_right_of(sibs.last) unless self == sibs.last
371 move_to_right_of(sibs.last) unless self == sibs.last
361 end
372 end
362 else
373 else
363 # move_to_child_of adds the project in last (ie.right) position
374 # move_to_child_of adds the project in last (ie.right) position
364 move_to_child_of(p)
375 move_to_child_of(p)
365 end
376 end
366 Issue.update_versions_from_hierarchy_change(self)
377 Issue.update_versions_from_hierarchy_change(self)
367 true
378 true
368 else
379 else
369 # Can not move to the given target
380 # Can not move to the given target
370 false
381 false
371 end
382 end
372 end
383 end
373
384
374 # Returns an array of the trackers used by the project and its active sub projects
385 # Returns an array of the trackers used by the project and its active sub projects
375 def rolled_up_trackers
386 def rolled_up_trackers
376 @rolled_up_trackers ||=
387 @rolled_up_trackers ||=
377 Tracker.find(:all, :joins => :projects,
388 Tracker.find(:all, :joins => :projects,
378 :select => "DISTINCT #{Tracker.table_name}.*",
389 :select => "DISTINCT #{Tracker.table_name}.*",
379 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
390 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
380 :order => "#{Tracker.table_name}.position")
391 :order => "#{Tracker.table_name}.position")
381 end
392 end
382
393
383 # Closes open and locked project versions that are completed
394 # Closes open and locked project versions that are completed
384 def close_completed_versions
395 def close_completed_versions
385 Version.transaction do
396 Version.transaction do
386 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
397 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
387 if version.completed?
398 if version.completed?
388 version.update_attribute(:status, 'closed')
399 version.update_attribute(:status, 'closed')
389 end
400 end
390 end
401 end
391 end
402 end
392 end
403 end
393
404
394 # Returns a scope of the Versions on subprojects
405 # Returns a scope of the Versions on subprojects
395 def rolled_up_versions
406 def rolled_up_versions
396 @rolled_up_versions ||=
407 @rolled_up_versions ||=
397 Version.scoped(:include => :project,
408 Version.scoped(:include => :project,
398 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
409 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
399 end
410 end
400
411
401 # Returns a scope of the Versions used by the project
412 # Returns a scope of the Versions used by the project
402 def shared_versions
413 def shared_versions
403 @shared_versions ||= begin
414 @shared_versions ||= begin
404 r = root? ? self : root
415 r = root? ? self : root
405 Version.scoped(:include => :project,
416 Version.scoped(:include => :project,
406 :conditions => "#{Project.table_name}.id = #{id}" +
417 :conditions => "#{Project.table_name}.id = #{id}" +
407 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
418 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
408 " #{Version.table_name}.sharing = 'system'" +
419 " #{Version.table_name}.sharing = 'system'" +
409 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
420 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
410 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
421 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
411 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
422 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
412 "))")
423 "))")
413 end
424 end
414 end
425 end
415
426
416 # Returns a hash of project users grouped by role
427 # Returns a hash of project users grouped by role
417 def users_by_role
428 def users_by_role
418 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
429 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
419 m.roles.each do |r|
430 m.roles.each do |r|
420 h[r] ||= []
431 h[r] ||= []
421 h[r] << m.user
432 h[r] << m.user
422 end
433 end
423 h
434 h
424 end
435 end
425 end
436 end
426
437
427 # Deletes all project's members
438 # Deletes all project's members
428 def delete_all_members
439 def delete_all_members
429 me, mr = Member.table_name, MemberRole.table_name
440 me, mr = Member.table_name, MemberRole.table_name
430 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
441 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
431 Member.delete_all(['project_id = ?', id])
442 Member.delete_all(['project_id = ?', id])
432 end
443 end
433
444
434 # Users/groups issues can be assigned to
445 # Users/groups issues can be assigned to
435 def assignable_users
446 def assignable_users
436 assignable = Setting.issue_group_assignment? ? member_principals : members
447 assignable = Setting.issue_group_assignment? ? member_principals : members
437 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
448 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
438 end
449 end
439
450
440 # Returns the mail adresses of users that should be always notified on project events
451 # Returns the mail adresses of users that should be always notified on project events
441 def recipients
452 def recipients
442 notified_users.collect {|user| user.mail}
453 notified_users.collect {|user| user.mail}
443 end
454 end
444
455
445 # Returns the users that should be notified on project events
456 # Returns the users that should be notified on project events
446 def notified_users
457 def notified_users
447 # TODO: User part should be extracted to User#notify_about?
458 # TODO: User part should be extracted to User#notify_about?
448 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
459 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
449 end
460 end
450
461
451 # Returns an array of all custom fields enabled for project issues
462 # Returns an array of all custom fields enabled for project issues
452 # (explictly associated custom fields and custom fields enabled for all projects)
463 # (explictly associated custom fields and custom fields enabled for all projects)
453 def all_issue_custom_fields
464 def all_issue_custom_fields
454 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
465 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
455 end
466 end
456
467
457 # Returns an array of all custom fields enabled for project time entries
468 # Returns an array of all custom fields enabled for project time entries
458 # (explictly associated custom fields and custom fields enabled for all projects)
469 # (explictly associated custom fields and custom fields enabled for all projects)
459 def all_time_entry_custom_fields
470 def all_time_entry_custom_fields
460 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
471 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
461 end
472 end
462
473
463 def project
474 def project
464 self
475 self
465 end
476 end
466
477
467 def <=>(project)
478 def <=>(project)
468 name.downcase <=> project.name.downcase
479 name.downcase <=> project.name.downcase
469 end
480 end
470
481
471 def to_s
482 def to_s
472 name
483 name
473 end
484 end
474
485
475 # Returns a short description of the projects (first lines)
486 # Returns a short description of the projects (first lines)
476 def short_description(length = 255)
487 def short_description(length = 255)
477 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
488 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
478 end
489 end
479
490
480 def css_classes
491 def css_classes
481 s = 'project'
492 s = 'project'
482 s << ' root' if root?
493 s << ' root' if root?
483 s << ' child' if child?
494 s << ' child' if child?
484 s << (leaf? ? ' leaf' : ' parent')
495 s << (leaf? ? ' leaf' : ' parent')
485 s
496 s
486 end
497 end
487
498
488 # The earliest start date of a project, based on it's issues and versions
499 # The earliest start date of a project, based on it's issues and versions
489 def start_date
500 def start_date
490 [
501 [
491 issues.minimum('start_date'),
502 issues.minimum('start_date'),
492 shared_versions.collect(&:effective_date),
503 shared_versions.collect(&:effective_date),
493 shared_versions.collect(&:start_date)
504 shared_versions.collect(&:start_date)
494 ].flatten.compact.min
505 ].flatten.compact.min
495 end
506 end
496
507
497 # The latest due date of an issue or version
508 # The latest due date of an issue or version
498 def due_date
509 def due_date
499 [
510 [
500 issues.maximum('due_date'),
511 issues.maximum('due_date'),
501 shared_versions.collect(&:effective_date),
512 shared_versions.collect(&:effective_date),
502 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
513 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
503 ].flatten.compact.max
514 ].flatten.compact.max
504 end
515 end
505
516
506 def overdue?
517 def overdue?
507 active? && !due_date.nil? && (due_date < Date.today)
518 active? && !due_date.nil? && (due_date < Date.today)
508 end
519 end
509
520
510 # Returns the percent completed for this project, based on the
521 # Returns the percent completed for this project, based on the
511 # progress on it's versions.
522 # progress on it's versions.
512 def completed_percent(options={:include_subprojects => false})
523 def completed_percent(options={:include_subprojects => false})
513 if options.delete(:include_subprojects)
524 if options.delete(:include_subprojects)
514 total = self_and_descendants.collect(&:completed_percent).sum
525 total = self_and_descendants.collect(&:completed_percent).sum
515
526
516 total / self_and_descendants.count
527 total / self_and_descendants.count
517 else
528 else
518 if versions.count > 0
529 if versions.count > 0
519 total = versions.collect(&:completed_pourcent).sum
530 total = versions.collect(&:completed_pourcent).sum
520
531
521 total / versions.count
532 total / versions.count
522 else
533 else
523 100
534 100
524 end
535 end
525 end
536 end
526 end
537 end
527
538
528 # Return true if this project is allowed to do the specified action.
539 # Return true if this project is allowed to do the specified action.
529 # action can be:
540 # action can be:
530 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
541 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
531 # * a permission Symbol (eg. :edit_project)
542 # * a permission Symbol (eg. :edit_project)
532 def allows_to?(action)
543 def allows_to?(action)
533 if action.is_a? Hash
544 if action.is_a? Hash
534 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
545 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
535 else
546 else
536 allowed_permissions.include? action
547 allowed_permissions.include? action
537 end
548 end
538 end
549 end
539
550
540 def module_enabled?(module_name)
551 def module_enabled?(module_name)
541 module_name = module_name.to_s
552 module_name = module_name.to_s
542 enabled_modules.detect {|m| m.name == module_name}
553 enabled_modules.detect {|m| m.name == module_name}
543 end
554 end
544
555
545 def enabled_module_names=(module_names)
556 def enabled_module_names=(module_names)
546 if module_names && module_names.is_a?(Array)
557 if module_names && module_names.is_a?(Array)
547 module_names = module_names.collect(&:to_s).reject(&:blank?)
558 module_names = module_names.collect(&:to_s).reject(&:blank?)
548 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
559 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
549 else
560 else
550 enabled_modules.clear
561 enabled_modules.clear
551 end
562 end
552 end
563 end
553
564
554 # Returns an array of the enabled modules names
565 # Returns an array of the enabled modules names
555 def enabled_module_names
566 def enabled_module_names
556 enabled_modules.collect(&:name)
567 enabled_modules.collect(&:name)
557 end
568 end
558
569
559 # Enable a specific module
570 # Enable a specific module
560 #
571 #
561 # Examples:
572 # Examples:
562 # project.enable_module!(:issue_tracking)
573 # project.enable_module!(:issue_tracking)
563 # project.enable_module!("issue_tracking")
574 # project.enable_module!("issue_tracking")
564 def enable_module!(name)
575 def enable_module!(name)
565 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
576 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
566 end
577 end
567
578
568 # Disable a module if it exists
579 # Disable a module if it exists
569 #
580 #
570 # Examples:
581 # Examples:
571 # project.disable_module!(:issue_tracking)
582 # project.disable_module!(:issue_tracking)
572 # project.disable_module!("issue_tracking")
583 # project.disable_module!("issue_tracking")
573 # project.disable_module!(project.enabled_modules.first)
584 # project.disable_module!(project.enabled_modules.first)
574 def disable_module!(target)
585 def disable_module!(target)
575 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
586 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
576 target.destroy unless target.blank?
587 target.destroy unless target.blank?
577 end
588 end
578
589
579 safe_attributes 'name',
590 safe_attributes 'name',
580 'description',
591 'description',
581 'homepage',
592 'homepage',
582 'is_public',
593 'is_public',
583 'identifier',
594 'identifier',
584 'custom_field_values',
595 'custom_field_values',
585 'custom_fields',
596 'custom_fields',
586 'tracker_ids',
597 'tracker_ids',
587 'issue_custom_field_ids'
598 'issue_custom_field_ids'
588
599
589 safe_attributes 'enabled_module_names',
600 safe_attributes 'enabled_module_names',
590 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
601 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
591
602
592 # Returns an array of projects that are in this project's hierarchy
603 # Returns an array of projects that are in this project's hierarchy
593 #
604 #
594 # Example: parents, children, siblings
605 # Example: parents, children, siblings
595 def hierarchy
606 def hierarchy
596 parents = project.self_and_ancestors || []
607 parents = project.self_and_ancestors || []
597 descendants = project.descendants || []
608 descendants = project.descendants || []
598 project_hierarchy = parents | descendants # Set union
609 project_hierarchy = parents | descendants # Set union
599 end
610 end
600
611
601 # Returns an auto-generated project identifier based on the last identifier used
612 # Returns an auto-generated project identifier based on the last identifier used
602 def self.next_identifier
613 def self.next_identifier
603 p = Project.find(:first, :order => 'created_on DESC')
614 p = Project.find(:first, :order => 'created_on DESC')
604 p.nil? ? nil : p.identifier.to_s.succ
615 p.nil? ? nil : p.identifier.to_s.succ
605 end
616 end
606
617
607 # Copies and saves the Project instance based on the +project+.
618 # Copies and saves the Project instance based on the +project+.
608 # Duplicates the source project's:
619 # Duplicates the source project's:
609 # * Wiki
620 # * Wiki
610 # * Versions
621 # * Versions
611 # * Categories
622 # * Categories
612 # * Issues
623 # * Issues
613 # * Members
624 # * Members
614 # * Queries
625 # * Queries
615 #
626 #
616 # Accepts an +options+ argument to specify what to copy
627 # Accepts an +options+ argument to specify what to copy
617 #
628 #
618 # Examples:
629 # Examples:
619 # project.copy(1) # => copies everything
630 # project.copy(1) # => copies everything
620 # project.copy(1, :only => 'members') # => copies members only
631 # project.copy(1, :only => 'members') # => copies members only
621 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
632 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
622 def copy(project, options={})
633 def copy(project, options={})
623 project = project.is_a?(Project) ? project : Project.find(project)
634 project = project.is_a?(Project) ? project : Project.find(project)
624
635
625 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
636 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
626 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
637 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
627
638
628 Project.transaction do
639 Project.transaction do
629 if save
640 if save
630 reload
641 reload
631 to_be_copied.each do |name|
642 to_be_copied.each do |name|
632 send "copy_#{name}", project
643 send "copy_#{name}", project
633 end
644 end
634 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
645 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
635 save
646 save
636 end
647 end
637 end
648 end
638 end
649 end
639
650
640
651
641 # Copies +project+ and returns the new instance. This will not save
652 # Copies +project+ and returns the new instance. This will not save
642 # the copy
653 # the copy
643 def self.copy_from(project)
654 def self.copy_from(project)
644 begin
655 begin
645 project = project.is_a?(Project) ? project : Project.find(project)
656 project = project.is_a?(Project) ? project : Project.find(project)
646 if project
657 if project
647 # clear unique attributes
658 # clear unique attributes
648 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
659 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
649 copy = Project.new(attributes)
660 copy = Project.new(attributes)
650 copy.enabled_modules = project.enabled_modules
661 copy.enabled_modules = project.enabled_modules
651 copy.trackers = project.trackers
662 copy.trackers = project.trackers
652 copy.custom_values = project.custom_values.collect {|v| v.clone}
663 copy.custom_values = project.custom_values.collect {|v| v.clone}
653 copy.issue_custom_fields = project.issue_custom_fields
664 copy.issue_custom_fields = project.issue_custom_fields
654 return copy
665 return copy
655 else
666 else
656 return nil
667 return nil
657 end
668 end
658 rescue ActiveRecord::RecordNotFound
669 rescue ActiveRecord::RecordNotFound
659 return nil
670 return nil
660 end
671 end
661 end
672 end
662
673
663 # Yields the given block for each project with its level in the tree
674 # Yields the given block for each project with its level in the tree
664 def self.project_tree(projects, &block)
675 def self.project_tree(projects, &block)
665 ancestors = []
676 ancestors = []
666 projects.sort_by(&:lft).each do |project|
677 projects.sort_by(&:lft).each do |project|
667 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
678 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
668 ancestors.pop
679 ancestors.pop
669 end
680 end
670 yield project, ancestors.size
681 yield project, ancestors.size
671 ancestors << project
682 ancestors << project
672 end
683 end
673 end
684 end
674
685
675 private
686 private
676
687
677 # Copies wiki from +project+
688 # Copies wiki from +project+
678 def copy_wiki(project)
689 def copy_wiki(project)
679 # Check that the source project has a wiki first
690 # Check that the source project has a wiki first
680 unless project.wiki.nil?
691 unless project.wiki.nil?
681 self.wiki ||= Wiki.new
692 self.wiki ||= Wiki.new
682 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
693 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
683 wiki_pages_map = {}
694 wiki_pages_map = {}
684 project.wiki.pages.each do |page|
695 project.wiki.pages.each do |page|
685 # Skip pages without content
696 # Skip pages without content
686 next if page.content.nil?
697 next if page.content.nil?
687 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
698 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
688 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
699 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
689 new_wiki_page.content = new_wiki_content
700 new_wiki_page.content = new_wiki_content
690 wiki.pages << new_wiki_page
701 wiki.pages << new_wiki_page
691 wiki_pages_map[page.id] = new_wiki_page
702 wiki_pages_map[page.id] = new_wiki_page
692 end
703 end
693 wiki.save
704 wiki.save
694 # Reproduce page hierarchy
705 # Reproduce page hierarchy
695 project.wiki.pages.each do |page|
706 project.wiki.pages.each do |page|
696 if page.parent_id && wiki_pages_map[page.id]
707 if page.parent_id && wiki_pages_map[page.id]
697 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
708 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
698 wiki_pages_map[page.id].save
709 wiki_pages_map[page.id].save
699 end
710 end
700 end
711 end
701 end
712 end
702 end
713 end
703
714
704 # Copies versions from +project+
715 # Copies versions from +project+
705 def copy_versions(project)
716 def copy_versions(project)
706 project.versions.each do |version|
717 project.versions.each do |version|
707 new_version = Version.new
718 new_version = Version.new
708 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
719 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
709 self.versions << new_version
720 self.versions << new_version
710 end
721 end
711 end
722 end
712
723
713 # Copies issue categories from +project+
724 # Copies issue categories from +project+
714 def copy_issue_categories(project)
725 def copy_issue_categories(project)
715 project.issue_categories.each do |issue_category|
726 project.issue_categories.each do |issue_category|
716 new_issue_category = IssueCategory.new
727 new_issue_category = IssueCategory.new
717 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
728 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
718 self.issue_categories << new_issue_category
729 self.issue_categories << new_issue_category
719 end
730 end
720 end
731 end
721
732
722 # Copies issues from +project+
733 # Copies issues from +project+
723 # Note: issues assigned to a closed version won't be copied due to validation rules
734 # Note: issues assigned to a closed version won't be copied due to validation rules
724 def copy_issues(project)
735 def copy_issues(project)
725 # Stores the source issue id as a key and the copied issues as the
736 # Stores the source issue id as a key and the copied issues as the
726 # value. Used to map the two togeather for issue relations.
737 # value. Used to map the two togeather for issue relations.
727 issues_map = {}
738 issues_map = {}
728
739
729 # Get issues sorted by root_id, lft so that parent issues
740 # Get issues sorted by root_id, lft so that parent issues
730 # get copied before their children
741 # get copied before their children
731 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
742 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
732 new_issue = Issue.new
743 new_issue = Issue.new
733 new_issue.copy_from(issue)
744 new_issue.copy_from(issue)
734 new_issue.project = self
745 new_issue.project = self
735 # Reassign fixed_versions by name, since names are unique per
746 # Reassign fixed_versions by name, since names are unique per
736 # project and the versions for self are not yet saved
747 # project and the versions for self are not yet saved
737 if issue.fixed_version
748 if issue.fixed_version
738 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
749 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
739 end
750 end
740 # Reassign the category by name, since names are unique per
751 # Reassign the category by name, since names are unique per
741 # project and the categories for self are not yet saved
752 # project and the categories for self are not yet saved
742 if issue.category
753 if issue.category
743 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
754 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
744 end
755 end
745 # Parent issue
756 # Parent issue
746 if issue.parent_id
757 if issue.parent_id
747 if copied_parent = issues_map[issue.parent_id]
758 if copied_parent = issues_map[issue.parent_id]
748 new_issue.parent_issue_id = copied_parent.id
759 new_issue.parent_issue_id = copied_parent.id
749 end
760 end
750 end
761 end
751
762
752 self.issues << new_issue
763 self.issues << new_issue
753 if new_issue.new_record?
764 if new_issue.new_record?
754 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
765 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
755 else
766 else
756 issues_map[issue.id] = new_issue unless new_issue.new_record?
767 issues_map[issue.id] = new_issue unless new_issue.new_record?
757 end
768 end
758 end
769 end
759
770
760 # Relations after in case issues related each other
771 # Relations after in case issues related each other
761 project.issues.each do |issue|
772 project.issues.each do |issue|
762 new_issue = issues_map[issue.id]
773 new_issue = issues_map[issue.id]
763 unless new_issue
774 unless new_issue
764 # Issue was not copied
775 # Issue was not copied
765 next
776 next
766 end
777 end
767
778
768 # Relations
779 # Relations
769 issue.relations_from.each do |source_relation|
780 issue.relations_from.each do |source_relation|
770 new_issue_relation = IssueRelation.new
781 new_issue_relation = IssueRelation.new
771 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
782 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
772 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
783 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
773 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
784 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
774 new_issue_relation.issue_to = source_relation.issue_to
785 new_issue_relation.issue_to = source_relation.issue_to
775 end
786 end
776 new_issue.relations_from << new_issue_relation
787 new_issue.relations_from << new_issue_relation
777 end
788 end
778
789
779 issue.relations_to.each do |source_relation|
790 issue.relations_to.each do |source_relation|
780 new_issue_relation = IssueRelation.new
791 new_issue_relation = IssueRelation.new
781 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
792 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
782 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
793 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
783 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
794 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
784 new_issue_relation.issue_from = source_relation.issue_from
795 new_issue_relation.issue_from = source_relation.issue_from
785 end
796 end
786 new_issue.relations_to << new_issue_relation
797 new_issue.relations_to << new_issue_relation
787 end
798 end
788 end
799 end
789 end
800 end
790
801
791 # Copies members from +project+
802 # Copies members from +project+
792 def copy_members(project)
803 def copy_members(project)
793 # Copy users first, then groups to handle members with inherited and given roles
804 # Copy users first, then groups to handle members with inherited and given roles
794 members_to_copy = []
805 members_to_copy = []
795 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
806 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
796 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
807 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
797
808
798 members_to_copy.each do |member|
809 members_to_copy.each do |member|
799 new_member = Member.new
810 new_member = Member.new
800 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
811 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
801 # only copy non inherited roles
812 # only copy non inherited roles
802 # inherited roles will be added when copying the group membership
813 # inherited roles will be added when copying the group membership
803 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
814 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
804 next if role_ids.empty?
815 next if role_ids.empty?
805 new_member.role_ids = role_ids
816 new_member.role_ids = role_ids
806 new_member.project = self
817 new_member.project = self
807 self.members << new_member
818 self.members << new_member
808 end
819 end
809 end
820 end
810
821
811 # Copies queries from +project+
822 # Copies queries from +project+
812 def copy_queries(project)
823 def copy_queries(project)
813 project.queries.each do |query|
824 project.queries.each do |query|
814 new_query = ::Query.new
825 new_query = ::Query.new
815 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
826 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
816 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
827 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
817 new_query.project = self
828 new_query.project = self
818 new_query.user_id = query.user_id
829 new_query.user_id = query.user_id
819 self.queries << new_query
830 self.queries << new_query
820 end
831 end
821 end
832 end
822
833
823 # Copies boards from +project+
834 # Copies boards from +project+
824 def copy_boards(project)
835 def copy_boards(project)
825 project.boards.each do |board|
836 project.boards.each do |board|
826 new_board = Board.new
837 new_board = Board.new
827 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
838 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
828 new_board.project = self
839 new_board.project = self
829 self.boards << new_board
840 self.boards << new_board
830 end
841 end
831 end
842 end
832
843
833 def allowed_permissions
844 def allowed_permissions
834 @allowed_permissions ||= begin
845 @allowed_permissions ||= begin
835 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
846 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
836 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
847 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
837 end
848 end
838 end
849 end
839
850
840 def allowed_actions
851 def allowed_actions
841 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
852 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
842 end
853 end
843
854
844 # Returns all the active Systemwide and project specific activities
855 # Returns all the active Systemwide and project specific activities
845 def active_activities
856 def active_activities
846 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
857 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
847
858
848 if overridden_activity_ids.empty?
859 if overridden_activity_ids.empty?
849 return TimeEntryActivity.shared.active
860 return TimeEntryActivity.shared.active
850 else
861 else
851 return system_activities_and_project_overrides
862 return system_activities_and_project_overrides
852 end
863 end
853 end
864 end
854
865
855 # Returns all the Systemwide and project specific activities
866 # Returns all the Systemwide and project specific activities
856 # (inactive and active)
867 # (inactive and active)
857 def all_activities
868 def all_activities
858 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
869 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
859
870
860 if overridden_activity_ids.empty?
871 if overridden_activity_ids.empty?
861 return TimeEntryActivity.shared
872 return TimeEntryActivity.shared
862 else
873 else
863 return system_activities_and_project_overrides(true)
874 return system_activities_and_project_overrides(true)
864 end
875 end
865 end
876 end
866
877
867 # Returns the systemwide active activities merged with the project specific overrides
878 # Returns the systemwide active activities merged with the project specific overrides
868 def system_activities_and_project_overrides(include_inactive=false)
879 def system_activities_and_project_overrides(include_inactive=false)
869 if include_inactive
880 if include_inactive
870 return TimeEntryActivity.shared.
881 return TimeEntryActivity.shared.
871 find(:all,
882 find(:all,
872 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
883 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
873 self.time_entry_activities
884 self.time_entry_activities
874 else
885 else
875 return TimeEntryActivity.shared.active.
886 return TimeEntryActivity.shared.active.
876 find(:all,
887 find(:all,
877 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
888 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
878 self.time_entry_activities.active
889 self.time_entry_activities.active
879 end
890 end
880 end
891 end
881
892
882 # Archives subprojects recursively
893 # Archives subprojects recursively
883 def archive!
894 def archive!
884 children.each do |subproject|
895 children.each do |subproject|
885 subproject.send :archive!
896 subproject.send :archive!
886 end
897 end
887 update_attribute :status, STATUS_ARCHIVED
898 update_attribute :status, STATUS_ARCHIVED
888 end
899 end
889 end
900 end
@@ -1,14 +1,21
1 <%= error_messages_for 'time_entry' %>
1 <%= error_messages_for 'time_entry' %>
2 <%= back_url_hidden_field_tag %>
2 <%= back_url_hidden_field_tag %>
3
3
4 <div class="box tabular">
4 <div class="box tabular">
5 <% if @time_entry.new_record? %>
6 <% if params[:project_id] %>
7 <%= f.hidden_field :project_id %>
8 <% else %>
9 <p><%= f.select :project_id, project_tree_options_for_select(Project.allowed_to(:log_time).all, :selected => @time_entry.project), :required => true %></p>
10 <% end %>
11 <% end %>
5 <p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
12 <p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
6 <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
13 <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
7 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
14 <p><%= f.text_field :hours, :size => 6, :required => true %></p>
8 <p><%= f.text_field :comments, :size => 100 %></p>
15 <p><%= f.text_field :comments, :size => 100 %></p>
9 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
16 <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p>
10 <% @time_entry.custom_field_values.each do |value| %>
17 <% @time_entry.custom_field_values.each do |value| %>
11 <p><%= custom_field_tag_with_label :time_entry, value %></p>
18 <p><%= custom_field_tag_with_label :time_entry, value %></p>
12 <% end %>
19 <% end %>
13 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
20 <%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %>
14 </div>
21 </div>
@@ -1,31 +1,33
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
2 <%= link_to l(:button_log_time),
3 {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue},
4 :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project, :global => true) %>
3 </div>
5 </div>
4
6
5 <%= render_timelog_breadcrumb %>
7 <%= render_timelog_breadcrumb %>
6
8
7 <h2><%= l(:label_spent_time) %></h2>
9 <h2><%= l(:label_spent_time) %></h2>
8
10
9 <% form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
11 <% form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
10 <%= render :partial => 'date_range' %>
12 <%= render :partial => 'date_range' %>
11 <% end %>
13 <% end %>
12
14
13 <div class="total-hours">
15 <div class="total-hours">
14 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
16 <p><%= l(:label_total) %>: <%= html_hours(l_hours(@total_hours)) %></p>
15 </div>
17 </div>
16
18
17 <% unless @entries.empty? %>
19 <% unless @entries.empty? %>
18 <%= render :partial => 'list', :locals => { :entries => @entries }%>
20 <%= render :partial => 'list', :locals => { :entries => @entries }%>
19 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
21 <p class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></p>
20
22
21 <% other_formats_links do |f| %>
23 <% other_formats_links do |f| %>
22 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
24 <%= f.link_to 'Atom', :url => params.merge({:issue_id => @issue, :key => User.current.rss_key}) %>
23 <%= f.link_to 'CSV', :url => params %>
25 <%= f.link_to 'CSV', :url => params %>
24 <% end %>
26 <% end %>
25 <% end %>
27 <% end %>
26
28
27 <% html_title l(:label_spent_time), l(:label_details) %>
29 <% html_title l(:label_spent_time), l(:label_details) %>
28
30
29 <% content_for :header_tags do %>
31 <% content_for :header_tags do %>
30 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
32 <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
31 <% end %>
33 <% end %>
@@ -1,7 +1,7
1 <h2><%= l(:label_spent_time) %></h2>
1 <h2><%= l(:label_spent_time) %></h2>
2
2
3 <% labelled_form_for @time_entry, :url => project_time_entries_path(@time_entry.project) do |f| %>
3 <% labelled_form_for @time_entry, :url => time_entries_path do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
7 <% end %>
7 <% end %>
@@ -1,610 +1,704
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require File.expand_path('../../test_helper', __FILE__)
19 require File.expand_path('../../test_helper', __FILE__)
20 require 'timelog_controller'
20 require 'timelog_controller'
21
21
22 # Re-raise errors caught by the controller.
22 # Re-raise errors caught by the controller.
23 class TimelogController; def rescue_action(e) raise e end; end
23 class TimelogController; def rescue_action(e) raise e end; end
24
24
25 class TimelogControllerTest < ActionController::TestCase
25 class TimelogControllerTest < ActionController::TestCase
26 fixtures :projects, :enabled_modules, :roles, :members,
26 fixtures :projects, :enabled_modules, :roles, :members,
27 :member_roles, :issues, :time_entries, :users,
27 :member_roles, :issues, :time_entries, :users,
28 :trackers, :enumerations, :issue_statuses,
28 :trackers, :enumerations, :issue_statuses,
29 :custom_fields, :custom_values
29 :custom_fields, :custom_values
30
30
31 include Redmine::I18n
31 include Redmine::I18n
32
32
33 def setup
33 def setup
34 @controller = TimelogController.new
34 @controller = TimelogController.new
35 @request = ActionController::TestRequest.new
35 @request = ActionController::TestRequest.new
36 @response = ActionController::TestResponse.new
36 @response = ActionController::TestResponse.new
37 end
37 end
38
38
39 def test_get_new
39 def test_get_new
40 @request.session[:user_id] = 3
40 @request.session[:user_id] = 3
41 get :new, :project_id => 1
41 get :new, :project_id => 1
42 assert_response :success
42 assert_response :success
43 assert_template 'new'
43 assert_template 'new'
44 # Default activity selected
44 # Default activity selected
45 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
45 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
46 :content => 'Development'
46 :content => 'Development'
47 end
47 end
48
48
49 def test_get_new_should_only_show_active_time_entry_activities
49 def test_get_new_should_only_show_active_time_entry_activities
50 @request.session[:user_id] = 3
50 @request.session[:user_id] = 3
51 get :new, :project_id => 1
51 get :new, :project_id => 1
52 assert_response :success
52 assert_response :success
53 assert_template 'new'
53 assert_template 'new'
54 assert_no_tag :tag => 'option', :content => 'Inactive Activity'
54 assert_no_tag 'select', :attributes => {:name => 'time_entry[project_id]'}
55 assert_no_tag 'option', :content => 'Inactive Activity'
56 end
57
58 def test_new_without_project
59 @request.session[:user_id] = 3
60 get :new
61 assert_response :success
62 assert_template 'new'
63 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'}
64 end
65
66 def test_new_without_project_should_deny_without_permission
67 Role.all.each {|role| role.remove_permission! :log_time}
68 @request.session[:user_id] = 3
69
70 get :new
71 assert_response 403
55 end
72 end
56
73
57 def test_get_edit_existing_time
74 def test_get_edit_existing_time
58 @request.session[:user_id] = 2
75 @request.session[:user_id] = 2
59 get :edit, :id => 2, :project_id => nil
76 get :edit, :id => 2, :project_id => nil
60 assert_response :success
77 assert_response :success
61 assert_template 'edit'
78 assert_template 'edit'
62 # Default activity selected
79 # Default activity selected
63 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
80 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
64 end
81 end
65
82
66 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
83 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
67 te = TimeEntry.find(1)
84 te = TimeEntry.find(1)
68 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
85 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
69 te.save!
86 te.save!
70
87
71 @request.session[:user_id] = 1
88 @request.session[:user_id] = 1
72 get :edit, :project_id => 1, :id => 1
89 get :edit, :project_id => 1, :id => 1
73 assert_response :success
90 assert_response :success
74 assert_template 'edit'
91 assert_template 'edit'
75 # Blank option since nothing is pre-selected
92 # Blank option since nothing is pre-selected
76 assert_tag :tag => 'option', :content => '--- Please select ---'
93 assert_tag :tag => 'option', :content => '--- Please select ---'
77 end
94 end
78
95
79 def test_post_create
96 def test_post_create
80 # TODO: should POST to issues’ time log instead of project. change form
97 # TODO: should POST to issues’ time log instead of project. change form
81 # and routing
98 # and routing
82 @request.session[:user_id] = 3
99 @request.session[:user_id] = 3
83 post :create, :project_id => 1,
100 post :create, :project_id => 1,
84 :time_entry => {:comments => 'Some work on TimelogControllerTest',
101 :time_entry => {:comments => 'Some work on TimelogControllerTest',
85 # Not the default activity
102 # Not the default activity
86 :activity_id => '11',
103 :activity_id => '11',
87 :spent_on => '2008-03-14',
104 :spent_on => '2008-03-14',
88 :issue_id => '1',
105 :issue_id => '1',
89 :hours => '7.3'}
106 :hours => '7.3'}
90 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
107 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
91
108
92 i = Issue.find(1)
109 i = Issue.find(1)
93 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
110 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
94 assert_not_nil t
111 assert_not_nil t
95 assert_equal 11, t.activity_id
112 assert_equal 11, t.activity_id
96 assert_equal 7.3, t.hours
113 assert_equal 7.3, t.hours
97 assert_equal 3, t.user_id
114 assert_equal 3, t.user_id
98 assert_equal i, t.issue
115 assert_equal i, t.issue
99 assert_equal i.project, t.project
116 assert_equal i.project, t.project
100 end
117 end
101
118
102 def test_post_create_with_blank_issue
119 def test_post_create_with_blank_issue
103 # TODO: should POST to issues’ time log instead of project. change form
120 # TODO: should POST to issues’ time log instead of project. change form
104 # and routing
121 # and routing
105 @request.session[:user_id] = 3
122 @request.session[:user_id] = 3
106 post :create, :project_id => 1,
123 post :create, :project_id => 1,
107 :time_entry => {:comments => 'Some work on TimelogControllerTest',
124 :time_entry => {:comments => 'Some work on TimelogControllerTest',
108 # Not the default activity
125 # Not the default activity
109 :activity_id => '11',
126 :activity_id => '11',
110 :issue_id => '',
127 :issue_id => '',
111 :spent_on => '2008-03-14',
128 :spent_on => '2008-03-14',
112 :hours => '7.3'}
129 :hours => '7.3'}
113 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
130 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
114
131
115 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
132 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
116 assert_not_nil t
133 assert_not_nil t
117 assert_equal 11, t.activity_id
134 assert_equal 11, t.activity_id
118 assert_equal 7.3, t.hours
135 assert_equal 7.3, t.hours
119 assert_equal 3, t.user_id
136 assert_equal 3, t.user_id
120 end
137 end
121
138
122 def test_create_and_continue
139 def test_create_and_continue
123 @request.session[:user_id] = 2
140 @request.session[:user_id] = 2
124 post :create, :project_id => 1,
141 post :create, :project_id => 1,
125 :time_entry => {:activity_id => '11',
142 :time_entry => {:activity_id => '11',
126 :issue_id => '',
143 :issue_id => '',
127 :spent_on => '2008-03-14',
144 :spent_on => '2008-03-14',
128 :hours => '7.3'},
145 :hours => '7.3'},
129 :continue => '1'
146 :continue => '1'
130 assert_redirected_to '/projects/ecookbook/time_entries/new'
147 assert_redirected_to '/projects/ecookbook/time_entries/new'
131 end
148 end
132
149
133 def test_create_and_continue_with_issue_id
150 def test_create_and_continue_with_issue_id
134 @request.session[:user_id] = 2
151 @request.session[:user_id] = 2
135 post :create, :project_id => 1,
152 post :create, :project_id => 1,
136 :time_entry => {:activity_id => '11',
153 :time_entry => {:activity_id => '11',
137 :issue_id => '1',
154 :issue_id => '1',
138 :spent_on => '2008-03-14',
155 :spent_on => '2008-03-14',
139 :hours => '7.3'},
156 :hours => '7.3'},
140 :continue => '1'
157 :continue => '1'
141 assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new'
158 assert_redirected_to '/projects/ecookbook/issues/1/time_entries/new'
142 end
159 end
143
160
161 def test_create_and_continue_without_project
162 @request.session[:user_id] = 2
163 post :create, :time_entry => {:project_id => '1',
164 :activity_id => '11',
165 :issue_id => '',
166 :spent_on => '2008-03-14',
167 :hours => '7.3'},
168 :continue => '1'
169
170 assert_redirected_to '/time_entries/new'
171 end
172
144 def test_create_without_log_time_permission_should_be_denied
173 def test_create_without_log_time_permission_should_be_denied
145 @request.session[:user_id] = 2
174 @request.session[:user_id] = 2
146 Role.find_by_name('Manager').remove_permission! :log_time
175 Role.find_by_name('Manager').remove_permission! :log_time
147 post :create, :project_id => 1,
176 post :create, :project_id => 1,
148 :time_entry => {:activity_id => '11',
177 :time_entry => {:activity_id => '11',
149 :issue_id => '',
178 :issue_id => '',
150 :spent_on => '2008-03-14',
179 :spent_on => '2008-03-14',
151 :hours => '7.3'}
180 :hours => '7.3'}
152
181
153 assert_response 403
182 assert_response 403
154 end
183 end
155
184
185 def test_create_with_failure
186 @request.session[:user_id] = 2
187 post :create, :project_id => 1,
188 :time_entry => {:activity_id => '',
189 :issue_id => '',
190 :spent_on => '2008-03-14',
191 :hours => '7.3'}
192
193 assert_response :success
194 assert_template 'new'
195 end
196
197 def test_create_without_project
198 @request.session[:user_id] = 2
199 assert_difference 'TimeEntry.count' do
200 post :create, :time_entry => {:project_id => '1',
201 :activity_id => '11',
202 :issue_id => '',
203 :spent_on => '2008-03-14',
204 :hours => '7.3'}
205 end
206
207 assert_redirected_to '/projects/ecookbook/time_entries'
208 time_entry = TimeEntry.first(:order => 'id DESC')
209 assert_equal 1, time_entry.project_id
210 end
211
212 def test_create_without_project_should_deny_without_permission
213 @request.session[:user_id] = 2
214 Project.find(3).disable_module!(:time_tracking)
215
216 assert_no_difference 'TimeEntry.count' do
217 post :create, :time_entry => {:project_id => '3',
218 :activity_id => '11',
219 :issue_id => '',
220 :spent_on => '2008-03-14',
221 :hours => '7.3'}
222 end
223
224 assert_response 403
225 end
226
227 def test_create_without_project_with_failure
228 @request.session[:user_id] = 2
229 assert_no_difference 'TimeEntry.count' do
230 post :create, :time_entry => {:project_id => '1',
231 :activity_id => '11',
232 :issue_id => '',
233 :spent_on => '2008-03-14',
234 :hours => ''}
235 end
236
237 assert_response :success
238 assert_tag 'select', :attributes => {:name => 'time_entry[project_id]'},
239 :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}}
240 end
241
156 def test_update
242 def test_update
157 entry = TimeEntry.find(1)
243 entry = TimeEntry.find(1)
158 assert_equal 1, entry.issue_id
244 assert_equal 1, entry.issue_id
159 assert_equal 2, entry.user_id
245 assert_equal 2, entry.user_id
160
246
161 @request.session[:user_id] = 1
247 @request.session[:user_id] = 1
162 put :update, :id => 1,
248 put :update, :id => 1,
163 :time_entry => {:issue_id => '2',
249 :time_entry => {:issue_id => '2',
164 :hours => '8'}
250 :hours => '8'}
165 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
251 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
166 entry.reload
252 entry.reload
167
253
168 assert_equal 8, entry.hours
254 assert_equal 8, entry.hours
169 assert_equal 2, entry.issue_id
255 assert_equal 2, entry.issue_id
170 assert_equal 2, entry.user_id
256 assert_equal 2, entry.user_id
171 end
257 end
172
258
173 def test_get_bulk_edit
259 def test_get_bulk_edit
174 @request.session[:user_id] = 2
260 @request.session[:user_id] = 2
175 get :bulk_edit, :ids => [1, 2]
261 get :bulk_edit, :ids => [1, 2]
176 assert_response :success
262 assert_response :success
177 assert_template 'bulk_edit'
263 assert_template 'bulk_edit'
178
264
179 # System wide custom field
265 # System wide custom field
180 assert_tag :select, :attributes => {:name => 'time_entry[custom_field_values][10]'}
266 assert_tag :select, :attributes => {:name => 'time_entry[custom_field_values][10]'}
181 end
267 end
182
268
183 def test_get_bulk_edit_on_different_projects
269 def test_get_bulk_edit_on_different_projects
184 @request.session[:user_id] = 2
270 @request.session[:user_id] = 2
185 get :bulk_edit, :ids => [1, 2, 6]
271 get :bulk_edit, :ids => [1, 2, 6]
186 assert_response :success
272 assert_response :success
187 assert_template 'bulk_edit'
273 assert_template 'bulk_edit'
188 end
274 end
189
275
190 def test_bulk_update
276 def test_bulk_update
191 @request.session[:user_id] = 2
277 @request.session[:user_id] = 2
192 # update time entry activity
278 # update time entry activity
193 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
279 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
194
280
195 assert_response 302
281 assert_response 302
196 # check that the issues were updated
282 # check that the issues were updated
197 assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id}
283 assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id}
198 end
284 end
199
285
200 def test_bulk_update_with_failure
286 def test_bulk_update_with_failure
201 @request.session[:user_id] = 2
287 @request.session[:user_id] = 2
202 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
288 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
203
289
204 assert_response 302
290 assert_response 302
205 assert_match /Failed to save 2 time entrie/, flash[:error]
291 assert_match /Failed to save 2 time entrie/, flash[:error]
206 end
292 end
207
293
208 def test_bulk_update_on_different_projects
294 def test_bulk_update_on_different_projects
209 @request.session[:user_id] = 2
295 @request.session[:user_id] = 2
210 # makes user a manager on the other project
296 # makes user a manager on the other project
211 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
297 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
212
298
213 # update time entry activity
299 # update time entry activity
214 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
300 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
215
301
216 assert_response 302
302 assert_response 302
217 # check that the issues were updated
303 # check that the issues were updated
218 assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id}
304 assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id}
219 end
305 end
220
306
221 def test_bulk_update_on_different_projects_without_rights
307 def test_bulk_update_on_different_projects_without_rights
222 @request.session[:user_id] = 3
308 @request.session[:user_id] = 3
223 user = User.find(3)
309 user = User.find(3)
224 action = { :controller => "timelog", :action => "bulk_update" }
310 action = { :controller => "timelog", :action => "bulk_update" }
225 assert user.allowed_to?(action, TimeEntry.find(1).project)
311 assert user.allowed_to?(action, TimeEntry.find(1).project)
226 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
312 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
227 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
313 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
228 assert_response 403
314 assert_response 403
229 end
315 end
230
316
231 def test_bulk_update_custom_field
317 def test_bulk_update_custom_field
232 @request.session[:user_id] = 2
318 @request.session[:user_id] = 2
233 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
319 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
234
320
235 assert_response 302
321 assert_response 302
236 assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value}
322 assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value}
237 end
323 end
238
324
239 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
325 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
240 @request.session[:user_id] = 2
326 @request.session[:user_id] = 2
241 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
327 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
242
328
243 assert_response :redirect
329 assert_response :redirect
244 assert_redirected_to '/time_entries'
330 assert_redirected_to '/time_entries'
245 end
331 end
246
332
247 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
333 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
248 @request.session[:user_id] = 2
334 @request.session[:user_id] = 2
249 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
335 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
250
336
251 assert_response :redirect
337 assert_response :redirect
252 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
338 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
253 end
339 end
254
340
255 def test_post_bulk_update_without_edit_permission_should_be_denied
341 def test_post_bulk_update_without_edit_permission_should_be_denied
256 @request.session[:user_id] = 2
342 @request.session[:user_id] = 2
257 Role.find_by_name('Manager').remove_permission! :edit_time_entries
343 Role.find_by_name('Manager').remove_permission! :edit_time_entries
258 post :bulk_update, :ids => [1,2]
344 post :bulk_update, :ids => [1,2]
259
345
260 assert_response 403
346 assert_response 403
261 end
347 end
262
348
263 def test_destroy
349 def test_destroy
264 @request.session[:user_id] = 2
350 @request.session[:user_id] = 2
265 delete :destroy, :id => 1
351 delete :destroy, :id => 1
266 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
352 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
267 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
353 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
268 assert_nil TimeEntry.find_by_id(1)
354 assert_nil TimeEntry.find_by_id(1)
269 end
355 end
270
356
271 def test_destroy_should_fail
357 def test_destroy_should_fail
272 # simulate that this fails (e.g. due to a plugin), see #5700
358 # simulate that this fails (e.g. due to a plugin), see #5700
273 TimeEntry.any_instance.expects(:destroy).returns(false)
359 TimeEntry.any_instance.expects(:destroy).returns(false)
274
360
275 @request.session[:user_id] = 2
361 @request.session[:user_id] = 2
276 delete :destroy, :id => 1
362 delete :destroy, :id => 1
277 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
363 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
278 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
364 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
279 assert_not_nil TimeEntry.find_by_id(1)
365 assert_not_nil TimeEntry.find_by_id(1)
280 end
366 end
281
367
282 def test_index_all_projects
368 def test_index_all_projects
283 get :index
369 get :index
284 assert_response :success
370 assert_response :success
285 assert_template 'index'
371 assert_template 'index'
286 assert_not_nil assigns(:total_hours)
372 assert_not_nil assigns(:total_hours)
287 assert_equal "162.90", "%.2f" % assigns(:total_hours)
373 assert_equal "162.90", "%.2f" % assigns(:total_hours)
288 assert_tag :form,
374 assert_tag :form,
289 :attributes => {:action => "/time_entries", :id => 'query_form'}
375 :attributes => {:action => "/time_entries", :id => 'query_form'}
290 end
376 end
291
377
378 def test_index_all_projects_should_show_log_time_link
379 @request.session[:user_id] = 2
380 get :index
381 assert_response :success
382 assert_template 'index'
383 assert_tag 'a', :attributes => {:href => '/time_entries/new'}, :content => /Log time/
384 end
385
292 def test_index_at_project_level
386 def test_index_at_project_level
293 get :index, :project_id => 'ecookbook'
387 get :index, :project_id => 'ecookbook'
294 assert_response :success
388 assert_response :success
295 assert_template 'index'
389 assert_template 'index'
296 assert_not_nil assigns(:entries)
390 assert_not_nil assigns(:entries)
297 assert_equal 4, assigns(:entries).size
391 assert_equal 4, assigns(:entries).size
298 # project and subproject
392 # project and subproject
299 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
393 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
300 assert_not_nil assigns(:total_hours)
394 assert_not_nil assigns(:total_hours)
301 assert_equal "162.90", "%.2f" % assigns(:total_hours)
395 assert_equal "162.90", "%.2f" % assigns(:total_hours)
302 # display all time by default
396 # display all time by default
303 assert_nil assigns(:from)
397 assert_nil assigns(:from)
304 assert_nil assigns(:to)
398 assert_nil assigns(:to)
305 assert_tag :form,
399 assert_tag :form,
306 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
400 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
307 end
401 end
308
402
309 def test_index_at_project_level_with_date_range
403 def test_index_at_project_level_with_date_range
310 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
404 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
311 assert_response :success
405 assert_response :success
312 assert_template 'index'
406 assert_template 'index'
313 assert_not_nil assigns(:entries)
407 assert_not_nil assigns(:entries)
314 assert_equal 3, assigns(:entries).size
408 assert_equal 3, assigns(:entries).size
315 assert_not_nil assigns(:total_hours)
409 assert_not_nil assigns(:total_hours)
316 assert_equal "12.90", "%.2f" % assigns(:total_hours)
410 assert_equal "12.90", "%.2f" % assigns(:total_hours)
317 assert_equal '2007-03-20'.to_date, assigns(:from)
411 assert_equal '2007-03-20'.to_date, assigns(:from)
318 assert_equal '2007-04-30'.to_date, assigns(:to)
412 assert_equal '2007-04-30'.to_date, assigns(:to)
319 assert_tag :form,
413 assert_tag :form,
320 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
414 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
321 end
415 end
322
416
323 def test_index_at_project_level_with_period
417 def test_index_at_project_level_with_period
324 get :index, :project_id => 'ecookbook', :period => '7_days'
418 get :index, :project_id => 'ecookbook', :period => '7_days'
325 assert_response :success
419 assert_response :success
326 assert_template 'index'
420 assert_template 'index'
327 assert_not_nil assigns(:entries)
421 assert_not_nil assigns(:entries)
328 assert_not_nil assigns(:total_hours)
422 assert_not_nil assigns(:total_hours)
329 assert_equal Date.today - 7, assigns(:from)
423 assert_equal Date.today - 7, assigns(:from)
330 assert_equal Date.today, assigns(:to)
424 assert_equal Date.today, assigns(:to)
331 assert_tag :form,
425 assert_tag :form,
332 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
426 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
333 end
427 end
334
428
335 def test_index_one_day
429 def test_index_one_day
336 get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => "2007-03-23"
430 get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => "2007-03-23"
337 assert_response :success
431 assert_response :success
338 assert_template 'index'
432 assert_template 'index'
339 assert_not_nil assigns(:total_hours)
433 assert_not_nil assigns(:total_hours)
340 assert_equal "4.25", "%.2f" % assigns(:total_hours)
434 assert_equal "4.25", "%.2f" % assigns(:total_hours)
341 assert_tag :form,
435 assert_tag :form,
342 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
436 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
343 end
437 end
344
438
345 def test_index_today
439 def test_index_today
346 Date.stubs(:today).returns('2011-12-15'.to_date)
440 Date.stubs(:today).returns('2011-12-15'.to_date)
347 get :index, :period => 'today'
441 get :index, :period => 'today'
348 assert_equal '2011-12-15'.to_date, assigns(:from)
442 assert_equal '2011-12-15'.to_date, assigns(:from)
349 assert_equal '2011-12-15'.to_date, assigns(:to)
443 assert_equal '2011-12-15'.to_date, assigns(:to)
350 end
444 end
351
445
352 def test_index_yesterday
446 def test_index_yesterday
353 Date.stubs(:today).returns('2011-12-15'.to_date)
447 Date.stubs(:today).returns('2011-12-15'.to_date)
354 get :index, :period => 'yesterday'
448 get :index, :period => 'yesterday'
355 assert_equal '2011-12-14'.to_date, assigns(:from)
449 assert_equal '2011-12-14'.to_date, assigns(:from)
356 assert_equal '2011-12-14'.to_date, assigns(:to)
450 assert_equal '2011-12-14'.to_date, assigns(:to)
357 end
451 end
358
452
359 def test_index_current_week
453 def test_index_current_week
360 Date.stubs(:today).returns('2011-12-15'.to_date)
454 Date.stubs(:today).returns('2011-12-15'.to_date)
361 get :index, :period => 'current_week'
455 get :index, :period => 'current_week'
362 assert_equal '2011-12-12'.to_date, assigns(:from)
456 assert_equal '2011-12-12'.to_date, assigns(:from)
363 assert_equal '2011-12-18'.to_date, assigns(:to)
457 assert_equal '2011-12-18'.to_date, assigns(:to)
364 end
458 end
365
459
366 def test_index_last_week
460 def test_index_last_week
367 Date.stubs(:today).returns('2011-12-15'.to_date)
461 Date.stubs(:today).returns('2011-12-15'.to_date)
368 get :index, :period => 'current_week'
462 get :index, :period => 'current_week'
369 assert_equal '2011-12-05'.to_date, assigns(:from)
463 assert_equal '2011-12-05'.to_date, assigns(:from)
370 assert_equal '2011-12-11'.to_date, assigns(:to)
464 assert_equal '2011-12-11'.to_date, assigns(:to)
371 end
465 end
372
466
373 def test_index_last_week
467 def test_index_last_week
374 Date.stubs(:today).returns('2011-12-15'.to_date)
468 Date.stubs(:today).returns('2011-12-15'.to_date)
375 get :index, :period => 'last_week'
469 get :index, :period => 'last_week'
376 assert_equal '2011-12-05'.to_date, assigns(:from)
470 assert_equal '2011-12-05'.to_date, assigns(:from)
377 assert_equal '2011-12-11'.to_date, assigns(:to)
471 assert_equal '2011-12-11'.to_date, assigns(:to)
378 end
472 end
379
473
380 def test_index_7_days
474 def test_index_7_days
381 Date.stubs(:today).returns('2011-12-15'.to_date)
475 Date.stubs(:today).returns('2011-12-15'.to_date)
382 get :index, :period => '7_days'
476 get :index, :period => '7_days'
383 assert_equal '2011-12-08'.to_date, assigns(:from)
477 assert_equal '2011-12-08'.to_date, assigns(:from)
384 assert_equal '2011-12-15'.to_date, assigns(:to)
478 assert_equal '2011-12-15'.to_date, assigns(:to)
385 end
479 end
386
480
387 def test_index_current_month
481 def test_index_current_month
388 Date.stubs(:today).returns('2011-12-15'.to_date)
482 Date.stubs(:today).returns('2011-12-15'.to_date)
389 get :index, :period => 'current_month'
483 get :index, :period => 'current_month'
390 assert_equal '2011-12-01'.to_date, assigns(:from)
484 assert_equal '2011-12-01'.to_date, assigns(:from)
391 assert_equal '2011-12-31'.to_date, assigns(:to)
485 assert_equal '2011-12-31'.to_date, assigns(:to)
392 end
486 end
393
487
394 def test_index_last_month
488 def test_index_last_month
395 Date.stubs(:today).returns('2011-12-15'.to_date)
489 Date.stubs(:today).returns('2011-12-15'.to_date)
396 get :index, :period => 'last_month'
490 get :index, :period => 'last_month'
397 assert_equal '2011-11-01'.to_date, assigns(:from)
491 assert_equal '2011-11-01'.to_date, assigns(:from)
398 assert_equal '2011-11-30'.to_date, assigns(:to)
492 assert_equal '2011-11-30'.to_date, assigns(:to)
399 end
493 end
400
494
401 def test_index_30_days
495 def test_index_30_days
402 Date.stubs(:today).returns('2011-12-15'.to_date)
496 Date.stubs(:today).returns('2011-12-15'.to_date)
403 get :index, :period => '30_days'
497 get :index, :period => '30_days'
404 assert_equal '2011-11-15'.to_date, assigns(:from)
498 assert_equal '2011-11-15'.to_date, assigns(:from)
405 assert_equal '2011-12-15'.to_date, assigns(:to)
499 assert_equal '2011-12-15'.to_date, assigns(:to)
406 end
500 end
407
501
408 def test_index_current_year
502 def test_index_current_year
409 Date.stubs(:today).returns('2011-12-15'.to_date)
503 Date.stubs(:today).returns('2011-12-15'.to_date)
410 get :index, :period => 'current_year'
504 get :index, :period => 'current_year'
411 assert_equal '2011-01-01'.to_date, assigns(:from)
505 assert_equal '2011-01-01'.to_date, assigns(:from)
412 assert_equal '2011-12-31'.to_date, assigns(:to)
506 assert_equal '2011-12-31'.to_date, assigns(:to)
413 end
507 end
414
508
415 def test_index_at_issue_level
509 def test_index_at_issue_level
416 get :index, :issue_id => 1
510 get :index, :issue_id => 1
417 assert_response :success
511 assert_response :success
418 assert_template 'index'
512 assert_template 'index'
419 assert_not_nil assigns(:entries)
513 assert_not_nil assigns(:entries)
420 assert_equal 2, assigns(:entries).size
514 assert_equal 2, assigns(:entries).size
421 assert_not_nil assigns(:total_hours)
515 assert_not_nil assigns(:total_hours)
422 assert_equal 154.25, assigns(:total_hours)
516 assert_equal 154.25, assigns(:total_hours)
423 # display all time
517 # display all time
424 assert_nil assigns(:from)
518 assert_nil assigns(:from)
425 assert_nil assigns(:to)
519 assert_nil assigns(:to)
426 # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
520 # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
427 # to use /issues/:issue_id/time_entries
521 # to use /issues/:issue_id/time_entries
428 assert_tag :form,
522 assert_tag :form,
429 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
523 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
430 end
524 end
431
525
432 def test_index_atom_feed
526 def test_index_atom_feed
433 get :index, :project_id => 1, :format => 'atom'
527 get :index, :project_id => 1, :format => 'atom'
434 assert_response :success
528 assert_response :success
435 assert_equal 'application/atom+xml', @response.content_type
529 assert_equal 'application/atom+xml', @response.content_type
436 assert_not_nil assigns(:items)
530 assert_not_nil assigns(:items)
437 assert assigns(:items).first.is_a?(TimeEntry)
531 assert assigns(:items).first.is_a?(TimeEntry)
438 end
532 end
439
533
440 def test_index_all_projects_csv_export
534 def test_index_all_projects_csv_export
441 Setting.date_format = '%m/%d/%Y'
535 Setting.date_format = '%m/%d/%Y'
442 get :index, :format => 'csv'
536 get :index, :format => 'csv'
443 assert_response :success
537 assert_response :success
444 assert_equal 'text/csv', @response.content_type
538 assert_equal 'text/csv', @response.content_type
445 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
539 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
446 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
540 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
447 end
541 end
448
542
449 def test_index_csv_export
543 def test_index_csv_export
450 Setting.date_format = '%m/%d/%Y'
544 Setting.date_format = '%m/%d/%Y'
451 get :index, :project_id => 1, :format => 'csv'
545 get :index, :project_id => 1, :format => 'csv'
452 assert_response :success
546 assert_response :success
453 assert_equal 'text/csv', @response.content_type
547 assert_equal 'text/csv', @response.content_type
454 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
548 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
455 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
549 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
456 end
550 end
457
551
458 def test_csv_big_5
552 def test_csv_big_5
459 user = User.find_by_id(3)
553 user = User.find_by_id(3)
460 user.language = "zh-TW"
554 user.language = "zh-TW"
461 assert user.save
555 assert user.save
462 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
556 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
463 str_big5 = "\xa4@\xa4\xeb"
557 str_big5 = "\xa4@\xa4\xeb"
464 if str_utf8.respond_to?(:force_encoding)
558 if str_utf8.respond_to?(:force_encoding)
465 str_utf8.force_encoding('UTF-8')
559 str_utf8.force_encoding('UTF-8')
466 str_big5.force_encoding('Big5')
560 str_big5.force_encoding('Big5')
467 end
561 end
468 @request.session[:user_id] = 3
562 @request.session[:user_id] = 3
469 post :create, :project_id => 1,
563 post :create, :project_id => 1,
470 :time_entry => {:comments => str_utf8,
564 :time_entry => {:comments => str_utf8,
471 # Not the default activity
565 # Not the default activity
472 :activity_id => '11',
566 :activity_id => '11',
473 :issue_id => '',
567 :issue_id => '',
474 :spent_on => '2011-11-10',
568 :spent_on => '2011-11-10',
475 :hours => '7.3'}
569 :hours => '7.3'}
476 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
570 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
477
571
478 t = TimeEntry.find_by_comments(str_utf8)
572 t = TimeEntry.find_by_comments(str_utf8)
479 assert_not_nil t
573 assert_not_nil t
480 assert_equal 11, t.activity_id
574 assert_equal 11, t.activity_id
481 assert_equal 7.3, t.hours
575 assert_equal 7.3, t.hours
482 assert_equal 3, t.user_id
576 assert_equal 3, t.user_id
483
577
484 get :index, :project_id => 1, :format => 'csv',
578 get :index, :project_id => 1, :format => 'csv',
485 :from => '2011-11-10', :to => '2011-11-10'
579 :from => '2011-11-10', :to => '2011-11-10'
486 assert_response :success
580 assert_response :success
487 assert_equal 'text/csv', @response.content_type
581 assert_equal 'text/csv', @response.content_type
488 ar = @response.body.chomp.split("\n")
582 ar = @response.body.chomp.split("\n")
489 s1 = "\xa4\xe9\xb4\xc1"
583 s1 = "\xa4\xe9\xb4\xc1"
490 if str_utf8.respond_to?(:force_encoding)
584 if str_utf8.respond_to?(:force_encoding)
491 s1.force_encoding('Big5')
585 s1.force_encoding('Big5')
492 end
586 end
493 assert ar[0].include?(s1)
587 assert ar[0].include?(s1)
494 assert ar[1].include?(str_big5)
588 assert ar[1].include?(str_big5)
495 end
589 end
496
590
497 def test_csv_cannot_convert_should_be_replaced_big_5
591 def test_csv_cannot_convert_should_be_replaced_big_5
498 user = User.find_by_id(3)
592 user = User.find_by_id(3)
499 user.language = "zh-TW"
593 user.language = "zh-TW"
500 assert user.save
594 assert user.save
501 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
595 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
502 if str_utf8.respond_to?(:force_encoding)
596 if str_utf8.respond_to?(:force_encoding)
503 str_utf8.force_encoding('UTF-8')
597 str_utf8.force_encoding('UTF-8')
504 end
598 end
505 @request.session[:user_id] = 3
599 @request.session[:user_id] = 3
506 post :create, :project_id => 1,
600 post :create, :project_id => 1,
507 :time_entry => {:comments => str_utf8,
601 :time_entry => {:comments => str_utf8,
508 # Not the default activity
602 # Not the default activity
509 :activity_id => '11',
603 :activity_id => '11',
510 :issue_id => '',
604 :issue_id => '',
511 :spent_on => '2011-11-10',
605 :spent_on => '2011-11-10',
512 :hours => '7.3'}
606 :hours => '7.3'}
513 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
607 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
514
608
515 t = TimeEntry.find_by_comments(str_utf8)
609 t = TimeEntry.find_by_comments(str_utf8)
516 assert_not_nil t
610 assert_not_nil t
517 assert_equal 11, t.activity_id
611 assert_equal 11, t.activity_id
518 assert_equal 7.3, t.hours
612 assert_equal 7.3, t.hours
519 assert_equal 3, t.user_id
613 assert_equal 3, t.user_id
520
614
521 get :index, :project_id => 1, :format => 'csv',
615 get :index, :project_id => 1, :format => 'csv',
522 :from => '2011-11-10', :to => '2011-11-10'
616 :from => '2011-11-10', :to => '2011-11-10'
523 assert_response :success
617 assert_response :success
524 assert_equal 'text/csv', @response.content_type
618 assert_equal 'text/csv', @response.content_type
525 ar = @response.body.chomp.split("\n")
619 ar = @response.body.chomp.split("\n")
526 s1 = "\xa4\xe9\xb4\xc1"
620 s1 = "\xa4\xe9\xb4\xc1"
527 if str_utf8.respond_to?(:force_encoding)
621 if str_utf8.respond_to?(:force_encoding)
528 s1.force_encoding('Big5')
622 s1.force_encoding('Big5')
529 end
623 end
530 assert ar[0].include?(s1)
624 assert ar[0].include?(s1)
531 s2 = ar[1].split(",")[8]
625 s2 = ar[1].split(",")[8]
532 if s2.respond_to?(:force_encoding)
626 if s2.respond_to?(:force_encoding)
533 s3 = "\xa5H?"
627 s3 = "\xa5H?"
534 s3.force_encoding('Big5')
628 s3.force_encoding('Big5')
535 assert_equal s3, s2
629 assert_equal s3, s2
536 elsif RUBY_PLATFORM == 'java'
630 elsif RUBY_PLATFORM == 'java'
537 assert_equal "??", s2
631 assert_equal "??", s2
538 else
632 else
539 assert_equal "\xa5H???", s2
633 assert_equal "\xa5H???", s2
540 end
634 end
541 end
635 end
542
636
543 def test_csv_tw
637 def test_csv_tw
544 with_settings :default_language => "zh-TW" do
638 with_settings :default_language => "zh-TW" do
545 str1 = "test_csv_tw"
639 str1 = "test_csv_tw"
546 user = User.find_by_id(3)
640 user = User.find_by_id(3)
547 te1 = TimeEntry.create(:spent_on => '2011-11-10',
641 te1 = TimeEntry.create(:spent_on => '2011-11-10',
548 :hours => 999.9,
642 :hours => 999.9,
549 :project => Project.find(1),
643 :project => Project.find(1),
550 :user => user,
644 :user => user,
551 :activity => TimeEntryActivity.find_by_name('Design'),
645 :activity => TimeEntryActivity.find_by_name('Design'),
552 :comments => str1)
646 :comments => str1)
553 te2 = TimeEntry.find_by_comments(str1)
647 te2 = TimeEntry.find_by_comments(str1)
554 assert_not_nil te2
648 assert_not_nil te2
555 assert_equal 999.9, te2.hours
649 assert_equal 999.9, te2.hours
556 assert_equal 3, te2.user_id
650 assert_equal 3, te2.user_id
557
651
558 get :index, :project_id => 1, :format => 'csv',
652 get :index, :project_id => 1, :format => 'csv',
559 :from => '2011-11-10', :to => '2011-11-10'
653 :from => '2011-11-10', :to => '2011-11-10'
560 assert_response :success
654 assert_response :success
561 assert_equal 'text/csv', @response.content_type
655 assert_equal 'text/csv', @response.content_type
562
656
563 ar = @response.body.chomp.split("\n")
657 ar = @response.body.chomp.split("\n")
564 s2 = ar[1].split(",")[7]
658 s2 = ar[1].split(",")[7]
565 assert_equal '999.9', s2
659 assert_equal '999.9', s2
566
660
567 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
661 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
568 if str_tw.respond_to?(:force_encoding)
662 if str_tw.respond_to?(:force_encoding)
569 str_tw.force_encoding('UTF-8')
663 str_tw.force_encoding('UTF-8')
570 end
664 end
571 assert_equal str_tw, l(:general_lang_name)
665 assert_equal str_tw, l(:general_lang_name)
572 assert_equal ',', l(:general_csv_separator)
666 assert_equal ',', l(:general_csv_separator)
573 assert_equal '.', l(:general_csv_decimal_separator)
667 assert_equal '.', l(:general_csv_decimal_separator)
574 end
668 end
575 end
669 end
576
670
577 def test_csv_fr
671 def test_csv_fr
578 with_settings :default_language => "fr" do
672 with_settings :default_language => "fr" do
579 str1 = "test_csv_fr"
673 str1 = "test_csv_fr"
580 user = User.find_by_id(3)
674 user = User.find_by_id(3)
581 te1 = TimeEntry.create(:spent_on => '2011-11-10',
675 te1 = TimeEntry.create(:spent_on => '2011-11-10',
582 :hours => 999.9,
676 :hours => 999.9,
583 :project => Project.find(1),
677 :project => Project.find(1),
584 :user => user,
678 :user => user,
585 :activity => TimeEntryActivity.find_by_name('Design'),
679 :activity => TimeEntryActivity.find_by_name('Design'),
586 :comments => str1)
680 :comments => str1)
587 te2 = TimeEntry.find_by_comments(str1)
681 te2 = TimeEntry.find_by_comments(str1)
588 assert_not_nil te2
682 assert_not_nil te2
589 assert_equal 999.9, te2.hours
683 assert_equal 999.9, te2.hours
590 assert_equal 3, te2.user_id
684 assert_equal 3, te2.user_id
591
685
592 get :index, :project_id => 1, :format => 'csv',
686 get :index, :project_id => 1, :format => 'csv',
593 :from => '2011-11-10', :to => '2011-11-10'
687 :from => '2011-11-10', :to => '2011-11-10'
594 assert_response :success
688 assert_response :success
595 assert_equal 'text/csv', @response.content_type
689 assert_equal 'text/csv', @response.content_type
596
690
597 ar = @response.body.chomp.split("\n")
691 ar = @response.body.chomp.split("\n")
598 s2 = ar[1].split(";")[7]
692 s2 = ar[1].split(";")[7]
599 assert_equal '999,9', s2
693 assert_equal '999,9', s2
600
694
601 str_fr = "Fran\xc3\xa7ais"
695 str_fr = "Fran\xc3\xa7ais"
602 if str_fr.respond_to?(:force_encoding)
696 if str_fr.respond_to?(:force_encoding)
603 str_fr.force_encoding('UTF-8')
697 str_fr.force_encoding('UTF-8')
604 end
698 end
605 assert_equal str_fr, l(:general_lang_name)
699 assert_equal str_fr, l(:general_lang_name)
606 assert_equal ';', l(:general_csv_separator)
700 assert_equal ';', l(:general_csv_separator)
607 assert_equal ',', l(:general_csv_decimal_separator)
701 assert_equal ',', l(:general_csv_decimal_separator)
608 end
702 end
609 end
703 end
610 end
704 end
General Comments 0
You need to be logged in to leave comments. Login now