##// END OF EJS Templates
Merged r4087 from trunk....
Eric Davis -
r4032:b5bbc933252d
parent child
Show More
@@ -1,324 +1,324
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
21 before_filter :find_optional_project, :only => [:report, :details]
21 before_filter :find_optional_project, :only => [:report, :details]
22 before_filter :load_available_criterias, :only => [:report]
22 before_filter :load_available_criterias, :only => [:report]
23
23
24 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
24 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
25
25
26 helper :sort
26 helper :sort
27 include SortHelper
27 include SortHelper
28 helper :issues
28 helper :issues
29 include TimelogHelper
29 include TimelogHelper
30 helper :custom_fields
30 helper :custom_fields
31 include CustomFieldsHelper
31 include CustomFieldsHelper
32
32
33 def report
33 def report
34 @criterias = params[:criterias] || []
34 @criterias = params[:criterias] || []
35 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
35 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
36 @criterias.uniq!
36 @criterias.uniq!
37 @criterias = @criterias[0,3]
37 @criterias = @criterias[0,3]
38
38
39 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
39 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
40
40
41 retrieve_date_range
41 retrieve_date_range
42
42
43 unless @criterias.empty?
43 unless @criterias.empty?
44 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
44 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
45 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
45 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
46 sql_condition = ''
46 sql_condition = ''
47
47
48 if @project.nil?
48 if @project.nil?
49 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
49 sql_condition = Project.allowed_to_condition(User.current, :view_time_entries)
50 elsif @issue.nil?
50 elsif @issue.nil?
51 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
51 sql_condition = @project.project_condition(Setting.display_subprojects_issues?)
52 else
52 else
53 sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
53 sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
54 end
54 end
55
55
56 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
56 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
57 sql << " FROM #{TimeEntry.table_name}"
57 sql << " FROM #{TimeEntry.table_name}"
58 sql << time_report_joins
58 sql << time_report_joins
59 sql << " WHERE"
59 sql << " WHERE"
60 sql << " (%s) AND" % sql_condition
60 sql << " (%s) AND" % sql_condition
61 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
61 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
62 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
62 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
63
63
64 @hours = ActiveRecord::Base.connection.select_all(sql)
64 @hours = ActiveRecord::Base.connection.select_all(sql)
65
65
66 @hours.each do |row|
66 @hours.each do |row|
67 case @columns
67 case @columns
68 when 'year'
68 when 'year'
69 row['year'] = row['tyear']
69 row['year'] = row['tyear']
70 when 'month'
70 when 'month'
71 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
71 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
72 when 'week'
72 when 'week'
73 row['week'] = "#{row['tyear']}-#{row['tweek']}"
73 row['week'] = "#{row['tyear']}-#{row['tweek']}"
74 when 'day'
74 when 'day'
75 row['day'] = "#{row['spent_on']}"
75 row['day'] = "#{row['spent_on']}"
76 end
76 end
77 end
77 end
78
78
79 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
79 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
80
80
81 @periods = []
81 @periods = []
82 # Date#at_beginning_of_ not supported in Rails 1.2.x
82 # Date#at_beginning_of_ not supported in Rails 1.2.x
83 date_from = @from.to_time
83 date_from = @from.to_time
84 # 100 columns max
84 # 100 columns max
85 while date_from <= @to.to_time && @periods.length < 100
85 while date_from <= @to.to_time && @periods.length < 100
86 case @columns
86 case @columns
87 when 'year'
87 when 'year'
88 @periods << "#{date_from.year}"
88 @periods << "#{date_from.year}"
89 date_from = (date_from + 1.year).at_beginning_of_year
89 date_from = (date_from + 1.year).at_beginning_of_year
90 when 'month'
90 when 'month'
91 @periods << "#{date_from.year}-#{date_from.month}"
91 @periods << "#{date_from.year}-#{date_from.month}"
92 date_from = (date_from + 1.month).at_beginning_of_month
92 date_from = (date_from + 1.month).at_beginning_of_month
93 when 'week'
93 when 'week'
94 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
94 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
95 date_from = (date_from + 7.day).at_beginning_of_week
95 date_from = (date_from + 7.day).at_beginning_of_week
96 when 'day'
96 when 'day'
97 @periods << "#{date_from.to_date}"
97 @periods << "#{date_from.to_date}"
98 date_from = date_from + 1.day
98 date_from = date_from + 1.day
99 end
99 end
100 end
100 end
101 end
101 end
102
102
103 respond_to do |format|
103 respond_to do |format|
104 format.html { render :layout => !request.xhr? }
104 format.html { render :layout => !request.xhr? }
105 format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
105 format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
106 end
106 end
107 end
107 end
108
108
109 def details
109 def details
110 sort_init 'spent_on', 'desc'
110 sort_init 'spent_on', 'desc'
111 sort_update 'spent_on' => 'spent_on',
111 sort_update 'spent_on' => 'spent_on',
112 'user' => 'user_id',
112 'user' => 'user_id',
113 'activity' => 'activity_id',
113 'activity' => 'activity_id',
114 'project' => "#{Project.table_name}.name",
114 'project' => "#{Project.table_name}.name",
115 'issue' => 'issue_id',
115 'issue' => 'issue_id',
116 'hours' => 'hours'
116 'hours' => 'hours'
117
117
118 cond = ARCondition.new
118 cond = ARCondition.new
119 if @project.nil?
119 if @project.nil?
120 cond << Project.allowed_to_condition(User.current, :view_time_entries)
120 cond << Project.allowed_to_condition(User.current, :view_time_entries)
121 elsif @issue.nil?
121 elsif @issue.nil?
122 cond << @project.project_condition(Setting.display_subprojects_issues?)
122 cond << @project.project_condition(Setting.display_subprojects_issues?)
123 else
123 else
124 cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
124 cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
125 end
125 end
126
126
127 retrieve_date_range
127 retrieve_date_range
128 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
128 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
129
129
130 TimeEntry.visible_by(User.current) do
130 TimeEntry.visible_by(User.current) do
131 respond_to do |format|
131 respond_to do |format|
132 format.html {
132 format.html {
133 # Paginate results
133 # Paginate results
134 @entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
134 @entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
135 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
135 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
136 @entries = TimeEntry.find(:all,
136 @entries = TimeEntry.find(:all,
137 :include => [:project, :activity, :user, {:issue => :tracker}],
137 :include => [:project, :activity, :user, {:issue => :tracker}],
138 :conditions => cond.conditions,
138 :conditions => cond.conditions,
139 :order => sort_clause,
139 :order => sort_clause,
140 :limit => @entry_pages.items_per_page,
140 :limit => @entry_pages.items_per_page,
141 :offset => @entry_pages.current.offset)
141 :offset => @entry_pages.current.offset)
142 @total_hours = TimeEntry.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
142 @total_hours = TimeEntry.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
143
143
144 render :layout => !request.xhr?
144 render :layout => !request.xhr?
145 }
145 }
146 format.atom {
146 format.atom {
147 entries = TimeEntry.find(:all,
147 entries = TimeEntry.find(:all,
148 :include => [:project, :activity, :user, {:issue => :tracker}],
148 :include => [:project, :activity, :user, {:issue => :tracker}],
149 :conditions => cond.conditions,
149 :conditions => cond.conditions,
150 :order => "#{TimeEntry.table_name}.created_on DESC",
150 :order => "#{TimeEntry.table_name}.created_on DESC",
151 :limit => Setting.feeds_limit.to_i)
151 :limit => Setting.feeds_limit.to_i)
152 render_feed(entries, :title => l(:label_spent_time))
152 render_feed(entries, :title => l(:label_spent_time))
153 }
153 }
154 format.csv {
154 format.csv {
155 # Export all entries
155 # Export all entries
156 @entries = TimeEntry.find(:all,
156 @entries = TimeEntry.find(:all,
157 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
157 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
158 :conditions => cond.conditions,
158 :conditions => cond.conditions,
159 :order => sort_clause)
159 :order => sort_clause)
160 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
160 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
161 }
161 }
162 end
162 end
163 end
163 end
164 end
164 end
165
165
166 def edit
166 def edit
167 (render_403; return) if @time_entry && !@time_entry.editable_by?(User.current)
167 (render_403; return) if @time_entry && !@time_entry.editable_by?(User.current)
168 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
168 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
169 @time_entry.attributes = params[:time_entry]
169 @time_entry.attributes = params[:time_entry]
170
170
171 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
171 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
172
172
173 if request.post? and @time_entry.save
173 if request.post? and @time_entry.save
174 flash[:notice] = l(:notice_successful_update)
174 flash[:notice] = l(:notice_successful_update)
175 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
175 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
176 return
176 return
177 end
177 end
178 end
178 end
179
179
180 def destroy
180 def destroy
181 (render_404; return) unless @time_entry
181 (render_404; return) unless @time_entry
182 (render_403; return) unless @time_entry.editable_by?(User.current)
182 (render_403; return) unless @time_entry.editable_by?(User.current)
183 if @time_entry.destroy && @time_entry.destroyed?
183 if @time_entry.destroy && @time_entry.destroyed?
184 flash[:notice] = l(:notice_successful_delete)
184 flash[:notice] = l(:notice_successful_delete)
185 else
185 else
186 flash[:error] = l(:notice_unable_delete_time_entry)
186 flash[:error] = l(:notice_unable_delete_time_entry)
187 end
187 end
188 redirect_to :back
188 redirect_to :back
189 rescue ::ActionController::RedirectBackError
189 rescue ::ActionController::RedirectBackError
190 redirect_to :action => 'details', :project_id => @time_entry.project
190 redirect_to :action => 'details', :project_id => @time_entry.project
191 end
191 end
192
192
193 private
193 private
194 def find_project
194 def find_project
195 if params[:id]
195 if params[:id]
196 @time_entry = TimeEntry.find(params[:id])
196 @time_entry = TimeEntry.find(params[:id])
197 @project = @time_entry.project
197 @project = @time_entry.project
198 elsif params[:issue_id]
198 elsif params[:issue_id]
199 @issue = Issue.find(params[:issue_id])
199 @issue = Issue.find(params[:issue_id])
200 @project = @issue.project
200 @project = @issue.project
201 elsif params[:project_id]
201 elsif params[:project_id]
202 @project = Project.find(params[:project_id])
202 @project = Project.find(params[:project_id])
203 else
203 else
204 render_404
204 render_404
205 return false
205 return false
206 end
206 end
207 rescue ActiveRecord::RecordNotFound
207 rescue ActiveRecord::RecordNotFound
208 render_404
208 render_404
209 end
209 end
210
210
211 def find_optional_project
211 def find_optional_project
212 if !params[:issue_id].blank?
212 if !params[:issue_id].blank?
213 @issue = Issue.find(params[:issue_id])
213 @issue = Issue.find(params[:issue_id])
214 @project = @issue.project
214 @project = @issue.project
215 elsif !params[:project_id].blank?
215 elsif !params[:project_id].blank?
216 @project = Project.find(params[:project_id])
216 @project = Project.find(params[:project_id])
217 end
217 end
218 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
218 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
219 end
219 end
220
220
221 # Retrieves the date range based on predefined ranges or specific from/to param dates
221 # Retrieves the date range based on predefined ranges or specific from/to param dates
222 def retrieve_date_range
222 def retrieve_date_range
223 @free_period = false
223 @free_period = false
224 @from, @to = nil, nil
224 @from, @to = nil, nil
225
225
226 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
226 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
227 case params[:period].to_s
227 case params[:period].to_s
228 when 'today'
228 when 'today'
229 @from = @to = Date.today
229 @from = @to = Date.today
230 when 'yesterday'
230 when 'yesterday'
231 @from = @to = Date.today - 1
231 @from = @to = Date.today - 1
232 when 'current_week'
232 when 'current_week'
233 @from = Date.today - (Date.today.cwday - 1)%7
233 @from = Date.today - (Date.today.cwday - 1)%7
234 @to = @from + 6
234 @to = @from + 6
235 when 'last_week'
235 when 'last_week'
236 @from = Date.today - 7 - (Date.today.cwday - 1)%7
236 @from = Date.today - 7 - (Date.today.cwday - 1)%7
237 @to = @from + 6
237 @to = @from + 6
238 when '7_days'
238 when '7_days'
239 @from = Date.today - 7
239 @from = Date.today - 7
240 @to = Date.today
240 @to = Date.today
241 when 'current_month'
241 when 'current_month'
242 @from = Date.civil(Date.today.year, Date.today.month, 1)
242 @from = Date.civil(Date.today.year, Date.today.month, 1)
243 @to = (@from >> 1) - 1
243 @to = (@from >> 1) - 1
244 when 'last_month'
244 when 'last_month'
245 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
245 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
246 @to = (@from >> 1) - 1
246 @to = (@from >> 1) - 1
247 when '30_days'
247 when '30_days'
248 @from = Date.today - 30
248 @from = Date.today - 30
249 @to = Date.today
249 @to = Date.today
250 when 'current_year'
250 when 'current_year'
251 @from = Date.civil(Date.today.year, 1, 1)
251 @from = Date.civil(Date.today.year, 1, 1)
252 @to = Date.civil(Date.today.year, 12, 31)
252 @to = Date.civil(Date.today.year, 12, 31)
253 end
253 end
254 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
254 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
255 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
255 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
256 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
256 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
257 @free_period = true
257 @free_period = true
258 else
258 else
259 # default
259 # default
260 end
260 end
261
261
262 @from, @to = @to, @from if @from && @to && @from > @to
262 @from, @to = @to, @from if @from && @to && @from > @to
263 @from ||= (TimeEntry.earilest_date_for_project || Date.today) - 1
263 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
264 @to ||= (TimeEntry.latest_date_for_project || Date.today)
264 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
265 end
265 end
266
266
267 def load_available_criterias
267 def load_available_criterias
268 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
268 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
269 :klass => Project,
269 :klass => Project,
270 :label => :label_project},
270 :label => :label_project},
271 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
271 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
272 :klass => Version,
272 :klass => Version,
273 :label => :label_version},
273 :label => :label_version},
274 'category' => {:sql => "#{Issue.table_name}.category_id",
274 'category' => {:sql => "#{Issue.table_name}.category_id",
275 :klass => IssueCategory,
275 :klass => IssueCategory,
276 :label => :field_category},
276 :label => :field_category},
277 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
277 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
278 :klass => User,
278 :klass => User,
279 :label => :label_member},
279 :label => :label_member},
280 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
280 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
281 :klass => Tracker,
281 :klass => Tracker,
282 :label => :label_tracker},
282 :label => :label_tracker},
283 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
283 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
284 :klass => TimeEntryActivity,
284 :klass => TimeEntryActivity,
285 :label => :label_activity},
285 :label => :label_activity},
286 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
286 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
287 :klass => Issue,
287 :klass => Issue,
288 :label => :label_issue}
288 :label => :label_issue}
289 }
289 }
290
290
291 # Add list and boolean custom fields as available criterias
291 # Add list and boolean custom fields as available criterias
292 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
292 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
293 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
293 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
294 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
294 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
295 :format => cf.field_format,
295 :format => cf.field_format,
296 :label => cf.name}
296 :label => cf.name}
297 end if @project
297 end if @project
298
298
299 # Add list and boolean time entry custom fields
299 # Add list and boolean time entry custom fields
300 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
300 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
301 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
301 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
302 :format => cf.field_format,
302 :format => cf.field_format,
303 :label => cf.name}
303 :label => cf.name}
304 end
304 end
305
305
306 # Add list and boolean time entry activity custom fields
306 # Add list and boolean time entry activity custom fields
307 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
307 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
308 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
308 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)",
309 :format => cf.field_format,
309 :format => cf.field_format,
310 :label => cf.name}
310 :label => cf.name}
311 end
311 end
312
312
313 call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
313 call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project })
314 @available_criterias
314 @available_criterias
315 end
315 end
316
316
317 def time_report_joins
317 def time_report_joins
318 sql = ''
318 sql = ''
319 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
319 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
320 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
320 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
321 call_hook(:controller_timelog_time_report_joins, {:sql => sql} )
321 call_hook(:controller_timelog_time_report_joins, {:sql => sql} )
322 sql
322 sql
323 end
323 end
324 end
324 end
@@ -1,721 +1,730
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities
24 has_many :time_entry_activities
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 has_many :memberships, :class_name => 'Member'
26 has_many :memberships, :class_name => 'Member'
27 has_many :member_principals, :class_name => 'Member',
27 has_many :member_principals, :class_name => 'Member',
28 :include => :principal,
28 :include => :principal,
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 has_many :users, :through => :members
30 has_many :users, :through => :members
31 has_many :principals, :through => :member_principals, :source => :principal
31 has_many :principals, :through => :member_principals, :source => :principal
32
32
33 has_many :enabled_modules, :dependent => :delete_all
33 has_many :enabled_modules, :dependent => :delete_all
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 has_many :issue_changes, :through => :issues, :source => :journals
36 has_many :issue_changes, :through => :issues, :source => :journals
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 has_many :time_entries, :dependent => :delete_all
38 has_many :time_entries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
40 has_many :documents, :dependent => :destroy
40 has_many :documents, :dependent => :destroy
41 has_many :news, :dependent => :delete_all, :include => :author
41 has_many :news, :dependent => :delete_all, :include => :author
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 has_one :repository, :dependent => :destroy
44 has_one :repository, :dependent => :destroy
45 has_many :changesets, :through => :repository
45 has_many :changesets, :through => :repository
46 has_one :wiki, :dependent => :destroy
46 has_one :wiki, :dependent => :destroy
47 # Custom field for the project issues
47 # Custom field for the project issues
48 has_and_belongs_to_many :issue_custom_fields,
48 has_and_belongs_to_many :issue_custom_fields,
49 :class_name => 'IssueCustomField',
49 :class_name => 'IssueCustomField',
50 :order => "#{CustomField.table_name}.position",
50 :order => "#{CustomField.table_name}.position",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 :association_foreign_key => 'custom_field_id'
52 :association_foreign_key => 'custom_field_id'
53
53
54 acts_as_nested_set :order => 'name'
54 acts_as_nested_set :order => 'name'
55 acts_as_attachable :view_permission => :view_files,
55 acts_as_attachable :view_permission => :view_files,
56 :delete_permission => :manage_files
56 :delete_permission => :manage_files
57
57
58 acts_as_customizable
58 acts_as_customizable
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
62 :author => nil
62 :author => nil
63
63
64 attr_protected :status, :enabled_module_names
64 attr_protected :status, :enabled_module_names
65
65
66 validates_presence_of :name, :identifier
66 validates_presence_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
68 validates_associated :repository, :wiki
68 validates_associated :repository, :wiki
69 validates_length_of :name, :maximum => 30
69 validates_length_of :name, :maximum => 30
70 validates_length_of :homepage, :maximum => 255
70 validates_length_of :homepage, :maximum => 255
71 validates_length_of :identifier, :in => 1..20
71 validates_length_of :identifier, :in => 1..20
72 # donwcase letters, digits, dashes but not digits only
72 # donwcase letters, digits, dashes but not digits only
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 # reserved words
74 # reserved words
75 validates_exclusion_of :identifier, :in => %w( new )
75 validates_exclusion_of :identifier, :in => %w( new )
76
76
77 before_destroy :delete_all_members, :destroy_children
77 before_destroy :delete_all_members, :destroy_children
78
78
79 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] } }
79 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] } }
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 named_scope :all_public, { :conditions => { :is_public => true } }
81 named_scope :all_public, { :conditions => { :is_public => true } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83
83
84 def identifier=(identifier)
84 def identifier=(identifier)
85 super unless identifier_frozen?
85 super unless identifier_frozen?
86 end
86 end
87
87
88 def identifier_frozen?
88 def identifier_frozen?
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 end
90 end
91
91
92 # returns latest created projects
92 # returns latest created projects
93 # non public projects will be returned only if user is a member of those
93 # non public projects will be returned only if user is a member of those
94 def self.latest(user=nil, count=5)
94 def self.latest(user=nil, count=5)
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 end
96 end
97
97
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 #
99 #
100 # Examples:
100 # Examples:
101 # Projects.visible_by(admin) => "projects.status = 1"
101 # Projects.visible_by(admin) => "projects.status = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 def self.visible_by(user=nil)
103 def self.visible_by(user=nil)
104 user ||= User.current
104 user ||= User.current
105 if user && user.admin?
105 if user && user.admin?
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 elsif user && user.memberships.any?
107 elsif user && user.memberships.any?
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
109 else
109 else
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 end
111 end
112 end
112 end
113
113
114 def self.allowed_to_condition(user, permission, options={})
114 def self.allowed_to_condition(user, permission, options={})
115 statements = []
115 statements = []
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 if perm = Redmine::AccessControl.permission(permission)
117 if perm = Redmine::AccessControl.permission(permission)
118 unless perm.project_module.nil?
118 unless perm.project_module.nil?
119 # If the permission belongs to a project module, make sure the module is enabled
119 # If the permission belongs to a project module, make sure the module is enabled
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 end
121 end
122 end
122 end
123 if options[:project]
123 if options[:project]
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 end
127 end
128 if user.admin?
128 if user.admin?
129 # no restriction
129 # no restriction
130 else
130 else
131 statements << "1=0"
131 statements << "1=0"
132 if user.logged?
132 if user.logged?
133 if Role.non_member.allowed_to?(permission) && !options[:member]
133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 end
135 end
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 else
138 else
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 # anonymous user allowed on public project
140 # anonymous user allowed on public project
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 end
142 end
143 end
143 end
144 end
144 end
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 end
146 end
147
147
148 # Returns the Systemwide and project specific activities
148 # Returns the Systemwide and project specific activities
149 def activities(include_inactive=false)
149 def activities(include_inactive=false)
150 if include_inactive
150 if include_inactive
151 return all_activities
151 return all_activities
152 else
152 else
153 return active_activities
153 return active_activities
154 end
154 end
155 end
155 end
156
156
157 # Will create a new Project specific Activity or update an existing one
157 # Will create a new Project specific Activity or update an existing one
158 #
158 #
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 # does not successfully save.
160 # does not successfully save.
161 def update_or_create_time_entry_activity(id, activity_hash)
161 def update_or_create_time_entry_activity(id, activity_hash)
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 self.create_time_entry_activity_if_needed(activity_hash)
163 self.create_time_entry_activity_if_needed(activity_hash)
164 else
164 else
165 activity = project.time_entry_activities.find_by_id(id.to_i)
165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 activity.update_attributes(activity_hash) if activity
166 activity.update_attributes(activity_hash) if activity
167 end
167 end
168 end
168 end
169
169
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 #
171 #
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 # does not successfully save.
173 # does not successfully save.
174 def create_time_entry_activity_if_needed(activity)
174 def create_time_entry_activity_if_needed(activity)
175 if activity['parent_id']
175 if activity['parent_id']
176
176
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 activity['name'] = parent_activity.name
178 activity['name'] = parent_activity.name
179 activity['position'] = parent_activity.position
179 activity['position'] = parent_activity.position
180
180
181 if Enumeration.overridding_change?(activity, parent_activity)
181 if Enumeration.overridding_change?(activity, parent_activity)
182 project_activity = self.time_entry_activities.create(activity)
182 project_activity = self.time_entry_activities.create(activity)
183
183
184 if project_activity.new_record?
184 if project_activity.new_record?
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 else
186 else
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 end
188 end
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 #
194 #
195 # Examples:
195 # Examples:
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 # project.project_condition(false) => "projects.id = 1"
197 # project.project_condition(false) => "projects.id = 1"
198 def project_condition(with_subprojects)
198 def project_condition(with_subprojects)
199 cond = "#{Project.table_name}.id = #{id}"
199 cond = "#{Project.table_name}.id = #{id}"
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 cond
201 cond
202 end
202 end
203
203
204 def self.find(*args)
204 def self.find(*args)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 project = find_by_identifier(*args)
206 project = find_by_identifier(*args)
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 project
208 project
209 else
209 else
210 super
210 super
211 end
211 end
212 end
212 end
213
213
214 def to_param
214 def to_param
215 # id is used for projects with a numeric identifier (compatibility)
215 # id is used for projects with a numeric identifier (compatibility)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 end
217 end
218
218
219 def active?
219 def active?
220 self.status == STATUS_ACTIVE
220 self.status == STATUS_ACTIVE
221 end
221 end
222
222
223 # Archives the project and its descendants
223 # Archives the project and its descendants
224 def archive
224 def archive
225 # Check that there is no issue of a non descendant project that is assigned
225 # Check that there is no issue of a non descendant project that is assigned
226 # to one of the project or descendant versions
226 # to one of the project or descendant versions
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 if v_ids.any? && Issue.find(:first, :include => :project,
228 if v_ids.any? && Issue.find(:first, :include => :project,
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 return false
231 return false
232 end
232 end
233 Project.transaction do
233 Project.transaction do
234 archive!
234 archive!
235 end
235 end
236 true
236 true
237 end
237 end
238
238
239 # Unarchives the project
239 # Unarchives the project
240 # All its ancestors must be active
240 # All its ancestors must be active
241 def unarchive
241 def unarchive
242 return false if ancestors.detect {|a| !a.active?}
242 return false if ancestors.detect {|a| !a.active?}
243 update_attribute :status, STATUS_ACTIVE
243 update_attribute :status, STATUS_ACTIVE
244 end
244 end
245
245
246 # Returns an array of projects the project can be moved to
246 # Returns an array of projects the project can be moved to
247 # by the current user
247 # by the current user
248 def allowed_parents
248 def allowed_parents
249 return @allowed_parents if @allowed_parents
249 return @allowed_parents if @allowed_parents
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 @allowed_parents = @allowed_parents - self_and_descendants
251 @allowed_parents = @allowed_parents - self_and_descendants
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
252 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
253 @allowed_parents << nil
253 @allowed_parents << nil
254 end
254 end
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 @allowed_parents << parent
256 @allowed_parents << parent
257 end
257 end
258 @allowed_parents
258 @allowed_parents
259 end
259 end
260
260
261 # Sets the parent of the project with authorization check
261 # Sets the parent of the project with authorization check
262 def set_allowed_parent!(p)
262 def set_allowed_parent!(p)
263 unless p.nil? || p.is_a?(Project)
263 unless p.nil? || p.is_a?(Project)
264 if p.to_s.blank?
264 if p.to_s.blank?
265 p = nil
265 p = nil
266 else
266 else
267 p = Project.find_by_id(p)
267 p = Project.find_by_id(p)
268 return false unless p
268 return false unless p
269 end
269 end
270 end
270 end
271 if p.nil?
271 if p.nil?
272 if !new_record? && allowed_parents.empty?
272 if !new_record? && allowed_parents.empty?
273 return false
273 return false
274 end
274 end
275 elsif !allowed_parents.include?(p)
275 elsif !allowed_parents.include?(p)
276 return false
276 return false
277 end
277 end
278 set_parent!(p)
278 set_parent!(p)
279 end
279 end
280
280
281 # Sets the parent of the project
281 # Sets the parent of the project
282 # Argument can be either a Project, a String, a Fixnum or nil
282 # Argument can be either a Project, a String, a Fixnum or nil
283 def set_parent!(p)
283 def set_parent!(p)
284 unless p.nil? || p.is_a?(Project)
284 unless p.nil? || p.is_a?(Project)
285 if p.to_s.blank?
285 if p.to_s.blank?
286 p = nil
286 p = nil
287 else
287 else
288 p = Project.find_by_id(p)
288 p = Project.find_by_id(p)
289 return false unless p
289 return false unless p
290 end
290 end
291 end
291 end
292 if p == parent && !p.nil?
292 if p == parent && !p.nil?
293 # Nothing to do
293 # Nothing to do
294 true
294 true
295 elsif p.nil? || (p.active? && move_possible?(p))
295 elsif p.nil? || (p.active? && move_possible?(p))
296 # Insert the project so that target's children or root projects stay alphabetically sorted
296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 sibs = (p.nil? ? self.class.roots : p.children)
297 sibs = (p.nil? ? self.class.roots : p.children)
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 if to_be_inserted_before
299 if to_be_inserted_before
300 move_to_left_of(to_be_inserted_before)
300 move_to_left_of(to_be_inserted_before)
301 elsif p.nil?
301 elsif p.nil?
302 if sibs.empty?
302 if sibs.empty?
303 # move_to_root adds the project in first (ie. left) position
303 # move_to_root adds the project in first (ie. left) position
304 move_to_root
304 move_to_root
305 else
305 else
306 move_to_right_of(sibs.last) unless self == sibs.last
306 move_to_right_of(sibs.last) unless self == sibs.last
307 end
307 end
308 else
308 else
309 # move_to_child_of adds the project in last (ie.right) position
309 # move_to_child_of adds the project in last (ie.right) position
310 move_to_child_of(p)
310 move_to_child_of(p)
311 end
311 end
312 Issue.update_versions_from_hierarchy_change(self)
312 Issue.update_versions_from_hierarchy_change(self)
313 true
313 true
314 else
314 else
315 # Can not move to the given target
315 # Can not move to the given target
316 false
316 false
317 end
317 end
318 end
318 end
319
319
320 # Returns an array of the trackers used by the project and its active sub projects
320 # Returns an array of the trackers used by the project and its active sub projects
321 def rolled_up_trackers
321 def rolled_up_trackers
322 @rolled_up_trackers ||=
322 @rolled_up_trackers ||=
323 Tracker.find(:all, :include => :projects,
323 Tracker.find(:all, :include => :projects,
324 :select => "DISTINCT #{Tracker.table_name}.*",
324 :select => "DISTINCT #{Tracker.table_name}.*",
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 :order => "#{Tracker.table_name}.position")
326 :order => "#{Tracker.table_name}.position")
327 end
327 end
328
328
329 # Closes open and locked project versions that are completed
329 # Closes open and locked project versions that are completed
330 def close_completed_versions
330 def close_completed_versions
331 Version.transaction do
331 Version.transaction do
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 if version.completed?
333 if version.completed?
334 version.update_attribute(:status, 'closed')
334 version.update_attribute(:status, 'closed')
335 end
335 end
336 end
336 end
337 end
337 end
338 end
338 end
339
339
340 # Returns a scope of the Versions on subprojects
340 # Returns a scope of the Versions on subprojects
341 def rolled_up_versions
341 def rolled_up_versions
342 @rolled_up_versions ||=
342 @rolled_up_versions ||=
343 Version.scoped(:include => :project,
343 Version.scoped(:include => :project,
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
344 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
345 end
345 end
346
346
347 # Returns a scope of the Versions used by the project
347 # Returns a scope of the Versions used by the project
348 def shared_versions
348 def shared_versions
349 @shared_versions ||=
349 @shared_versions ||=
350 Version.scoped(:include => :project,
350 Version.scoped(:include => :project,
351 :conditions => "#{Project.table_name}.id = #{id}" +
351 :conditions => "#{Project.table_name}.id = #{id}" +
352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
352 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
353 " #{Version.table_name}.sharing = 'system'" +
353 " #{Version.table_name}.sharing = 'system'" +
354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
354 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
355 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
356 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
357 "))")
357 "))")
358 end
358 end
359
359
360 # Returns a hash of project users grouped by role
360 # Returns a hash of project users grouped by role
361 def users_by_role
361 def users_by_role
362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
362 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
363 m.roles.each do |r|
363 m.roles.each do |r|
364 h[r] ||= []
364 h[r] ||= []
365 h[r] << m.user
365 h[r] << m.user
366 end
366 end
367 h
367 h
368 end
368 end
369 end
369 end
370
370
371 # Deletes all project's members
371 # Deletes all project's members
372 def delete_all_members
372 def delete_all_members
373 me, mr = Member.table_name, MemberRole.table_name
373 me, mr = Member.table_name, MemberRole.table_name
374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
374 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
375 Member.delete_all(['project_id = ?', id])
375 Member.delete_all(['project_id = ?', id])
376 end
376 end
377
377
378 # Users issues can be assigned to
378 # Users issues can be assigned to
379 def assignable_users
379 def assignable_users
380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
380 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
381 end
381 end
382
382
383 # Returns the mail adresses of users that should be always notified on project events
383 # Returns the mail adresses of users that should be always notified on project events
384 def recipients
384 def recipients
385 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
385 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
386 end
386 end
387
387
388 # Returns the users that should be notified on project events
388 # Returns the users that should be notified on project events
389 def notified_users
389 def notified_users
390 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
390 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
391 end
391 end
392
392
393 # Returns an array of all custom fields enabled for project issues
393 # Returns an array of all custom fields enabled for project issues
394 # (explictly associated custom fields and custom fields enabled for all projects)
394 # (explictly associated custom fields and custom fields enabled for all projects)
395 def all_issue_custom_fields
395 def all_issue_custom_fields
396 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
396 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
397 end
397 end
398
398
399 def project
399 def project
400 self
400 self
401 end
401 end
402
402
403 def <=>(project)
403 def <=>(project)
404 name.downcase <=> project.name.downcase
404 name.downcase <=> project.name.downcase
405 end
405 end
406
406
407 def to_s
407 def to_s
408 name
408 name
409 end
409 end
410
410
411 # Returns a short description of the projects (first lines)
411 # Returns a short description of the projects (first lines)
412 def short_description(length = 255)
412 def short_description(length = 255)
413 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
413 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
414 end
414 end
415
415
416 def css_classes
416 def css_classes
417 s = 'project'
417 s = 'project'
418 s << ' root' if root?
418 s << ' root' if root?
419 s << ' child' if child?
419 s << ' child' if child?
420 s << (leaf? ? ' leaf' : ' parent')
420 s << (leaf? ? ' leaf' : ' parent')
421 s
421 s
422 end
422 end
423
423
424 # Return true if this project is allowed to do the specified action.
424 # Return true if this project is allowed to do the specified action.
425 # action can be:
425 # action can be:
426 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
426 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
427 # * a permission Symbol (eg. :edit_project)
427 # * a permission Symbol (eg. :edit_project)
428 def allows_to?(action)
428 def allows_to?(action)
429 if action.is_a? Hash
429 if action.is_a? Hash
430 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
430 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
431 else
431 else
432 allowed_permissions.include? action
432 allowed_permissions.include? action
433 end
433 end
434 end
434 end
435
435
436 def module_enabled?(module_name)
436 def module_enabled?(module_name)
437 module_name = module_name.to_s
437 module_name = module_name.to_s
438 enabled_modules.detect {|m| m.name == module_name}
438 enabled_modules.detect {|m| m.name == module_name}
439 end
439 end
440
440
441 def enabled_module_names=(module_names)
441 def enabled_module_names=(module_names)
442 if module_names && module_names.is_a?(Array)
442 if module_names && module_names.is_a?(Array)
443 module_names = module_names.collect(&:to_s)
443 module_names = module_names.collect(&:to_s)
444 # remove disabled modules
444 # remove disabled modules
445 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
445 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
446 # add new modules
446 # add new modules
447 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
447 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
448 else
448 else
449 enabled_modules.clear
449 enabled_modules.clear
450 end
450 end
451 end
451 end
452
453 # Returns an array of projects that are in this project's hierarchy
454 #
455 # Example: parents, children, siblings
456 def hierarchy
457 parents = project.self_and_ancestors || []
458 descendants = project.descendants || []
459 project_hierarchy = parents | descendants # Set union
460 end
452
461
453 # Returns an auto-generated project identifier based on the last identifier used
462 # Returns an auto-generated project identifier based on the last identifier used
454 def self.next_identifier
463 def self.next_identifier
455 p = Project.find(:first, :order => 'created_on DESC')
464 p = Project.find(:first, :order => 'created_on DESC')
456 p.nil? ? nil : p.identifier.to_s.succ
465 p.nil? ? nil : p.identifier.to_s.succ
457 end
466 end
458
467
459 # Copies and saves the Project instance based on the +project+.
468 # Copies and saves the Project instance based on the +project+.
460 # Duplicates the source project's:
469 # Duplicates the source project's:
461 # * Wiki
470 # * Wiki
462 # * Versions
471 # * Versions
463 # * Categories
472 # * Categories
464 # * Issues
473 # * Issues
465 # * Members
474 # * Members
466 # * Queries
475 # * Queries
467 #
476 #
468 # Accepts an +options+ argument to specify what to copy
477 # Accepts an +options+ argument to specify what to copy
469 #
478 #
470 # Examples:
479 # Examples:
471 # project.copy(1) # => copies everything
480 # project.copy(1) # => copies everything
472 # project.copy(1, :only => 'members') # => copies members only
481 # project.copy(1, :only => 'members') # => copies members only
473 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
482 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
474 def copy(project, options={})
483 def copy(project, options={})
475 project = project.is_a?(Project) ? project : Project.find(project)
484 project = project.is_a?(Project) ? project : Project.find(project)
476
485
477 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
486 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
478 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
487 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
479
488
480 Project.transaction do
489 Project.transaction do
481 if save
490 if save
482 reload
491 reload
483 to_be_copied.each do |name|
492 to_be_copied.each do |name|
484 send "copy_#{name}", project
493 send "copy_#{name}", project
485 end
494 end
486 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
495 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
487 save
496 save
488 end
497 end
489 end
498 end
490 end
499 end
491
500
492
501
493 # Copies +project+ and returns the new instance. This will not save
502 # Copies +project+ and returns the new instance. This will not save
494 # the copy
503 # the copy
495 def self.copy_from(project)
504 def self.copy_from(project)
496 begin
505 begin
497 project = project.is_a?(Project) ? project : Project.find(project)
506 project = project.is_a?(Project) ? project : Project.find(project)
498 if project
507 if project
499 # clear unique attributes
508 # clear unique attributes
500 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
509 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
501 copy = Project.new(attributes)
510 copy = Project.new(attributes)
502 copy.enabled_modules = project.enabled_modules
511 copy.enabled_modules = project.enabled_modules
503 copy.trackers = project.trackers
512 copy.trackers = project.trackers
504 copy.custom_values = project.custom_values.collect {|v| v.clone}
513 copy.custom_values = project.custom_values.collect {|v| v.clone}
505 copy.issue_custom_fields = project.issue_custom_fields
514 copy.issue_custom_fields = project.issue_custom_fields
506 return copy
515 return copy
507 else
516 else
508 return nil
517 return nil
509 end
518 end
510 rescue ActiveRecord::RecordNotFound
519 rescue ActiveRecord::RecordNotFound
511 return nil
520 return nil
512 end
521 end
513 end
522 end
514
523
515 private
524 private
516
525
517 # Destroys children before destroying self
526 # Destroys children before destroying self
518 def destroy_children
527 def destroy_children
519 children.each do |child|
528 children.each do |child|
520 child.destroy
529 child.destroy
521 end
530 end
522 end
531 end
523
532
524 # Copies wiki from +project+
533 # Copies wiki from +project+
525 def copy_wiki(project)
534 def copy_wiki(project)
526 # Check that the source project has a wiki first
535 # Check that the source project has a wiki first
527 unless project.wiki.nil?
536 unless project.wiki.nil?
528 self.wiki ||= Wiki.new
537 self.wiki ||= Wiki.new
529 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
538 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
530 wiki_pages_map = {}
539 wiki_pages_map = {}
531 project.wiki.pages.each do |page|
540 project.wiki.pages.each do |page|
532 # Skip pages without content
541 # Skip pages without content
533 next if page.content.nil?
542 next if page.content.nil?
534 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
543 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
535 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
544 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
536 new_wiki_page.content = new_wiki_content
545 new_wiki_page.content = new_wiki_content
537 wiki.pages << new_wiki_page
546 wiki.pages << new_wiki_page
538 wiki_pages_map[page.id] = new_wiki_page
547 wiki_pages_map[page.id] = new_wiki_page
539 end
548 end
540 wiki.save
549 wiki.save
541 # Reproduce page hierarchy
550 # Reproduce page hierarchy
542 project.wiki.pages.each do |page|
551 project.wiki.pages.each do |page|
543 if page.parent_id && wiki_pages_map[page.id]
552 if page.parent_id && wiki_pages_map[page.id]
544 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
553 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
545 wiki_pages_map[page.id].save
554 wiki_pages_map[page.id].save
546 end
555 end
547 end
556 end
548 end
557 end
549 end
558 end
550
559
551 # Copies versions from +project+
560 # Copies versions from +project+
552 def copy_versions(project)
561 def copy_versions(project)
553 project.versions.each do |version|
562 project.versions.each do |version|
554 new_version = Version.new
563 new_version = Version.new
555 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
564 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
556 self.versions << new_version
565 self.versions << new_version
557 end
566 end
558 end
567 end
559
568
560 # Copies issue categories from +project+
569 # Copies issue categories from +project+
561 def copy_issue_categories(project)
570 def copy_issue_categories(project)
562 project.issue_categories.each do |issue_category|
571 project.issue_categories.each do |issue_category|
563 new_issue_category = IssueCategory.new
572 new_issue_category = IssueCategory.new
564 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
573 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
565 self.issue_categories << new_issue_category
574 self.issue_categories << new_issue_category
566 end
575 end
567 end
576 end
568
577
569 # Copies issues from +project+
578 # Copies issues from +project+
570 def copy_issues(project)
579 def copy_issues(project)
571 # Stores the source issue id as a key and the copied issues as the
580 # Stores the source issue id as a key and the copied issues as the
572 # value. Used to map the two togeather for issue relations.
581 # value. Used to map the two togeather for issue relations.
573 issues_map = {}
582 issues_map = {}
574
583
575 # Get issues sorted by root_id, lft so that parent issues
584 # Get issues sorted by root_id, lft so that parent issues
576 # get copied before their children
585 # get copied before their children
577 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
586 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
578 new_issue = Issue.new
587 new_issue = Issue.new
579 new_issue.copy_from(issue)
588 new_issue.copy_from(issue)
580 new_issue.project = self
589 new_issue.project = self
581 # Reassign fixed_versions by name, since names are unique per
590 # Reassign fixed_versions by name, since names are unique per
582 # project and the versions for self are not yet saved
591 # project and the versions for self are not yet saved
583 if issue.fixed_version
592 if issue.fixed_version
584 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
593 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
585 end
594 end
586 # Reassign the category by name, since names are unique per
595 # Reassign the category by name, since names are unique per
587 # project and the categories for self are not yet saved
596 # project and the categories for self are not yet saved
588 if issue.category
597 if issue.category
589 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
598 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
590 end
599 end
591 # Parent issue
600 # Parent issue
592 if issue.parent_id
601 if issue.parent_id
593 if copied_parent = issues_map[issue.parent_id]
602 if copied_parent = issues_map[issue.parent_id]
594 new_issue.parent_issue_id = copied_parent.id
603 new_issue.parent_issue_id = copied_parent.id
595 end
604 end
596 end
605 end
597
606
598 self.issues << new_issue
607 self.issues << new_issue
599 issues_map[issue.id] = new_issue
608 issues_map[issue.id] = new_issue
600 end
609 end
601
610
602 # Relations after in case issues related each other
611 # Relations after in case issues related each other
603 project.issues.each do |issue|
612 project.issues.each do |issue|
604 new_issue = issues_map[issue.id]
613 new_issue = issues_map[issue.id]
605
614
606 # Relations
615 # Relations
607 issue.relations_from.each do |source_relation|
616 issue.relations_from.each do |source_relation|
608 new_issue_relation = IssueRelation.new
617 new_issue_relation = IssueRelation.new
609 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
618 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
610 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
619 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
611 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
620 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
612 new_issue_relation.issue_to = source_relation.issue_to
621 new_issue_relation.issue_to = source_relation.issue_to
613 end
622 end
614 new_issue.relations_from << new_issue_relation
623 new_issue.relations_from << new_issue_relation
615 end
624 end
616
625
617 issue.relations_to.each do |source_relation|
626 issue.relations_to.each do |source_relation|
618 new_issue_relation = IssueRelation.new
627 new_issue_relation = IssueRelation.new
619 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
628 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
620 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
629 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
621 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
630 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
622 new_issue_relation.issue_from = source_relation.issue_from
631 new_issue_relation.issue_from = source_relation.issue_from
623 end
632 end
624 new_issue.relations_to << new_issue_relation
633 new_issue.relations_to << new_issue_relation
625 end
634 end
626 end
635 end
627 end
636 end
628
637
629 # Copies members from +project+
638 # Copies members from +project+
630 def copy_members(project)
639 def copy_members(project)
631 project.memberships.each do |member|
640 project.memberships.each do |member|
632 new_member = Member.new
641 new_member = Member.new
633 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
642 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
634 # only copy non inherited roles
643 # only copy non inherited roles
635 # inherited roles will be added when copying the group membership
644 # inherited roles will be added when copying the group membership
636 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
645 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
637 next if role_ids.empty?
646 next if role_ids.empty?
638 new_member.role_ids = role_ids
647 new_member.role_ids = role_ids
639 new_member.project = self
648 new_member.project = self
640 self.members << new_member
649 self.members << new_member
641 end
650 end
642 end
651 end
643
652
644 # Copies queries from +project+
653 # Copies queries from +project+
645 def copy_queries(project)
654 def copy_queries(project)
646 project.queries.each do |query|
655 project.queries.each do |query|
647 new_query = Query.new
656 new_query = Query.new
648 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
657 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
649 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
658 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
650 new_query.project = self
659 new_query.project = self
651 self.queries << new_query
660 self.queries << new_query
652 end
661 end
653 end
662 end
654
663
655 # Copies boards from +project+
664 # Copies boards from +project+
656 def copy_boards(project)
665 def copy_boards(project)
657 project.boards.each do |board|
666 project.boards.each do |board|
658 new_board = Board.new
667 new_board = Board.new
659 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
668 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
660 new_board.project = self
669 new_board.project = self
661 self.boards << new_board
670 self.boards << new_board
662 end
671 end
663 end
672 end
664
673
665 def allowed_permissions
674 def allowed_permissions
666 @allowed_permissions ||= begin
675 @allowed_permissions ||= begin
667 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
676 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
668 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
677 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
669 end
678 end
670 end
679 end
671
680
672 def allowed_actions
681 def allowed_actions
673 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
682 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
674 end
683 end
675
684
676 # Returns all the active Systemwide and project specific activities
685 # Returns all the active Systemwide and project specific activities
677 def active_activities
686 def active_activities
678 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
687 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
679
688
680 if overridden_activity_ids.empty?
689 if overridden_activity_ids.empty?
681 return TimeEntryActivity.shared.active
690 return TimeEntryActivity.shared.active
682 else
691 else
683 return system_activities_and_project_overrides
692 return system_activities_and_project_overrides
684 end
693 end
685 end
694 end
686
695
687 # Returns all the Systemwide and project specific activities
696 # Returns all the Systemwide and project specific activities
688 # (inactive and active)
697 # (inactive and active)
689 def all_activities
698 def all_activities
690 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
699 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
691
700
692 if overridden_activity_ids.empty?
701 if overridden_activity_ids.empty?
693 return TimeEntryActivity.shared
702 return TimeEntryActivity.shared
694 else
703 else
695 return system_activities_and_project_overrides(true)
704 return system_activities_and_project_overrides(true)
696 end
705 end
697 end
706 end
698
707
699 # Returns the systemwide active activities merged with the project specific overrides
708 # Returns the systemwide active activities merged with the project specific overrides
700 def system_activities_and_project_overrides(include_inactive=false)
709 def system_activities_and_project_overrides(include_inactive=false)
701 if include_inactive
710 if include_inactive
702 return TimeEntryActivity.shared.
711 return TimeEntryActivity.shared.
703 find(:all,
712 find(:all,
704 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
713 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
705 self.time_entry_activities
714 self.time_entry_activities
706 else
715 else
707 return TimeEntryActivity.shared.active.
716 return TimeEntryActivity.shared.active.
708 find(:all,
717 find(:all,
709 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
718 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
710 self.time_entry_activities.active
719 self.time_entry_activities.active
711 end
720 end
712 end
721 end
713
722
714 # Archives subprojects recursively
723 # Archives subprojects recursively
715 def archive!
724 def archive!
716 children.each do |subproject|
725 children.each do |subproject|
717 subproject.send :archive!
726 subproject.send :archive!
718 end
727 end
719 update_attribute :status, STATUS_ARCHIVED
728 update_attribute :status, STATUS_ARCHIVED
720 end
729 end
721 end
730 end
@@ -1,92 +1,100
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 TimeEntry < ActiveRecord::Base
18 class TimeEntry < ActiveRecord::Base
19 # could have used polymorphic association
19 # could have used polymorphic association
20 # project association here allows easy loading of time entries at project level with one database trip
20 # project association here allows easy loading of time entries at project level with one database trip
21 belongs_to :project
21 belongs_to :project
22 belongs_to :issue
22 belongs_to :issue
23 belongs_to :user
23 belongs_to :user
24 belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
24 belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
25
25
26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27
27
28 acts_as_customizable
28 acts_as_customizable
29 acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
29 acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}},
30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}},
31 :author => :user,
31 :author => :user,
32 :description => :comments
32 :description => :comments
33
33
34 acts_as_activity_provider :timestamp => "#{table_name}.created_on",
34 acts_as_activity_provider :timestamp => "#{table_name}.created_on",
35 :author_key => :user_id,
35 :author_key => :user_id,
36 :find_options => {:include => :project}
36 :find_options => {:include => :project}
37
37
38 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
38 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
39 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
39 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
40 validates_length_of :comments, :maximum => 255, :allow_nil => true
40 validates_length_of :comments, :maximum => 255, :allow_nil => true
41
41
42 def after_initialize
42 def after_initialize
43 if new_record? && self.activity.nil?
43 if new_record? && self.activity.nil?
44 if default_activity = TimeEntryActivity.default
44 if default_activity = TimeEntryActivity.default
45 self.activity_id = default_activity.id
45 self.activity_id = default_activity.id
46 end
46 end
47 self.hours = nil if hours == 0
47 self.hours = nil if hours == 0
48 end
48 end
49 end
49 end
50
50
51 def before_validation
51 def before_validation
52 self.project = issue.project if issue && project.nil?
52 self.project = issue.project if issue && project.nil?
53 end
53 end
54
54
55 def validate
55 def validate
56 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
56 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
57 errors.add :project_id, :invalid if project.nil?
57 errors.add :project_id, :invalid if project.nil?
58 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
58 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
59 end
59 end
60
60
61 def hours=(h)
61 def hours=(h)
62 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
62 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
63 end
63 end
64
64
65 # tyear, tmonth, tweek assigned where setting spent_on attributes
65 # tyear, tmonth, tweek assigned where setting spent_on attributes
66 # these attributes make time aggregations easier
66 # these attributes make time aggregations easier
67 def spent_on=(date)
67 def spent_on=(date)
68 super
68 super
69 self.tyear = spent_on ? spent_on.year : nil
69 self.tyear = spent_on ? spent_on.year : nil
70 self.tmonth = spent_on ? spent_on.month : nil
70 self.tmonth = spent_on ? spent_on.month : nil
71 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
71 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
72 end
72 end
73
73
74 # Returns true if the time entry can be edited by usr, otherwise false
74 # Returns true if the time entry can be edited by usr, otherwise false
75 def editable_by?(usr)
75 def editable_by?(usr)
76 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
76 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
77 end
77 end
78
78
79 def self.visible_by(usr)
79 def self.visible_by(usr)
80 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
80 with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
81 yield
81 yield
82 end
82 end
83 end
83 end
84
84
85 def self.earilest_date_for_project
85 def self.earilest_date_for_project(project=nil)
86 TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries))
86 finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries))
87 if project
88 finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)]
89 end
90 TimeEntry.minimum(:spent_on, :include => :project, :conditions => finder_conditions.conditions)
87 end
91 end
88
92
89 def self.latest_date_for_project
93 def self.latest_date_for_project(project=nil)
90 TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries))
94 finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries))
95 if project
96 finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)]
97 end
98 TimeEntry.maximum(:spent_on, :include => :project, :conditions => finder_conditions.conditions)
91 end
99 end
92 end
100 end
@@ -1,358 +1,358
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # redMine - project management software
2 # redMine - project management software
3 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
19 require File.dirname(__FILE__) + '/../test_helper'
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, :member_roles, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
26 fixtures :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
27
27
28 def setup
28 def setup
29 @controller = TimelogController.new
29 @controller = TimelogController.new
30 @request = ActionController::TestRequest.new
30 @request = ActionController::TestRequest.new
31 @response = ActionController::TestResponse.new
31 @response = ActionController::TestResponse.new
32 end
32 end
33
33
34 def test_get_edit
34 def test_get_edit
35 @request.session[:user_id] = 3
35 @request.session[:user_id] = 3
36 get :edit, :project_id => 1
36 get :edit, :project_id => 1
37 assert_response :success
37 assert_response :success
38 assert_template 'edit'
38 assert_template 'edit'
39 # Default activity selected
39 # Default activity selected
40 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
40 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
41 :content => 'Development'
41 :content => 'Development'
42 end
42 end
43
43
44 def test_get_edit_existing_time
44 def test_get_edit_existing_time
45 @request.session[:user_id] = 2
45 @request.session[:user_id] = 2
46 get :edit, :id => 2, :project_id => nil
46 get :edit, :id => 2, :project_id => nil
47 assert_response :success
47 assert_response :success
48 assert_template 'edit'
48 assert_template 'edit'
49 # Default activity selected
49 # Default activity selected
50 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/timelog/edit/2' }
50 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/timelog/edit/2' }
51 end
51 end
52
52
53 def test_get_edit_should_only_show_active_time_entry_activities
53 def test_get_edit_should_only_show_active_time_entry_activities
54 @request.session[:user_id] = 3
54 @request.session[:user_id] = 3
55 get :edit, :project_id => 1
55 get :edit, :project_id => 1
56 assert_response :success
56 assert_response :success
57 assert_template 'edit'
57 assert_template 'edit'
58 assert_no_tag :tag => 'option', :content => 'Inactive Activity'
58 assert_no_tag :tag => 'option', :content => 'Inactive Activity'
59
59
60 end
60 end
61
61
62 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
62 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
63 te = TimeEntry.find(1)
63 te = TimeEntry.find(1)
64 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
64 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
65 te.save!
65 te.save!
66
66
67 @request.session[:user_id] = 1
67 @request.session[:user_id] = 1
68 get :edit, :project_id => 1, :id => 1
68 get :edit, :project_id => 1, :id => 1
69 assert_response :success
69 assert_response :success
70 assert_template 'edit'
70 assert_template 'edit'
71 # Blank option since nothing is pre-selected
71 # Blank option since nothing is pre-selected
72 assert_tag :tag => 'option', :content => '--- Please select ---'
72 assert_tag :tag => 'option', :content => '--- Please select ---'
73 end
73 end
74
74
75 def test_post_edit
75 def test_post_edit
76 # TODO: should POST to issues’ time log instead of project. change form
76 # TODO: should POST to issues’ time log instead of project. change form
77 # and routing
77 # and routing
78 @request.session[:user_id] = 3
78 @request.session[:user_id] = 3
79 post :edit, :project_id => 1,
79 post :edit, :project_id => 1,
80 :time_entry => {:comments => 'Some work on TimelogControllerTest',
80 :time_entry => {:comments => 'Some work on TimelogControllerTest',
81 # Not the default activity
81 # Not the default activity
82 :activity_id => '11',
82 :activity_id => '11',
83 :spent_on => '2008-03-14',
83 :spent_on => '2008-03-14',
84 :issue_id => '1',
84 :issue_id => '1',
85 :hours => '7.3'}
85 :hours => '7.3'}
86 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
86 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
87
87
88 i = Issue.find(1)
88 i = Issue.find(1)
89 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
89 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
90 assert_not_nil t
90 assert_not_nil t
91 assert_equal 11, t.activity_id
91 assert_equal 11, t.activity_id
92 assert_equal 7.3, t.hours
92 assert_equal 7.3, t.hours
93 assert_equal 3, t.user_id
93 assert_equal 3, t.user_id
94 assert_equal i, t.issue
94 assert_equal i, t.issue
95 assert_equal i.project, t.project
95 assert_equal i.project, t.project
96 end
96 end
97
97
98 def test_update
98 def test_update
99 entry = TimeEntry.find(1)
99 entry = TimeEntry.find(1)
100 assert_equal 1, entry.issue_id
100 assert_equal 1, entry.issue_id
101 assert_equal 2, entry.user_id
101 assert_equal 2, entry.user_id
102
102
103 @request.session[:user_id] = 1
103 @request.session[:user_id] = 1
104 post :edit, :id => 1,
104 post :edit, :id => 1,
105 :time_entry => {:issue_id => '2',
105 :time_entry => {:issue_id => '2',
106 :hours => '8'}
106 :hours => '8'}
107 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
107 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
108 entry.reload
108 entry.reload
109
109
110 assert_equal 8, entry.hours
110 assert_equal 8, entry.hours
111 assert_equal 2, entry.issue_id
111 assert_equal 2, entry.issue_id
112 assert_equal 2, entry.user_id
112 assert_equal 2, entry.user_id
113 end
113 end
114
114
115 def test_destroy
115 def test_destroy
116 @request.session[:user_id] = 2
116 @request.session[:user_id] = 2
117 post :destroy, :id => 1
117 post :destroy, :id => 1
118 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
118 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
119 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
119 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
120 assert_nil TimeEntry.find_by_id(1)
120 assert_nil TimeEntry.find_by_id(1)
121 end
121 end
122
122
123 def test_destroy_should_fail
123 def test_destroy_should_fail
124 # simulate that this fails (e.g. due to a plugin), see #5700
124 # simulate that this fails (e.g. due to a plugin), see #5700
125 TimeEntry.class_eval do
125 TimeEntry.class_eval do
126 before_destroy :stop_callback_chain
126 before_destroy :stop_callback_chain
127 def stop_callback_chain ; return false ; end
127 def stop_callback_chain ; return false ; end
128 end
128 end
129
129
130 @request.session[:user_id] = 2
130 @request.session[:user_id] = 2
131 post :destroy, :id => 1
131 post :destroy, :id => 1
132 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
132 assert_redirected_to :action => 'details', :project_id => 'ecookbook'
133 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
133 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
134 assert_not_nil TimeEntry.find_by_id(1)
134 assert_not_nil TimeEntry.find_by_id(1)
135
135
136 # remove the simulation
136 # remove the simulation
137 TimeEntry.before_destroy.reject! {|callback| callback.method == :stop_callback_chain }
137 TimeEntry.before_destroy.reject! {|callback| callback.method == :stop_callback_chain }
138 end
138 end
139
139
140 def test_report_no_criteria
140 def test_report_no_criteria
141 get :report, :project_id => 1
141 get :report, :project_id => 1
142 assert_response :success
142 assert_response :success
143 assert_template 'report'
143 assert_template 'report'
144 end
144 end
145
145
146 def test_report_all_projects
146 def test_report_all_projects
147 get :report
147 get :report
148 assert_response :success
148 assert_response :success
149 assert_template 'report'
149 assert_template 'report'
150 end
150 end
151
151
152 def test_report_all_projects_denied
152 def test_report_all_projects_denied
153 r = Role.anonymous
153 r = Role.anonymous
154 r.permissions.delete(:view_time_entries)
154 r.permissions.delete(:view_time_entries)
155 r.permissions_will_change!
155 r.permissions_will_change!
156 r.save
156 r.save
157 get :report
157 get :report
158 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
158 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport'
159 end
159 end
160
160
161 def test_report_all_projects_one_criteria
161 def test_report_all_projects_one_criteria
162 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
162 get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
163 assert_response :success
163 assert_response :success
164 assert_template 'report'
164 assert_template 'report'
165 assert_not_nil assigns(:total_hours)
165 assert_not_nil assigns(:total_hours)
166 assert_equal "8.65", "%.2f" % assigns(:total_hours)
166 assert_equal "8.65", "%.2f" % assigns(:total_hours)
167 end
167 end
168
168
169 def test_report_all_time
169 def test_report_all_time
170 get :report, :project_id => 1, :criterias => ['project', 'issue']
170 get :report, :project_id => 1, :criterias => ['project', 'issue']
171 assert_response :success
171 assert_response :success
172 assert_template 'report'
172 assert_template 'report'
173 assert_not_nil assigns(:total_hours)
173 assert_not_nil assigns(:total_hours)
174 assert_equal "162.90", "%.2f" % assigns(:total_hours)
174 assert_equal "162.90", "%.2f" % assigns(:total_hours)
175 end
175 end
176
176
177 def test_report_all_time_by_day
177 def test_report_all_time_by_day
178 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
178 get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
179 assert_response :success
179 assert_response :success
180 assert_template 'report'
180 assert_template 'report'
181 assert_not_nil assigns(:total_hours)
181 assert_not_nil assigns(:total_hours)
182 assert_equal "162.90", "%.2f" % assigns(:total_hours)
182 assert_equal "162.90", "%.2f" % assigns(:total_hours)
183 assert_tag :tag => 'th', :content => '2007-03-12'
183 assert_tag :tag => 'th', :content => '2007-03-12'
184 end
184 end
185
185
186 def test_report_one_criteria
186 def test_report_one_criteria
187 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
187 get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
188 assert_response :success
188 assert_response :success
189 assert_template 'report'
189 assert_template 'report'
190 assert_not_nil assigns(:total_hours)
190 assert_not_nil assigns(:total_hours)
191 assert_equal "8.65", "%.2f" % assigns(:total_hours)
191 assert_equal "8.65", "%.2f" % assigns(:total_hours)
192 end
192 end
193
193
194 def test_report_two_criterias
194 def test_report_two_criterias
195 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
195 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
196 assert_response :success
196 assert_response :success
197 assert_template 'report'
197 assert_template 'report'
198 assert_not_nil assigns(:total_hours)
198 assert_not_nil assigns(:total_hours)
199 assert_equal "162.90", "%.2f" % assigns(:total_hours)
199 assert_equal "162.90", "%.2f" % assigns(:total_hours)
200 end
200 end
201
201
202 def test_report_one_day
202 def test_report_one_day
203 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criterias => ["member", "activity"]
203 get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criterias => ["member", "activity"]
204 assert_response :success
204 assert_response :success
205 assert_template 'report'
205 assert_template 'report'
206 assert_not_nil assigns(:total_hours)
206 assert_not_nil assigns(:total_hours)
207 assert_equal "4.25", "%.2f" % assigns(:total_hours)
207 assert_equal "4.25", "%.2f" % assigns(:total_hours)
208 end
208 end
209
209
210 def test_report_at_issue_level
210 def test_report_at_issue_level
211 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
211 get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
212 assert_response :success
212 assert_response :success
213 assert_template 'report'
213 assert_template 'report'
214 assert_not_nil assigns(:total_hours)
214 assert_not_nil assigns(:total_hours)
215 assert_equal "154.25", "%.2f" % assigns(:total_hours)
215 assert_equal "154.25", "%.2f" % assigns(:total_hours)
216 end
216 end
217
217
218 def test_report_custom_field_criteria
218 def test_report_custom_field_criteria
219 get :report, :project_id => 1, :criterias => ['project', 'cf_1', 'cf_7']
219 get :report, :project_id => 1, :criterias => ['project', 'cf_1', 'cf_7']
220 assert_response :success
220 assert_response :success
221 assert_template 'report'
221 assert_template 'report'
222 assert_not_nil assigns(:total_hours)
222 assert_not_nil assigns(:total_hours)
223 assert_not_nil assigns(:criterias)
223 assert_not_nil assigns(:criterias)
224 assert_equal 3, assigns(:criterias).size
224 assert_equal 3, assigns(:criterias).size
225 assert_equal "162.90", "%.2f" % assigns(:total_hours)
225 assert_equal "162.90", "%.2f" % assigns(:total_hours)
226 # Custom field column
226 # Custom field column
227 assert_tag :tag => 'th', :content => 'Database'
227 assert_tag :tag => 'th', :content => 'Database'
228 # Custom field row
228 # Custom field row
229 assert_tag :tag => 'td', :content => 'MySQL',
229 assert_tag :tag => 'td', :content => 'MySQL',
230 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
230 :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
231 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
231 :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
232 :content => '1' }}
232 :content => '1' }}
233 # Second custom field column
233 # Second custom field column
234 assert_tag :tag => 'th', :content => 'Billable'
234 assert_tag :tag => 'th', :content => 'Billable'
235 end
235 end
236
236
237 def test_report_one_criteria_no_result
237 def test_report_one_criteria_no_result
238 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
238 get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
239 assert_response :success
239 assert_response :success
240 assert_template 'report'
240 assert_template 'report'
241 assert_not_nil assigns(:total_hours)
241 assert_not_nil assigns(:total_hours)
242 assert_equal "0.00", "%.2f" % assigns(:total_hours)
242 assert_equal "0.00", "%.2f" % assigns(:total_hours)
243 end
243 end
244
244
245 def test_report_all_projects_csv_export
245 def test_report_all_projects_csv_export
246 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
246 get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
247 assert_response :success
247 assert_response :success
248 assert_equal 'text/csv', @response.content_type
248 assert_equal 'text/csv', @response.content_type
249 lines = @response.body.chomp.split("\n")
249 lines = @response.body.chomp.split("\n")
250 # Headers
250 # Headers
251 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
251 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
252 # Total row
252 # Total row
253 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
253 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
254 end
254 end
255
255
256 def test_report_csv_export
256 def test_report_csv_export
257 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
257 get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
258 assert_response :success
258 assert_response :success
259 assert_equal 'text/csv', @response.content_type
259 assert_equal 'text/csv', @response.content_type
260 lines = @response.body.chomp.split("\n")
260 lines = @response.body.chomp.split("\n")
261 # Headers
261 # Headers
262 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
262 assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
263 # Total row
263 # Total row
264 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
264 assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
265 end
265 end
266
266
267 def test_details_all_projects
267 def test_details_all_projects
268 get :details
268 get :details
269 assert_response :success
269 assert_response :success
270 assert_template 'details'
270 assert_template 'details'
271 assert_not_nil assigns(:total_hours)
271 assert_not_nil assigns(:total_hours)
272 assert_equal "162.90", "%.2f" % assigns(:total_hours)
272 assert_equal "162.90", "%.2f" % assigns(:total_hours)
273 end
273 end
274
274
275 def test_details_at_project_level
275 def test_details_at_project_level
276 get :details, :project_id => 1
276 get :details, :project_id => 1
277 assert_response :success
277 assert_response :success
278 assert_template 'details'
278 assert_template 'details'
279 assert_not_nil assigns(:entries)
279 assert_not_nil assigns(:entries)
280 assert_equal 4, assigns(:entries).size
280 assert_equal 4, assigns(:entries).size
281 # project and subproject
281 # project and subproject
282 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
282 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
283 assert_not_nil assigns(:total_hours)
283 assert_not_nil assigns(:total_hours)
284 assert_equal "162.90", "%.2f" % assigns(:total_hours)
284 assert_equal "162.90", "%.2f" % assigns(:total_hours)
285 # display all time by default
285 # display all time by default
286 assert_equal '2007-03-11'.to_date, assigns(:from)
286 assert_equal '2007-03-12'.to_date, assigns(:from)
287 assert_equal '2007-04-22'.to_date, assigns(:to)
287 assert_equal '2007-04-22'.to_date, assigns(:to)
288 end
288 end
289
289
290 def test_details_at_project_level_with_date_range
290 def test_details_at_project_level_with_date_range
291 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
291 get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30'
292 assert_response :success
292 assert_response :success
293 assert_template 'details'
293 assert_template 'details'
294 assert_not_nil assigns(:entries)
294 assert_not_nil assigns(:entries)
295 assert_equal 3, assigns(:entries).size
295 assert_equal 3, assigns(:entries).size
296 assert_not_nil assigns(:total_hours)
296 assert_not_nil assigns(:total_hours)
297 assert_equal "12.90", "%.2f" % assigns(:total_hours)
297 assert_equal "12.90", "%.2f" % assigns(:total_hours)
298 assert_equal '2007-03-20'.to_date, assigns(:from)
298 assert_equal '2007-03-20'.to_date, assigns(:from)
299 assert_equal '2007-04-30'.to_date, assigns(:to)
299 assert_equal '2007-04-30'.to_date, assigns(:to)
300 end
300 end
301
301
302 def test_details_at_project_level_with_period
302 def test_details_at_project_level_with_period
303 get :details, :project_id => 1, :period => '7_days'
303 get :details, :project_id => 1, :period => '7_days'
304 assert_response :success
304 assert_response :success
305 assert_template 'details'
305 assert_template 'details'
306 assert_not_nil assigns(:entries)
306 assert_not_nil assigns(:entries)
307 assert_not_nil assigns(:total_hours)
307 assert_not_nil assigns(:total_hours)
308 assert_equal Date.today - 7, assigns(:from)
308 assert_equal Date.today - 7, assigns(:from)
309 assert_equal Date.today, assigns(:to)
309 assert_equal Date.today, assigns(:to)
310 end
310 end
311
311
312 def test_details_one_day
312 def test_details_one_day
313 get :details, :project_id => 1, :from => "2007-03-23", :to => "2007-03-23"
313 get :details, :project_id => 1, :from => "2007-03-23", :to => "2007-03-23"
314 assert_response :success
314 assert_response :success
315 assert_template 'details'
315 assert_template 'details'
316 assert_not_nil assigns(:total_hours)
316 assert_not_nil assigns(:total_hours)
317 assert_equal "4.25", "%.2f" % assigns(:total_hours)
317 assert_equal "4.25", "%.2f" % assigns(:total_hours)
318 end
318 end
319
319
320 def test_details_at_issue_level
320 def test_details_at_issue_level
321 get :details, :issue_id => 1
321 get :details, :issue_id => 1
322 assert_response :success
322 assert_response :success
323 assert_template 'details'
323 assert_template 'details'
324 assert_not_nil assigns(:entries)
324 assert_not_nil assigns(:entries)
325 assert_equal 2, assigns(:entries).size
325 assert_equal 2, assigns(:entries).size
326 assert_not_nil assigns(:total_hours)
326 assert_not_nil assigns(:total_hours)
327 assert_equal 154.25, assigns(:total_hours)
327 assert_equal 154.25, assigns(:total_hours)
328 # display all time by default
328 # display all time based on what's been logged
329 assert_equal '2007-03-11'.to_date, assigns(:from)
329 assert_equal '2007-03-12'.to_date, assigns(:from)
330 assert_equal '2007-04-22'.to_date, assigns(:to)
330 assert_equal '2007-04-22'.to_date, assigns(:to)
331 end
331 end
332
332
333 def test_details_atom_feed
333 def test_details_atom_feed
334 get :details, :project_id => 1, :format => 'atom'
334 get :details, :project_id => 1, :format => 'atom'
335 assert_response :success
335 assert_response :success
336 assert_equal 'application/atom+xml', @response.content_type
336 assert_equal 'application/atom+xml', @response.content_type
337 assert_not_nil assigns(:items)
337 assert_not_nil assigns(:items)
338 assert assigns(:items).first.is_a?(TimeEntry)
338 assert assigns(:items).first.is_a?(TimeEntry)
339 end
339 end
340
340
341 def test_details_all_projects_csv_export
341 def test_details_all_projects_csv_export
342 Setting.date_format = '%m/%d/%Y'
342 Setting.date_format = '%m/%d/%Y'
343 get :details, :format => 'csv'
343 get :details, :format => 'csv'
344 assert_response :success
344 assert_response :success
345 assert_equal 'text/csv', @response.content_type
345 assert_equal 'text/csv', @response.content_type
346 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
346 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
347 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
347 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
348 end
348 end
349
349
350 def test_details_csv_export
350 def test_details_csv_export
351 Setting.date_format = '%m/%d/%Y'
351 Setting.date_format = '%m/%d/%Y'
352 get :details, :project_id => 1, :format => 'csv'
352 get :details, :project_id => 1, :format => 'csv'
353 assert_response :success
353 assert_response :success
354 assert_equal 'text/csv', @response.content_type
354 assert_equal 'text/csv', @response.content_type
355 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
355 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
356 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
356 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
357 end
357 end
358 end
358 end
@@ -1,66 +1,99
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class TimeEntryTest < ActiveSupport::TestCase
20 class TimeEntryTest < ActiveSupport::TestCase
21 fixtures :issues, :projects, :users, :time_entries
21 fixtures :issues, :projects, :users, :time_entries
22
22
23 def test_hours_format
23 def test_hours_format
24 assertions = { "2" => 2.0,
24 assertions = { "2" => 2.0,
25 "21.1" => 21.1,
25 "21.1" => 21.1,
26 "2,1" => 2.1,
26 "2,1" => 2.1,
27 "1,5h" => 1.5,
27 "1,5h" => 1.5,
28 "7:12" => 7.2,
28 "7:12" => 7.2,
29 "10h" => 10.0,
29 "10h" => 10.0,
30 "10 h" => 10.0,
30 "10 h" => 10.0,
31 "45m" => 0.75,
31 "45m" => 0.75,
32 "45 m" => 0.75,
32 "45 m" => 0.75,
33 "3h15" => 3.25,
33 "3h15" => 3.25,
34 "3h 15" => 3.25,
34 "3h 15" => 3.25,
35 "3 h 15" => 3.25,
35 "3 h 15" => 3.25,
36 "3 h 15m" => 3.25,
36 "3 h 15m" => 3.25,
37 "3 h 15 m" => 3.25,
37 "3 h 15 m" => 3.25,
38 "3 hours" => 3.0,
38 "3 hours" => 3.0,
39 "12min" => 0.2,
39 "12min" => 0.2,
40 }
40 }
41
41
42 assertions.each do |k, v|
42 assertions.each do |k, v|
43 t = TimeEntry.new(:hours => k)
43 t = TimeEntry.new(:hours => k)
44 assert_equal v, t.hours, "Converting #{k} failed:"
44 assert_equal v, t.hours, "Converting #{k} failed:"
45 end
45 end
46 end
46 end
47
47
48 def test_hours_should_default_to_nil
48 def test_hours_should_default_to_nil
49 assert_nil TimeEntry.new.hours
49 assert_nil TimeEntry.new.hours
50 end
50 end
51
51
52 context "#earilest_date_for_project" do
52 context "#earilest_date_for_project" do
53 should "return the lowest spent_on value that is visible to the current user" do
53 setup do
54 User.current = nil
54 User.current = nil
55 assert_equal "2007-03-12", TimeEntry.earilest_date_for_project.to_s
55 @public_project = Project.generate!(:is_public => true)
56 @issue = Issue.generate_for_project!(@public_project)
57 TimeEntry.generate!(:spent_on => '2010-01-01',
58 :issue => @issue,
59 :project => @public_project)
56 end
60 end
61
62 context "without a project" do
63 should "return the lowest spent_on value that is visible to the current user" do
64 assert_equal "2007-03-12", TimeEntry.earilest_date_for_project.to_s
65 end
66 end
67
68 context "with a project" do
69 should "return the lowest spent_on value that is visible to the current user for that project and it's subprojects only" do
70 assert_equal "2010-01-01", TimeEntry.earilest_date_for_project(@public_project).to_s
71 end
72 end
73
57 end
74 end
58
75
59 context "#latest_date_for_project" do
76 context "#latest_date_for_project" do
60 should "return the highest spent_on value that is visible to the current user" do
77 setup do
61 User.current = nil
78 User.current = nil
62 assert_equal "2007-04-22", TimeEntry.latest_date_for_project.to_s
79 @public_project = Project.generate!(:is_public => true)
80 @issue = Issue.generate_for_project!(@public_project)
81 TimeEntry.generate!(:spent_on => '2010-01-01',
82 :issue => @issue,
83 :project => @public_project)
63 end
84 end
64 end
85
65
86 context "without a project" do
87 should "return the highest spent_on value that is visible to the current user" do
88 assert_equal "2010-01-01", TimeEntry.latest_date_for_project.to_s
89 end
90 end
91
92 context "with a project" do
93 should "return the highest spent_on value that is visible to the current user for that project and it's subprojects only" do
94 project = Project.find(1)
95 assert_equal "2007-04-22", TimeEntry.latest_date_for_project(project).to_s
96 end
97 end
98 end
66 end
99 end
General Comments 0
You need to be logged in to leave comments. Login now