##// END OF EJS Templates
Change the TimelogController's to/from dates based on the project time entries...
Eric Davis -
r3973:cdfc57d5442f
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,765 +1,774
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 # The earliest start date of a project, based on it's issues and versions
424 # The earliest start date of a project, based on it's issues and versions
425 def start_date
425 def start_date
426 if module_enabled?(:issue_tracking)
426 if module_enabled?(:issue_tracking)
427 [
427 [
428 issues.minimum('start_date'),
428 issues.minimum('start_date'),
429 shared_versions.collect(&:effective_date),
429 shared_versions.collect(&:effective_date),
430 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
430 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
431 ].flatten.compact.min
431 ].flatten.compact.min
432 end
432 end
433 end
433 end
434
434
435 # The latest due date of an issue or version
435 # The latest due date of an issue or version
436 def due_date
436 def due_date
437 if module_enabled?(:issue_tracking)
437 if module_enabled?(:issue_tracking)
438 [
438 [
439 issues.maximum('due_date'),
439 issues.maximum('due_date'),
440 shared_versions.collect(&:effective_date),
440 shared_versions.collect(&:effective_date),
441 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
441 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
442 ].flatten.compact.max
442 ].flatten.compact.max
443 end
443 end
444 end
444 end
445
445
446 def overdue?
446 def overdue?
447 active? && !due_date.nil? && (due_date < Date.today)
447 active? && !due_date.nil? && (due_date < Date.today)
448 end
448 end
449
449
450 # Returns the percent completed for this project, based on the
450 # Returns the percent completed for this project, based on the
451 # progress on it's versions.
451 # progress on it's versions.
452 def completed_percent(options={:include_subprojects => false})
452 def completed_percent(options={:include_subprojects => false})
453 if options.delete(:include_subprojects)
453 if options.delete(:include_subprojects)
454 total = self_and_descendants.collect(&:completed_percent).sum
454 total = self_and_descendants.collect(&:completed_percent).sum
455
455
456 total / self_and_descendants.count
456 total / self_and_descendants.count
457 else
457 else
458 if versions.count > 0
458 if versions.count > 0
459 total = versions.collect(&:completed_pourcent).sum
459 total = versions.collect(&:completed_pourcent).sum
460
460
461 total / versions.count
461 total / versions.count
462 else
462 else
463 100
463 100
464 end
464 end
465 end
465 end
466 end
466 end
467
467
468 # Return true if this project is allowed to do the specified action.
468 # Return true if this project is allowed to do the specified action.
469 # action can be:
469 # action can be:
470 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
470 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
471 # * a permission Symbol (eg. :edit_project)
471 # * a permission Symbol (eg. :edit_project)
472 def allows_to?(action)
472 def allows_to?(action)
473 if action.is_a? Hash
473 if action.is_a? Hash
474 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
474 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
475 else
475 else
476 allowed_permissions.include? action
476 allowed_permissions.include? action
477 end
477 end
478 end
478 end
479
479
480 def module_enabled?(module_name)
480 def module_enabled?(module_name)
481 module_name = module_name.to_s
481 module_name = module_name.to_s
482 enabled_modules.detect {|m| m.name == module_name}
482 enabled_modules.detect {|m| m.name == module_name}
483 end
483 end
484
484
485 def enabled_module_names=(module_names)
485 def enabled_module_names=(module_names)
486 if module_names && module_names.is_a?(Array)
486 if module_names && module_names.is_a?(Array)
487 module_names = module_names.collect(&:to_s)
487 module_names = module_names.collect(&:to_s)
488 # remove disabled modules
488 # remove disabled modules
489 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
489 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
490 # add new modules
490 # add new modules
491 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
491 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
492 else
492 else
493 enabled_modules.clear
493 enabled_modules.clear
494 end
494 end
495 end
495 end
496
497 # Returns an array of projects that are in this project's hierarchy
498 #
499 # Example: parents, children, siblings
500 def hierarchy
501 parents = project.self_and_ancestors || []
502 descendants = project.descendants || []
503 project_hierarchy = parents | descendants # Set union
504 end
496
505
497 # Returns an auto-generated project identifier based on the last identifier used
506 # Returns an auto-generated project identifier based on the last identifier used
498 def self.next_identifier
507 def self.next_identifier
499 p = Project.find(:first, :order => 'created_on DESC')
508 p = Project.find(:first, :order => 'created_on DESC')
500 p.nil? ? nil : p.identifier.to_s.succ
509 p.nil? ? nil : p.identifier.to_s.succ
501 end
510 end
502
511
503 # Copies and saves the Project instance based on the +project+.
512 # Copies and saves the Project instance based on the +project+.
504 # Duplicates the source project's:
513 # Duplicates the source project's:
505 # * Wiki
514 # * Wiki
506 # * Versions
515 # * Versions
507 # * Categories
516 # * Categories
508 # * Issues
517 # * Issues
509 # * Members
518 # * Members
510 # * Queries
519 # * Queries
511 #
520 #
512 # Accepts an +options+ argument to specify what to copy
521 # Accepts an +options+ argument to specify what to copy
513 #
522 #
514 # Examples:
523 # Examples:
515 # project.copy(1) # => copies everything
524 # project.copy(1) # => copies everything
516 # project.copy(1, :only => 'members') # => copies members only
525 # project.copy(1, :only => 'members') # => copies members only
517 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
526 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
518 def copy(project, options={})
527 def copy(project, options={})
519 project = project.is_a?(Project) ? project : Project.find(project)
528 project = project.is_a?(Project) ? project : Project.find(project)
520
529
521 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
530 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
522 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
531 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
523
532
524 Project.transaction do
533 Project.transaction do
525 if save
534 if save
526 reload
535 reload
527 to_be_copied.each do |name|
536 to_be_copied.each do |name|
528 send "copy_#{name}", project
537 send "copy_#{name}", project
529 end
538 end
530 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
539 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
531 save
540 save
532 end
541 end
533 end
542 end
534 end
543 end
535
544
536
545
537 # Copies +project+ and returns the new instance. This will not save
546 # Copies +project+ and returns the new instance. This will not save
538 # the copy
547 # the copy
539 def self.copy_from(project)
548 def self.copy_from(project)
540 begin
549 begin
541 project = project.is_a?(Project) ? project : Project.find(project)
550 project = project.is_a?(Project) ? project : Project.find(project)
542 if project
551 if project
543 # clear unique attributes
552 # clear unique attributes
544 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
553 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
545 copy = Project.new(attributes)
554 copy = Project.new(attributes)
546 copy.enabled_modules = project.enabled_modules
555 copy.enabled_modules = project.enabled_modules
547 copy.trackers = project.trackers
556 copy.trackers = project.trackers
548 copy.custom_values = project.custom_values.collect {|v| v.clone}
557 copy.custom_values = project.custom_values.collect {|v| v.clone}
549 copy.issue_custom_fields = project.issue_custom_fields
558 copy.issue_custom_fields = project.issue_custom_fields
550 return copy
559 return copy
551 else
560 else
552 return nil
561 return nil
553 end
562 end
554 rescue ActiveRecord::RecordNotFound
563 rescue ActiveRecord::RecordNotFound
555 return nil
564 return nil
556 end
565 end
557 end
566 end
558
567
559 private
568 private
560
569
561 # Destroys children before destroying self
570 # Destroys children before destroying self
562 def destroy_children
571 def destroy_children
563 children.each do |child|
572 children.each do |child|
564 child.destroy
573 child.destroy
565 end
574 end
566 end
575 end
567
576
568 # Copies wiki from +project+
577 # Copies wiki from +project+
569 def copy_wiki(project)
578 def copy_wiki(project)
570 # Check that the source project has a wiki first
579 # Check that the source project has a wiki first
571 unless project.wiki.nil?
580 unless project.wiki.nil?
572 self.wiki ||= Wiki.new
581 self.wiki ||= Wiki.new
573 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
582 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
574 wiki_pages_map = {}
583 wiki_pages_map = {}
575 project.wiki.pages.each do |page|
584 project.wiki.pages.each do |page|
576 # Skip pages without content
585 # Skip pages without content
577 next if page.content.nil?
586 next if page.content.nil?
578 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
587 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
579 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
588 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
580 new_wiki_page.content = new_wiki_content
589 new_wiki_page.content = new_wiki_content
581 wiki.pages << new_wiki_page
590 wiki.pages << new_wiki_page
582 wiki_pages_map[page.id] = new_wiki_page
591 wiki_pages_map[page.id] = new_wiki_page
583 end
592 end
584 wiki.save
593 wiki.save
585 # Reproduce page hierarchy
594 # Reproduce page hierarchy
586 project.wiki.pages.each do |page|
595 project.wiki.pages.each do |page|
587 if page.parent_id && wiki_pages_map[page.id]
596 if page.parent_id && wiki_pages_map[page.id]
588 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
597 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
589 wiki_pages_map[page.id].save
598 wiki_pages_map[page.id].save
590 end
599 end
591 end
600 end
592 end
601 end
593 end
602 end
594
603
595 # Copies versions from +project+
604 # Copies versions from +project+
596 def copy_versions(project)
605 def copy_versions(project)
597 project.versions.each do |version|
606 project.versions.each do |version|
598 new_version = Version.new
607 new_version = Version.new
599 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
608 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
600 self.versions << new_version
609 self.versions << new_version
601 end
610 end
602 end
611 end
603
612
604 # Copies issue categories from +project+
613 # Copies issue categories from +project+
605 def copy_issue_categories(project)
614 def copy_issue_categories(project)
606 project.issue_categories.each do |issue_category|
615 project.issue_categories.each do |issue_category|
607 new_issue_category = IssueCategory.new
616 new_issue_category = IssueCategory.new
608 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
617 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
609 self.issue_categories << new_issue_category
618 self.issue_categories << new_issue_category
610 end
619 end
611 end
620 end
612
621
613 # Copies issues from +project+
622 # Copies issues from +project+
614 def copy_issues(project)
623 def copy_issues(project)
615 # Stores the source issue id as a key and the copied issues as the
624 # Stores the source issue id as a key and the copied issues as the
616 # value. Used to map the two togeather for issue relations.
625 # value. Used to map the two togeather for issue relations.
617 issues_map = {}
626 issues_map = {}
618
627
619 # Get issues sorted by root_id, lft so that parent issues
628 # Get issues sorted by root_id, lft so that parent issues
620 # get copied before their children
629 # get copied before their children
621 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
630 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
622 new_issue = Issue.new
631 new_issue = Issue.new
623 new_issue.copy_from(issue)
632 new_issue.copy_from(issue)
624 new_issue.project = self
633 new_issue.project = self
625 # Reassign fixed_versions by name, since names are unique per
634 # Reassign fixed_versions by name, since names are unique per
626 # project and the versions for self are not yet saved
635 # project and the versions for self are not yet saved
627 if issue.fixed_version
636 if issue.fixed_version
628 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
637 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
629 end
638 end
630 # Reassign the category by name, since names are unique per
639 # Reassign the category by name, since names are unique per
631 # project and the categories for self are not yet saved
640 # project and the categories for self are not yet saved
632 if issue.category
641 if issue.category
633 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
642 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
634 end
643 end
635 # Parent issue
644 # Parent issue
636 if issue.parent_id
645 if issue.parent_id
637 if copied_parent = issues_map[issue.parent_id]
646 if copied_parent = issues_map[issue.parent_id]
638 new_issue.parent_issue_id = copied_parent.id
647 new_issue.parent_issue_id = copied_parent.id
639 end
648 end
640 end
649 end
641
650
642 self.issues << new_issue
651 self.issues << new_issue
643 issues_map[issue.id] = new_issue
652 issues_map[issue.id] = new_issue
644 end
653 end
645
654
646 # Relations after in case issues related each other
655 # Relations after in case issues related each other
647 project.issues.each do |issue|
656 project.issues.each do |issue|
648 new_issue = issues_map[issue.id]
657 new_issue = issues_map[issue.id]
649
658
650 # Relations
659 # Relations
651 issue.relations_from.each do |source_relation|
660 issue.relations_from.each do |source_relation|
652 new_issue_relation = IssueRelation.new
661 new_issue_relation = IssueRelation.new
653 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
662 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
654 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
663 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
655 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
664 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
656 new_issue_relation.issue_to = source_relation.issue_to
665 new_issue_relation.issue_to = source_relation.issue_to
657 end
666 end
658 new_issue.relations_from << new_issue_relation
667 new_issue.relations_from << new_issue_relation
659 end
668 end
660
669
661 issue.relations_to.each do |source_relation|
670 issue.relations_to.each do |source_relation|
662 new_issue_relation = IssueRelation.new
671 new_issue_relation = IssueRelation.new
663 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
672 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
664 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
673 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
665 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
674 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
666 new_issue_relation.issue_from = source_relation.issue_from
675 new_issue_relation.issue_from = source_relation.issue_from
667 end
676 end
668 new_issue.relations_to << new_issue_relation
677 new_issue.relations_to << new_issue_relation
669 end
678 end
670 end
679 end
671 end
680 end
672
681
673 # Copies members from +project+
682 # Copies members from +project+
674 def copy_members(project)
683 def copy_members(project)
675 project.memberships.each do |member|
684 project.memberships.each do |member|
676 new_member = Member.new
685 new_member = Member.new
677 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
686 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
678 # only copy non inherited roles
687 # only copy non inherited roles
679 # inherited roles will be added when copying the group membership
688 # inherited roles will be added when copying the group membership
680 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
689 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
681 next if role_ids.empty?
690 next if role_ids.empty?
682 new_member.role_ids = role_ids
691 new_member.role_ids = role_ids
683 new_member.project = self
692 new_member.project = self
684 self.members << new_member
693 self.members << new_member
685 end
694 end
686 end
695 end
687
696
688 # Copies queries from +project+
697 # Copies queries from +project+
689 def copy_queries(project)
698 def copy_queries(project)
690 project.queries.each do |query|
699 project.queries.each do |query|
691 new_query = Query.new
700 new_query = Query.new
692 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
701 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
693 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
702 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
694 new_query.project = self
703 new_query.project = self
695 self.queries << new_query
704 self.queries << new_query
696 end
705 end
697 end
706 end
698
707
699 # Copies boards from +project+
708 # Copies boards from +project+
700 def copy_boards(project)
709 def copy_boards(project)
701 project.boards.each do |board|
710 project.boards.each do |board|
702 new_board = Board.new
711 new_board = Board.new
703 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
712 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
704 new_board.project = self
713 new_board.project = self
705 self.boards << new_board
714 self.boards << new_board
706 end
715 end
707 end
716 end
708
717
709 def allowed_permissions
718 def allowed_permissions
710 @allowed_permissions ||= begin
719 @allowed_permissions ||= begin
711 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
720 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
712 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
721 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
713 end
722 end
714 end
723 end
715
724
716 def allowed_actions
725 def allowed_actions
717 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
726 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
718 end
727 end
719
728
720 # Returns all the active Systemwide and project specific activities
729 # Returns all the active Systemwide and project specific activities
721 def active_activities
730 def active_activities
722 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
731 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
723
732
724 if overridden_activity_ids.empty?
733 if overridden_activity_ids.empty?
725 return TimeEntryActivity.shared.active
734 return TimeEntryActivity.shared.active
726 else
735 else
727 return system_activities_and_project_overrides
736 return system_activities_and_project_overrides
728 end
737 end
729 end
738 end
730
739
731 # Returns all the Systemwide and project specific activities
740 # Returns all the Systemwide and project specific activities
732 # (inactive and active)
741 # (inactive and active)
733 def all_activities
742 def all_activities
734 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
743 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
735
744
736 if overridden_activity_ids.empty?
745 if overridden_activity_ids.empty?
737 return TimeEntryActivity.shared
746 return TimeEntryActivity.shared
738 else
747 else
739 return system_activities_and_project_overrides(true)
748 return system_activities_and_project_overrides(true)
740 end
749 end
741 end
750 end
742
751
743 # Returns the systemwide active activities merged with the project specific overrides
752 # Returns the systemwide active activities merged with the project specific overrides
744 def system_activities_and_project_overrides(include_inactive=false)
753 def system_activities_and_project_overrides(include_inactive=false)
745 if include_inactive
754 if include_inactive
746 return TimeEntryActivity.shared.
755 return TimeEntryActivity.shared.
747 find(:all,
756 find(:all,
748 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
757 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
749 self.time_entry_activities
758 self.time_entry_activities
750 else
759 else
751 return TimeEntryActivity.shared.active.
760 return TimeEntryActivity.shared.active.
752 find(:all,
761 find(:all,
753 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
762 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
754 self.time_entry_activities.active
763 self.time_entry_activities.active
755 end
764 end
756 end
765 end
757
766
758 # Archives subprojects recursively
767 # Archives subprojects recursively
759 def archive!
768 def archive!
760 children.each do |subproject|
769 children.each do |subproject|
761 subproject.send :archive!
770 subproject.send :archive!
762 end
771 end
763 update_attribute :status, STATUS_ARCHIVED
772 update_attribute :status, STATUS_ARCHIVED
764 end
773 end
765 end
774 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