##// END OF EJS Templates
Removed unnecessary calculations in time entries index....
Jean-Philippe Lang -
r7965:10509933486a
parent child
Show More
@@ -1,333 +1,331
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimelogController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_project, :only => [:new, :create]
21 21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 23 before_filter :authorize, :except => [:index, :report]
24 24 before_filter :find_optional_project, :only => [:index, :report]
25 25 accept_rss_auth :index
26 26 accept_api_auth :index, :show, :create, :update, :destroy
27 27
28 28 helper :sort
29 29 include SortHelper
30 30 helper :issues
31 31 include TimelogHelper
32 32 helper :custom_fields
33 33 include CustomFieldsHelper
34 34
35 35 def index
36 36 sort_init 'spent_on', 'desc'
37 37 sort_update 'spent_on' => 'spent_on',
38 38 'user' => 'user_id',
39 39 'activity' => 'activity_id',
40 40 'project' => "#{Project.table_name}.name",
41 41 'issue' => 'issue_id',
42 42 'hours' => 'hours'
43 43
44 44 retrieve_date_range
45 45
46 46 scope = TimeEntry.visible.spent_between(@from, @to)
47 47 if @issue
48 48 scope = scope.on_issue(@issue)
49 49 elsif @project
50 50 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
51 51 end
52 52
53 53 respond_to do |format|
54 54 format.html {
55 55 # Paginate results
56 56 @entry_count = scope.count
57 57 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
58 58 @entries = scope.all(
59 59 :include => [:project, :activity, :user, {:issue => :tracker}],
60 60 :order => sort_clause,
61 61 :limit => @entry_pages.items_per_page,
62 62 :offset => @entry_pages.current.offset
63 63 )
64 64 @total_hours = scope.sum(:hours).to_f
65 65
66 66 render :layout => !request.xhr?
67 67 }
68 68 format.api {
69 69 @entry_count = scope.count
70 70 @offset, @limit = api_offset_and_limit
71 71 @entries = scope.all(
72 72 :include => [:project, :activity, :user, {:issue => :tracker}],
73 73 :order => sort_clause,
74 74 :limit => @limit,
75 75 :offset => @offset
76 76 )
77 77 }
78 78 format.atom {
79 79 entries = scope.all(
80 80 :include => [:project, :activity, :user, {:issue => :tracker}],
81 81 :order => "#{TimeEntry.table_name}.created_on DESC",
82 82 :limit => Setting.feeds_limit.to_i
83 83 )
84 84 render_feed(entries, :title => l(:label_spent_time))
85 85 }
86 86 format.csv {
87 87 # Export all entries
88 88 @entries = scope.all(
89 89 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
90 90 :order => sort_clause
91 91 )
92 92 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
93 93 }
94 94 end
95 95 end
96 96
97 97 def report
98 98 retrieve_date_range
99 99 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
100 100
101 101 respond_to do |format|
102 102 format.html { render :layout => !request.xhr? }
103 103 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
104 104 end
105 105 end
106 106
107 107 def show
108 108 respond_to do |format|
109 109 # TODO: Implement html response
110 110 format.html { render :nothing => true, :status => 406 }
111 111 format.api
112 112 end
113 113 end
114 114
115 115 def new
116 116 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
117 117 @time_entry.attributes = params[:time_entry]
118 118
119 119 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
120 120 render :action => 'edit'
121 121 end
122 122
123 123 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
124 124 def create
125 125 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
126 126 @time_entry.attributes = params[:time_entry]
127 127
128 128 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
129 129
130 130 if @time_entry.save
131 131 respond_to do |format|
132 132 format.html {
133 133 flash[:notice] = l(:notice_successful_update)
134 134 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
135 135 }
136 136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
137 137 end
138 138 else
139 139 respond_to do |format|
140 140 format.html { render :action => 'edit' }
141 141 format.api { render_validation_errors(@time_entry) }
142 142 end
143 143 end
144 144 end
145 145
146 146 def edit
147 147 @time_entry.attributes = params[:time_entry]
148 148
149 149 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
150 150 end
151 151
152 152 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
153 153 def update
154 154 @time_entry.attributes = params[:time_entry]
155 155
156 156 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
157 157
158 158 if @time_entry.save
159 159 respond_to do |format|
160 160 format.html {
161 161 flash[:notice] = l(:notice_successful_update)
162 162 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
163 163 }
164 164 format.api { head :ok }
165 165 end
166 166 else
167 167 respond_to do |format|
168 168 format.html { render :action => 'edit' }
169 169 format.api { render_validation_errors(@time_entry) }
170 170 end
171 171 end
172 172 end
173 173
174 174 def bulk_edit
175 175 @available_activities = TimeEntryActivity.shared.active
176 176 @custom_fields = TimeEntry.first.available_custom_fields
177 177 end
178 178
179 179 def bulk_update
180 180 attributes = parse_params_for_bulk_time_entry_attributes(params)
181 181
182 182 unsaved_time_entry_ids = []
183 183 @time_entries.each do |time_entry|
184 184 time_entry.reload
185 185 time_entry.attributes = attributes
186 186 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
187 187 unless time_entry.save
188 188 # Keep unsaved time_entry ids to display them in flash error
189 189 unsaved_time_entry_ids << time_entry.id
190 190 end
191 191 end
192 192 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
193 193 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
194 194 end
195 195
196 196 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
197 197 def destroy
198 198 @time_entries.each do |t|
199 199 begin
200 200 unless t.destroy && t.destroyed?
201 201 respond_to do |format|
202 202 format.html {
203 203 flash[:error] = l(:notice_unable_delete_time_entry)
204 204 redirect_to :back
205 205 }
206 206 format.api { render_validation_errors(t) }
207 207 end
208 208 return
209 209 end
210 210 rescue ::ActionController::RedirectBackError
211 211 redirect_to :action => 'index', :project_id => @projects.first
212 212 return
213 213 end
214 214 end
215 215
216 216 respond_to do |format|
217 217 format.html {
218 218 flash[:notice] = l(:notice_successful_delete)
219 219 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
220 220 }
221 221 format.api { head :ok }
222 222 end
223 223 end
224 224
225 225 private
226 226 def find_time_entry
227 227 @time_entry = TimeEntry.find(params[:id])
228 228 unless @time_entry.editable_by?(User.current)
229 229 render_403
230 230 return false
231 231 end
232 232 @project = @time_entry.project
233 233 rescue ActiveRecord::RecordNotFound
234 234 render_404
235 235 end
236 236
237 237 def find_time_entries
238 238 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
239 239 raise ActiveRecord::RecordNotFound if @time_entries.empty?
240 240 @projects = @time_entries.collect(&:project).compact.uniq
241 241 @project = @projects.first if @projects.size == 1
242 242 rescue ActiveRecord::RecordNotFound
243 243 render_404
244 244 end
245 245
246 246 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
247 247 if unsaved_time_entry_ids.empty?
248 248 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
249 249 else
250 250 flash[:error] = l(:notice_failed_to_save_time_entries,
251 251 :count => unsaved_time_entry_ids.size,
252 252 :total => time_entries.size,
253 253 :ids => '#' + unsaved_time_entry_ids.join(', #'))
254 254 end
255 255 end
256 256
257 257 def find_project
258 258 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
259 259 @issue = Issue.find(issue_id)
260 260 @project = @issue.project
261 261 elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
262 262 @project = Project.find(project_id)
263 263 else
264 264 render_404
265 265 return false
266 266 end
267 267 rescue ActiveRecord::RecordNotFound
268 268 render_404
269 269 end
270 270
271 271 def find_optional_project
272 272 if !params[:issue_id].blank?
273 273 @issue = Issue.find(params[:issue_id])
274 274 @project = @issue.project
275 275 elsif !params[:project_id].blank?
276 276 @project = Project.find(params[:project_id])
277 277 end
278 278 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
279 279 end
280 280
281 281 # Retrieves the date range based on predefined ranges or specific from/to param dates
282 282 def retrieve_date_range
283 283 @free_period = false
284 284 @from, @to = nil, nil
285 285
286 286 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
287 287 case params[:period].to_s
288 288 when 'today'
289 289 @from = @to = Date.today
290 290 when 'yesterday'
291 291 @from = @to = Date.today - 1
292 292 when 'current_week'
293 293 @from = Date.today - (Date.today.cwday - 1)%7
294 294 @to = @from + 6
295 295 when 'last_week'
296 296 @from = Date.today - 7 - (Date.today.cwday - 1)%7
297 297 @to = @from + 6
298 298 when '7_days'
299 299 @from = Date.today - 7
300 300 @to = Date.today
301 301 when 'current_month'
302 302 @from = Date.civil(Date.today.year, Date.today.month, 1)
303 303 @to = (@from >> 1) - 1
304 304 when 'last_month'
305 305 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
306 306 @to = (@from >> 1) - 1
307 307 when '30_days'
308 308 @from = Date.today - 30
309 309 @to = Date.today
310 310 when 'current_year'
311 311 @from = Date.civil(Date.today.year, 1, 1)
312 312 @to = Date.civil(Date.today.year, 12, 31)
313 313 end
314 314 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
315 315 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
316 316 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
317 317 @free_period = true
318 318 else
319 319 # default
320 320 end
321 321
322 322 @from, @to = @to, @from if @from && @to && @from > @to
323 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
324 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
325 323 end
326 324
327 325 def parse_params_for_bulk_time_entry_attributes(params)
328 326 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
329 327 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
330 328 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
331 329 attributes
332 330 end
333 331 end
@@ -1,115 +1,107
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimeEntry < ActiveRecord::Base
19 19 # could have used polymorphic association
20 20 # project association here allows easy loading of time entries at project level with one database trip
21 21 belongs_to :project
22 22 belongs_to :issue
23 23 belongs_to :user
24 24 belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
25 25
26 26 attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
27 27
28 28 acts_as_customizable
29 29 acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
30 30 :url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
31 31 :author => :user,
32 32 :description => :comments
33 33
34 34 acts_as_activity_provider :timestamp => "#{table_name}.created_on",
35 35 :author_key => :user_id,
36 36 :find_options => {:include => :project}
37 37
38 38 validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
39 39 validates_numericality_of :hours, :allow_nil => true, :message => :invalid
40 40 validates_length_of :comments, :maximum => 255, :allow_nil => true
41 41 before_validation :set_project_if_nil
42 42 validate :validate_time_entry
43 43
44 44 named_scope :visible, lambda {|*args| {
45 45 :include => :project,
46 46 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args)
47 47 }}
48 48 named_scope :on_issue, lambda {|issue| {
49 49 :include => :issue,
50 50 :conditions => "#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}"
51 51 }}
52 52 named_scope :on_project, lambda {|project, include_subprojects| {
53 53 :include => :project,
54 54 :conditions => project.project_condition(include_subprojects)
55 55 }}
56 named_scope :spent_between, lambda {|from, to| {
57 :conditions => ["#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to]
58 }}
59
56 named_scope :spent_between, lambda {|from, to|
57 if from && to
58 {:conditions => ["#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to]}
59 elsif from
60 {:conditions => ["#{TimeEntry.table_name}.spent_on >= ?", from]}
61 elsif to
62 {:conditions => ["#{TimeEntry.table_name}.spent_on <= ?", to]}
63 else
64 {}
65 end
66 }
67
60 68 def after_initialize
61 69 if new_record? && self.activity.nil?
62 70 if default_activity = TimeEntryActivity.default
63 71 self.activity_id = default_activity.id
64 72 end
65 73 self.hours = nil if hours == 0
66 74 end
67 75 end
68 76
69 77 def set_project_if_nil
70 78 self.project = issue.project if issue && project.nil?
71 79 end
72 80
73 81 def validate_time_entry
74 82 errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
75 83 errors.add :project_id, :invalid if project.nil?
76 84 errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
77 85 end
78 86
79 87 def hours=(h)
80 88 write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
81 89 end
82 90
83 91 # tyear, tmonth, tweek assigned where setting spent_on attributes
84 92 # these attributes make time aggregations easier
85 93 def spent_on=(date)
86 94 super
87 95 if spent_on.is_a?(Time)
88 96 self.spent_on = spent_on.to_date
89 97 end
90 98 self.tyear = spent_on ? spent_on.year : nil
91 99 self.tmonth = spent_on ? spent_on.month : nil
92 100 self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
93 101 end
94 102
95 103 # Returns true if the time entry can be edited by usr, otherwise false
96 104 def editable_by?(usr)
97 105 (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
98 106 end
99
100 def self.earilest_date_for_project(project=nil)
101 finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries))
102 if project
103 finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)]
104 end
105 TimeEntry.minimum(:spent_on, :include => :project, :conditions => finder_conditions.conditions)
106 end
107
108 def self.latest_date_for_project(project=nil)
109 finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries))
110 if project
111 finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)]
112 end
113 TimeEntry.maximum(:spent_on, :include => :project, :conditions => finder_conditions.conditions)
114 end
115 107 end
@@ -1,510 +1,510
1 1 # -*- coding: utf-8 -*-
2 2 # Redmine - project management software
3 3 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 4 #
5 5 # This program is free software; you can redistribute it and/or
6 6 # modify it under the terms of the GNU General Public License
7 7 # as published by the Free Software Foundation; either version 2
8 8 # of the License, or (at your option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful,
11 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 13 # GNU General Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License
16 16 # along with this program; if not, write to the Free Software
17 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 18
19 19 require File.expand_path('../../test_helper', __FILE__)
20 20 require 'timelog_controller'
21 21
22 22 # Re-raise errors caught by the controller.
23 23 class TimelogController; def rescue_action(e) raise e end; end
24 24
25 25 class TimelogControllerTest < ActionController::TestCase
26 26 fixtures :projects, :enabled_modules, :roles, :members,
27 27 :member_roles, :issues, :time_entries, :users,
28 28 :trackers, :enumerations, :issue_statuses,
29 29 :custom_fields, :custom_values
30 30
31 31 include Redmine::I18n
32 32
33 33 def setup
34 34 @controller = TimelogController.new
35 35 @request = ActionController::TestRequest.new
36 36 @response = ActionController::TestResponse.new
37 37 end
38 38
39 39 def test_get_new
40 40 @request.session[:user_id] = 3
41 41 get :new, :project_id => 1
42 42 assert_response :success
43 43 assert_template 'edit'
44 44 # Default activity selected
45 45 assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
46 46 :content => 'Development'
47 47 end
48 48
49 49 def test_get_new_should_only_show_active_time_entry_activities
50 50 @request.session[:user_id] = 3
51 51 get :new, :project_id => 1
52 52 assert_response :success
53 53 assert_template 'edit'
54 54 assert_no_tag :tag => 'option', :content => 'Inactive Activity'
55 55 end
56 56
57 57 def test_get_edit_existing_time
58 58 @request.session[:user_id] = 2
59 59 get :edit, :id => 2, :project_id => nil
60 60 assert_response :success
61 61 assert_template 'edit'
62 62 # Default activity selected
63 63 assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/time_entries/2' }
64 64 end
65 65
66 66 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
67 67 te = TimeEntry.find(1)
68 68 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
69 69 te.save!
70 70
71 71 @request.session[:user_id] = 1
72 72 get :edit, :project_id => 1, :id => 1
73 73 assert_response :success
74 74 assert_template 'edit'
75 75 # Blank option since nothing is pre-selected
76 76 assert_tag :tag => 'option', :content => '--- Please select ---'
77 77 end
78 78
79 79 def test_post_create
80 80 # TODO: should POST to issues’ time log instead of project. change form
81 81 # and routing
82 82 @request.session[:user_id] = 3
83 83 post :create, :project_id => 1,
84 84 :time_entry => {:comments => 'Some work on TimelogControllerTest',
85 85 # Not the default activity
86 86 :activity_id => '11',
87 87 :spent_on => '2008-03-14',
88 88 :issue_id => '1',
89 89 :hours => '7.3'}
90 90 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
91 91
92 92 i = Issue.find(1)
93 93 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
94 94 assert_not_nil t
95 95 assert_equal 11, t.activity_id
96 96 assert_equal 7.3, t.hours
97 97 assert_equal 3, t.user_id
98 98 assert_equal i, t.issue
99 99 assert_equal i.project, t.project
100 100 end
101 101
102 102 def test_post_create_with_blank_issue
103 103 # TODO: should POST to issues’ time log instead of project. change form
104 104 # and routing
105 105 @request.session[:user_id] = 3
106 106 post :create, :project_id => 1,
107 107 :time_entry => {:comments => 'Some work on TimelogControllerTest',
108 108 # Not the default activity
109 109 :activity_id => '11',
110 110 :issue_id => '',
111 111 :spent_on => '2008-03-14',
112 112 :hours => '7.3'}
113 113 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
114 114
115 115 t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
116 116 assert_not_nil t
117 117 assert_equal 11, t.activity_id
118 118 assert_equal 7.3, t.hours
119 119 assert_equal 3, t.user_id
120 120 end
121 121
122 122 def test_create_without_log_time_permission_should_be_denied
123 123 @request.session[:user_id] = 2
124 124 Role.find_by_name('Manager').remove_permission! :log_time
125 125 post :create, :project_id => 1,
126 126 :time_entry => {:activity_id => '11',
127 127 :issue_id => '',
128 128 :spent_on => '2008-03-14',
129 129 :hours => '7.3'}
130 130
131 131 assert_response 403
132 132 end
133 133
134 134 def test_update
135 135 entry = TimeEntry.find(1)
136 136 assert_equal 1, entry.issue_id
137 137 assert_equal 2, entry.user_id
138 138
139 139 @request.session[:user_id] = 1
140 140 put :update, :id => 1,
141 141 :time_entry => {:issue_id => '2',
142 142 :hours => '8'}
143 143 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
144 144 entry.reload
145 145
146 146 assert_equal 8, entry.hours
147 147 assert_equal 2, entry.issue_id
148 148 assert_equal 2, entry.user_id
149 149 end
150 150
151 151 def test_get_bulk_edit
152 152 @request.session[:user_id] = 2
153 153 get :bulk_edit, :ids => [1, 2]
154 154 assert_response :success
155 155 assert_template 'bulk_edit'
156 156
157 157 # System wide custom field
158 158 assert_tag :select, :attributes => {:name => 'time_entry[custom_field_values][10]'}
159 159 end
160 160
161 161 def test_get_bulk_edit_on_different_projects
162 162 @request.session[:user_id] = 2
163 163 get :bulk_edit, :ids => [1, 2, 6]
164 164 assert_response :success
165 165 assert_template 'bulk_edit'
166 166 end
167 167
168 168 def test_bulk_update
169 169 @request.session[:user_id] = 2
170 170 # update time entry activity
171 171 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
172 172
173 173 assert_response 302
174 174 # check that the issues were updated
175 175 assert_equal [9, 9], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.activity_id}
176 176 end
177 177
178 178 def test_bulk_update_on_different_projects
179 179 @request.session[:user_id] = 2
180 180 # makes user a manager on the other project
181 181 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
182 182
183 183 # update time entry activity
184 184 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
185 185
186 186 assert_response 302
187 187 # check that the issues were updated
188 188 assert_equal [9, 9, 9], TimeEntry.find_all_by_id([1, 2, 4]).collect {|i| i.activity_id}
189 189 end
190 190
191 191 def test_bulk_update_on_different_projects_without_rights
192 192 @request.session[:user_id] = 3
193 193 user = User.find(3)
194 194 action = { :controller => "timelog", :action => "bulk_update" }
195 195 assert user.allowed_to?(action, TimeEntry.find(1).project)
196 196 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
197 197 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
198 198 assert_response 403
199 199 end
200 200
201 201 def test_bulk_update_custom_field
202 202 @request.session[:user_id] = 2
203 203 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
204 204
205 205 assert_response 302
206 206 assert_equal ["0", "0"], TimeEntry.find_all_by_id([1, 2]).collect {|i| i.custom_value_for(10).value}
207 207 end
208 208
209 209 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
210 210 @request.session[:user_id] = 2
211 211 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
212 212
213 213 assert_response :redirect
214 214 assert_redirected_to '/time_entries'
215 215 end
216 216
217 217 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
218 218 @request.session[:user_id] = 2
219 219 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
220 220
221 221 assert_response :redirect
222 222 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
223 223 end
224 224
225 225 def test_post_bulk_update_without_edit_permission_should_be_denied
226 226 @request.session[:user_id] = 2
227 227 Role.find_by_name('Manager').remove_permission! :edit_time_entries
228 228 post :bulk_update, :ids => [1,2]
229 229
230 230 assert_response 403
231 231 end
232 232
233 233 def test_destroy
234 234 @request.session[:user_id] = 2
235 235 delete :destroy, :id => 1
236 236 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
237 237 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
238 238 assert_nil TimeEntry.find_by_id(1)
239 239 end
240 240
241 241 def test_destroy_should_fail
242 242 # simulate that this fails (e.g. due to a plugin), see #5700
243 243 TimeEntry.any_instance.expects(:destroy).returns(false)
244 244
245 245 @request.session[:user_id] = 2
246 246 delete :destroy, :id => 1
247 247 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
248 248 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
249 249 assert_not_nil TimeEntry.find_by_id(1)
250 250 end
251 251
252 252 def test_index_all_projects
253 253 get :index
254 254 assert_response :success
255 255 assert_template 'index'
256 256 assert_not_nil assigns(:total_hours)
257 257 assert_equal "162.90", "%.2f" % assigns(:total_hours)
258 258 assert_tag :form,
259 259 :attributes => {:action => "/time_entries", :id => 'query_form'}
260 260 end
261 261
262 262 def test_index_at_project_level
263 263 get :index, :project_id => 'ecookbook'
264 264 assert_response :success
265 265 assert_template 'index'
266 266 assert_not_nil assigns(:entries)
267 267 assert_equal 4, assigns(:entries).size
268 268 # project and subproject
269 269 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
270 270 assert_not_nil assigns(:total_hours)
271 271 assert_equal "162.90", "%.2f" % assigns(:total_hours)
272 272 # display all time by default
273 assert_equal '2007-03-12'.to_date, assigns(:from)
274 assert_equal '2007-04-22'.to_date, assigns(:to)
273 assert_nil assigns(:from)
274 assert_nil assigns(:to)
275 275 assert_tag :form,
276 276 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
277 277 end
278 278
279 279 def test_index_at_project_level_with_date_range
280 280 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
281 281 assert_response :success
282 282 assert_template 'index'
283 283 assert_not_nil assigns(:entries)
284 284 assert_equal 3, assigns(:entries).size
285 285 assert_not_nil assigns(:total_hours)
286 286 assert_equal "12.90", "%.2f" % assigns(:total_hours)
287 287 assert_equal '2007-03-20'.to_date, assigns(:from)
288 288 assert_equal '2007-04-30'.to_date, assigns(:to)
289 289 assert_tag :form,
290 290 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
291 291 end
292 292
293 293 def test_index_at_project_level_with_period
294 294 get :index, :project_id => 'ecookbook', :period => '7_days'
295 295 assert_response :success
296 296 assert_template 'index'
297 297 assert_not_nil assigns(:entries)
298 298 assert_not_nil assigns(:total_hours)
299 299 assert_equal Date.today - 7, assigns(:from)
300 300 assert_equal Date.today, assigns(:to)
301 301 assert_tag :form,
302 302 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
303 303 end
304 304
305 305 def test_index_one_day
306 306 get :index, :project_id => 'ecookbook', :from => "2007-03-23", :to => "2007-03-23"
307 307 assert_response :success
308 308 assert_template 'index'
309 309 assert_not_nil assigns(:total_hours)
310 310 assert_equal "4.25", "%.2f" % assigns(:total_hours)
311 311 assert_tag :form,
312 312 :attributes => {:action => "/projects/ecookbook/time_entries", :id => 'query_form'}
313 313 end
314 314
315 315 def test_index_at_issue_level
316 316 get :index, :issue_id => 1
317 317 assert_response :success
318 318 assert_template 'index'
319 319 assert_not_nil assigns(:entries)
320 320 assert_equal 2, assigns(:entries).size
321 321 assert_not_nil assigns(:total_hours)
322 322 assert_equal 154.25, assigns(:total_hours)
323 # display all time based on what's been logged
324 assert_equal '2007-03-12'.to_date, assigns(:from)
325 assert_equal '2007-04-22'.to_date, assigns(:to)
323 # display all time
324 assert_nil assigns(:from)
325 assert_nil assigns(:to)
326 326 # TODO: remove /projects/:project_id/issues/:issue_id/time_entries routes
327 327 # to use /issues/:issue_id/time_entries
328 328 assert_tag :form,
329 329 :attributes => {:action => "/projects/ecookbook/issues/1/time_entries", :id => 'query_form'}
330 330 end
331 331
332 332 def test_index_atom_feed
333 333 get :index, :project_id => 1, :format => 'atom'
334 334 assert_response :success
335 335 assert_equal 'application/atom+xml', @response.content_type
336 336 assert_not_nil assigns(:items)
337 337 assert assigns(:items).first.is_a?(TimeEntry)
338 338 end
339 339
340 340 def test_index_all_projects_csv_export
341 341 Setting.date_format = '%m/%d/%Y'
342 342 get :index, :format => 'csv'
343 343 assert_response :success
344 344 assert_equal 'text/csv', @response.content_type
345 345 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
346 346 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
347 347 end
348 348
349 349 def test_index_csv_export
350 350 Setting.date_format = '%m/%d/%Y'
351 351 get :index, :project_id => 1, :format => 'csv'
352 352 assert_response :success
353 353 assert_equal 'text/csv', @response.content_type
354 354 assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment,Overtime\n")
355 355 assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\",\"\"\n")
356 356 end
357 357
358 358 def test_csv_big_5
359 359 user = User.find_by_id(3)
360 360 user.language = "zh-TW"
361 361 assert user.save
362 362 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
363 363 str_big5 = "\xa4@\xa4\xeb"
364 364 if str_utf8.respond_to?(:force_encoding)
365 365 str_utf8.force_encoding('UTF-8')
366 366 str_big5.force_encoding('Big5')
367 367 end
368 368 @request.session[:user_id] = 3
369 369 post :create, :project_id => 1,
370 370 :time_entry => {:comments => str_utf8,
371 371 # Not the default activity
372 372 :activity_id => '11',
373 373 :issue_id => '',
374 374 :spent_on => '2011-11-10',
375 375 :hours => '7.3'}
376 376 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
377 377
378 378 t = TimeEntry.find_by_comments(str_utf8)
379 379 assert_not_nil t
380 380 assert_equal 11, t.activity_id
381 381 assert_equal 7.3, t.hours
382 382 assert_equal 3, t.user_id
383 383
384 384 get :index, :project_id => 1, :format => 'csv',
385 385 :from => '2011-11-10', :to => '2011-11-10'
386 386 assert_response :success
387 387 assert_equal 'text/csv', @response.content_type
388 388 ar = @response.body.chomp.split("\n")
389 389 s1 = "\xa4\xe9\xb4\xc1"
390 390 if str_utf8.respond_to?(:force_encoding)
391 391 s1.force_encoding('Big5')
392 392 end
393 393 assert ar[0].include?(s1)
394 394 assert ar[1].include?(str_big5)
395 395 end
396 396
397 397 def test_csv_cannot_convert_should_be_replaced_big_5
398 398 user = User.find_by_id(3)
399 399 user.language = "zh-TW"
400 400 assert user.save
401 401 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
402 402 if str_utf8.respond_to?(:force_encoding)
403 403 str_utf8.force_encoding('UTF-8')
404 404 end
405 405 @request.session[:user_id] = 3
406 406 post :create, :project_id => 1,
407 407 :time_entry => {:comments => str_utf8,
408 408 # Not the default activity
409 409 :activity_id => '11',
410 410 :issue_id => '',
411 411 :spent_on => '2011-11-10',
412 412 :hours => '7.3'}
413 413 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
414 414
415 415 t = TimeEntry.find_by_comments(str_utf8)
416 416 assert_not_nil t
417 417 assert_equal 11, t.activity_id
418 418 assert_equal 7.3, t.hours
419 419 assert_equal 3, t.user_id
420 420
421 421 get :index, :project_id => 1, :format => 'csv',
422 422 :from => '2011-11-10', :to => '2011-11-10'
423 423 assert_response :success
424 424 assert_equal 'text/csv', @response.content_type
425 425 ar = @response.body.chomp.split("\n")
426 426 s1 = "\xa4\xe9\xb4\xc1"
427 427 if str_utf8.respond_to?(:force_encoding)
428 428 s1.force_encoding('Big5')
429 429 end
430 430 assert ar[0].include?(s1)
431 431 s2 = ar[1].split(",")[8]
432 432 if s2.respond_to?(:force_encoding)
433 433 s3 = "\xa5H?"
434 434 s3.force_encoding('Big5')
435 435 assert_equal s3, s2
436 436 elsif RUBY_PLATFORM == 'java'
437 437 assert_equal "??", s2
438 438 else
439 439 assert_equal "\xa5H???", s2
440 440 end
441 441 end
442 442
443 443 def test_csv_tw
444 444 with_settings :default_language => "zh-TW" do
445 445 str1 = "test_csv_tw"
446 446 user = User.find_by_id(3)
447 447 te1 = TimeEntry.create(:spent_on => '2011-11-10',
448 448 :hours => 999.9,
449 449 :project => Project.find(1),
450 450 :user => user,
451 451 :activity => TimeEntryActivity.find_by_name('Design'),
452 452 :comments => str1)
453 453 te2 = TimeEntry.find_by_comments(str1)
454 454 assert_not_nil te2
455 455 assert_equal 999.9, te2.hours
456 456 assert_equal 3, te2.user_id
457 457
458 458 get :index, :project_id => 1, :format => 'csv',
459 459 :from => '2011-11-10', :to => '2011-11-10'
460 460 assert_response :success
461 461 assert_equal 'text/csv', @response.content_type
462 462
463 463 ar = @response.body.chomp.split("\n")
464 464 s2 = ar[1].split(",")[7]
465 465 assert_equal '999.9', s2
466 466
467 467 str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)"
468 468 if str_tw.respond_to?(:force_encoding)
469 469 str_tw.force_encoding('UTF-8')
470 470 end
471 471 assert_equal str_tw, l(:general_lang_name)
472 472 assert_equal ',', l(:general_csv_separator)
473 473 assert_equal '.', l(:general_csv_decimal_separator)
474 474 end
475 475 end
476 476
477 477 def test_csv_fr
478 478 with_settings :default_language => "fr" do
479 479 str1 = "test_csv_fr"
480 480 user = User.find_by_id(3)
481 481 te1 = TimeEntry.create(:spent_on => '2011-11-10',
482 482 :hours => 999.9,
483 483 :project => Project.find(1),
484 484 :user => user,
485 485 :activity => TimeEntryActivity.find_by_name('Design'),
486 486 :comments => str1)
487 487 te2 = TimeEntry.find_by_comments(str1)
488 488 assert_not_nil te2
489 489 assert_equal 999.9, te2.hours
490 490 assert_equal 3, te2.user_id
491 491
492 492 get :index, :project_id => 1, :format => 'csv',
493 493 :from => '2011-11-10', :to => '2011-11-10'
494 494 assert_response :success
495 495 assert_equal 'text/csv', @response.content_type
496 496
497 497 ar = @response.body.chomp.split("\n")
498 498 s2 = ar[1].split(";")[7]
499 499 assert_equal '999,9', s2
500 500
501 501 str_fr = "Fran\xc3\xa7ais"
502 502 if str_fr.respond_to?(:force_encoding)
503 503 str_fr.force_encoding('UTF-8')
504 504 end
505 505 assert_equal str_fr, l(:general_lang_name)
506 506 assert_equal ';', l(:general_csv_separator)
507 507 assert_equal ',', l(:general_csv_decimal_separator)
508 508 end
509 509 end
510 510 end
@@ -1,176 +1,128
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class TimeEntryTest < ActiveSupport::TestCase
21 21 fixtures :issues, :projects, :users, :time_entries,
22 22 :members, :roles, :member_roles, :auth_sources,
23 23 :trackers, :issue_statuses,
24 24 :projects_trackers,
25 25 :journals, :journal_details,
26 26 :issue_categories, :enumerations,
27 27 :groups_users,
28 28 :enabled_modules,
29 29 :workflows
30 30
31 31 def test_hours_format
32 32 assertions = { "2" => 2.0,
33 33 "21.1" => 21.1,
34 34 "2,1" => 2.1,
35 35 "1,5h" => 1.5,
36 36 "7:12" => 7.2,
37 37 "10h" => 10.0,
38 38 "10 h" => 10.0,
39 39 "45m" => 0.75,
40 40 "45 m" => 0.75,
41 41 "3h15" => 3.25,
42 42 "3h 15" => 3.25,
43 43 "3 h 15" => 3.25,
44 44 "3 h 15m" => 3.25,
45 45 "3 h 15 m" => 3.25,
46 46 "3 hours" => 3.0,
47 47 "12min" => 0.2,
48 48 }
49 49
50 50 assertions.each do |k, v|
51 51 t = TimeEntry.new(:hours => k)
52 52 assert_equal v, t.hours, "Converting #{k} failed:"
53 53 end
54 54 end
55 55
56 56 def test_hours_should_default_to_nil
57 57 assert_nil TimeEntry.new.hours
58 58 end
59 59
60 60 def test_spent_on_with_blank
61 61 c = TimeEntry.new
62 62 c.spent_on = ''
63 63 assert_nil c.spent_on
64 64 end
65 65
66 66 def test_spent_on_with_nil
67 67 c = TimeEntry.new
68 68 c.spent_on = nil
69 69 assert_nil c.spent_on
70 70 end
71 71
72 72 def test_spent_on_with_string
73 73 c = TimeEntry.new
74 74 c.spent_on = "2011-01-14"
75 75 assert_equal Date.parse("2011-01-14"), c.spent_on
76 76 end
77 77
78 78 def test_spent_on_with_invalid_string
79 79 c = TimeEntry.new
80 80 c.spent_on = "foo"
81 81 assert_nil c.spent_on
82 82 end
83 83
84 84 def test_spent_on_with_date
85 85 c = TimeEntry.new
86 86 c.spent_on = Date.today
87 87 assert_equal Date.today, c.spent_on
88 88 end
89 89
90 90 def test_spent_on_with_time
91 91 c = TimeEntry.new
92 92 c.spent_on = Time.now
93 93 assert_equal Date.today, c.spent_on
94 94 end
95 95
96 96 def test_validate_time_entry
97 97 anon = User.anonymous
98 98 project = Project.find(1)
99 99 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => anon.id, :status_id => 1,
100 100 :priority => IssuePriority.all.first, :subject => 'test_create',
101 101 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
102 102 assert issue.save
103 103 activity = TimeEntryActivity.find_by_name('Design')
104 104 te = TimeEntry.create(:spent_on => '2010-01-01',
105 105 :hours => 100000,
106 106 :issue => issue,
107 107 :project => project,
108 108 :user => anon,
109 109 :activity => activity)
110 110 assert_equal 1, te.errors.count
111 111 end
112 112
113 113 def test_set_project_if_nil
114 114 anon = User.anonymous
115 115 project = Project.find(1)
116 116 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => anon.id, :status_id => 1,
117 117 :priority => IssuePriority.all.first, :subject => 'test_create',
118 118 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
119 119 assert issue.save
120 120 activity = TimeEntryActivity.find_by_name('Design')
121 121 te = TimeEntry.create(:spent_on => '2010-01-01',
122 122 :hours => 10,
123 123 :issue => issue,
124 124 :user => anon,
125 125 :activity => activity)
126 126 assert_equal project.id, te.project.id
127 127 end
128
129 context "#earilest_date_for_project" do
130 setup do
131 User.current = nil
132 @public_project = Project.generate!(:is_public => true)
133 @issue = Issue.generate_for_project!(@public_project)
134 TimeEntry.generate!(:spent_on => '2010-01-01',
135 :issue => @issue,
136 :project => @public_project)
137 end
138
139 context "without a project" do
140 should "return the lowest spent_on value that is visible to the current user" do
141 assert_equal "2007-03-12", TimeEntry.earilest_date_for_project.to_s
142 end
143 end
144
145 context "with a project" do
146 should "return the lowest spent_on value that is visible to the current user for that project and it's subprojects only" do
147 assert_equal "2010-01-01", TimeEntry.earilest_date_for_project(@public_project).to_s
148 end
149 end
150
151 end
152
153 context "#latest_date_for_project" do
154 setup do
155 User.current = nil
156 @public_project = Project.generate!(:is_public => true)
157 @issue = Issue.generate_for_project!(@public_project)
158 TimeEntry.generate!(:spent_on => '2010-01-01',
159 :issue => @issue,
160 :project => @public_project)
161 end
162
163 context "without a project" do
164 should "return the highest spent_on value that is visible to the current user" do
165 assert_equal "2010-01-01", TimeEntry.latest_date_for_project.to_s
166 end
167 end
168
169 context "with a project" do
170 should "return the highest spent_on value that is visible to the current user for that project and it's subprojects only" do
171 project = Project.find(1)
172 assert_equal "2007-04-22", TimeEntry.latest_date_for_project(project).to_s
173 end
174 end
175 end
176 128 end
General Comments 0
You need to be logged in to leave comments. Login now