##// END OF EJS Templates
Merged r16056 (#14817)....
Jean-Philippe Lang -
r15715:ac456fafc8c0
parent child
Show More
@@ -1,281 +1,281
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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
21 21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 22 before_filter :check_editability, :only => [:edit, :update]
23 23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
24 24 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
25 25
26 26 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
27 27 before_filter :authorize_global, :only => [:new, :create, :index, :report]
28 28
29 29 accept_rss_auth :index
30 30 accept_api_auth :index, :show, :create, :update, :destroy
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :sort
35 35 include SortHelper
36 36 helper :issues
37 37 include TimelogHelper
38 38 helper :custom_fields
39 39 include CustomFieldsHelper
40 40 helper :queries
41 41 include QueriesHelper
42 42
43 43 def index
44 44 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
45 45
46 46 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
47 47 sort_update(@query.sortable_columns)
48 48 scope = time_entry_scope(:order => sort_clause).
49 49 includes(:project, :user, :issue).
50 50 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
51 51
52 52 respond_to do |format|
53 53 format.html {
54 54 @entry_count = scope.count
55 55 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
56 56 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
57 57 @total_hours = scope.sum(:hours).to_f
58 58
59 59 render :layout => !request.xhr?
60 60 }
61 61 format.api {
62 62 @entry_count = scope.count
63 63 @offset, @limit = api_offset_and_limit
64 64 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
65 65 }
66 66 format.atom {
67 67 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
68 68 render_feed(entries, :title => l(:label_spent_time))
69 69 }
70 70 format.csv {
71 71 # Export all entries
72 72 @entries = scope.to_a
73 73 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
74 74 }
75 75 end
76 76 end
77 77
78 78 def report
79 79 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
80 80 scope = time_entry_scope
81 81
82 82 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
83 83
84 84 respond_to do |format|
85 85 format.html { render :layout => !request.xhr? }
86 86 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
87 87 end
88 88 end
89 89
90 90 def show
91 91 respond_to do |format|
92 92 # TODO: Implement html response
93 93 format.html { render :nothing => true, :status => 406 }
94 94 format.api
95 95 end
96 96 end
97 97
98 98 def new
99 99 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
100 100 @time_entry.safe_attributes = params[:time_entry]
101 101 end
102 102
103 103 def create
104 104 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
105 105 @time_entry.safe_attributes = params[:time_entry]
106 106 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
107 107 render_403
108 108 return
109 109 end
110 110
111 111 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
112 112
113 113 if @time_entry.save
114 114 respond_to do |format|
115 115 format.html {
116 116 flash[:notice] = l(:notice_successful_create)
117 117 if params[:continue]
118 118 options = {
119 119 :time_entry => {
120 120 :project_id => params[:time_entry][:project_id],
121 121 :issue_id => @time_entry.issue_id,
122 122 :activity_id => @time_entry.activity_id
123 123 },
124 124 :back_url => params[:back_url]
125 125 }
126 126 if params[:project_id] && @time_entry.project
127 127 redirect_to new_project_time_entry_path(@time_entry.project, options)
128 128 elsif params[:issue_id] && @time_entry.issue
129 129 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
130 130 else
131 131 redirect_to new_time_entry_path(options)
132 132 end
133 133 else
134 134 redirect_back_or_default project_time_entries_path(@time_entry.project)
135 135 end
136 136 }
137 137 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
138 138 end
139 139 else
140 140 respond_to do |format|
141 141 format.html { render :action => 'new' }
142 142 format.api { render_validation_errors(@time_entry) }
143 143 end
144 144 end
145 145 end
146 146
147 147 def edit
148 148 @time_entry.safe_attributes = params[:time_entry]
149 149 end
150 150
151 151 def update
152 152 @time_entry.safe_attributes = params[:time_entry]
153 153
154 154 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
155 155
156 156 if @time_entry.save
157 157 respond_to do |format|
158 158 format.html {
159 159 flash[:notice] = l(:notice_successful_update)
160 160 redirect_back_or_default project_time_entries_path(@time_entry.project)
161 161 }
162 162 format.api { render_api_ok }
163 163 end
164 164 else
165 165 respond_to do |format|
166 166 format.html { render :action => 'edit' }
167 167 format.api { render_validation_errors(@time_entry) }
168 168 end
169 169 end
170 170 end
171 171
172 172 def bulk_edit
173 173 @available_activities = @projects.map(&:activities).reduce(:&)
174 174 @custom_fields = TimeEntry.first.available_custom_fields
175 175 end
176 176
177 177 def bulk_update
178 178 attributes = parse_params_for_bulk_update(params[:time_entry])
179 179
180 180 unsaved_time_entry_ids = []
181 181 @time_entries.each do |time_entry|
182 182 time_entry.reload
183 183 time_entry.safe_attributes = attributes
184 184 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
185 185 unless time_entry.save
186 186 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
187 187 # Keep unsaved time_entry ids to display them in flash error
188 188 unsaved_time_entry_ids << time_entry.id
189 189 end
190 190 end
191 191 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
192 192 redirect_back_or_default project_time_entries_path(@projects.first)
193 193 end
194 194
195 195 def destroy
196 196 destroyed = TimeEntry.transaction do
197 197 @time_entries.each do |t|
198 198 unless t.destroy && t.destroyed?
199 199 raise ActiveRecord::Rollback
200 200 end
201 201 end
202 202 end
203 203
204 204 respond_to do |format|
205 205 format.html {
206 206 if destroyed
207 207 flash[:notice] = l(:notice_successful_delete)
208 208 else
209 209 flash[:error] = l(:notice_unable_delete_time_entry)
210 210 end
211 redirect_back_or_default project_time_entries_path(@projects.first)
211 redirect_back_or_default project_time_entries_path(@projects.first), :referer => true
212 212 }
213 213 format.api {
214 214 if destroyed
215 215 render_api_ok
216 216 else
217 217 render_validation_errors(@time_entries)
218 218 end
219 219 }
220 220 end
221 221 end
222 222
223 223 private
224 224 def find_time_entry
225 225 @time_entry = TimeEntry.find(params[:id])
226 226 @project = @time_entry.project
227 227 rescue ActiveRecord::RecordNotFound
228 228 render_404
229 229 end
230 230
231 231 def check_editability
232 232 unless @time_entry.editable_by?(User.current)
233 233 render_403
234 234 return false
235 235 end
236 236 end
237 237
238 238 def find_time_entries
239 239 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).
240 240 preload(:project => :time_entry_activities).
241 241 preload(:user).to_a
242 242
243 243 raise ActiveRecord::RecordNotFound if @time_entries.empty?
244 244 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
245 245 @projects = @time_entries.collect(&:project).compact.uniq
246 246 @project = @projects.first if @projects.size == 1
247 247 rescue ActiveRecord::RecordNotFound
248 248 render_404
249 249 end
250 250
251 251 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
252 252 if unsaved_time_entry_ids.empty?
253 253 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
254 254 else
255 255 flash[:error] = l(:notice_failed_to_save_time_entries,
256 256 :count => unsaved_time_entry_ids.size,
257 257 :total => time_entries.size,
258 258 :ids => '#' + unsaved_time_entry_ids.join(', #'))
259 259 end
260 260 end
261 261
262 262 def find_optional_project
263 263 if params[:issue_id].present?
264 264 @issue = Issue.find(params[:issue_id])
265 265 @project = @issue.project
266 266 elsif params[:project_id].present?
267 267 @project = Project.find(params[:project_id])
268 268 end
269 269 rescue ActiveRecord::RecordNotFound
270 270 render_404
271 271 end
272 272
273 273 # Returns the TimeEntry scope for index and report actions
274 274 def time_entry_scope(options={})
275 275 scope = @query.results_scope(options)
276 276 if @issue
277 277 scope = scope.on_issue(@issue)
278 278 end
279 279 scope
280 280 end
281 281 end
@@ -1,835 +1,844
1 1 # -*- coding: utf-8 -*-
2 2 # Redmine - project management software
3 3 # Copyright (C) 2006-2016 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
21 21 class TimelogControllerTest < ActionController::TestCase
22 22 fixtures :projects, :enabled_modules, :roles, :members,
23 23 :member_roles, :issues, :time_entries, :users,
24 24 :trackers, :enumerations, :issue_statuses,
25 25 :custom_fields, :custom_values,
26 26 :projects_trackers, :custom_fields_trackers,
27 27 :custom_fields_projects
28 28
29 29 include Redmine::I18n
30 30
31 31 def test_new
32 32 @request.session[:user_id] = 3
33 33 get :new
34 34 assert_response :success
35 35 assert_template 'new'
36 36 assert_select 'input[name=?][type=hidden]', 'project_id', 0
37 37 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
38 38 assert_select 'select[name=?]', 'time_entry[project_id]' do
39 39 # blank option for project
40 40 assert_select 'option[value=""]'
41 41 end
42 42 end
43 43
44 44 def test_new_with_project_id
45 45 @request.session[:user_id] = 3
46 46 get :new, :project_id => 1
47 47 assert_response :success
48 48 assert_template 'new'
49 49 assert_select 'input[name=?][type=hidden]', 'project_id'
50 50 assert_select 'input[name=?][type=hidden]', 'issue_id', 0
51 51 assert_select 'select[name=?]', 'time_entry[project_id]', 0
52 52 end
53 53
54 54 def test_new_with_issue_id
55 55 @request.session[:user_id] = 3
56 56 get :new, :issue_id => 2
57 57 assert_response :success
58 58 assert_template 'new'
59 59 assert_select 'input[name=?][type=hidden]', 'project_id', 0
60 60 assert_select 'input[name=?][type=hidden]', 'issue_id'
61 61 assert_select 'select[name=?]', 'time_entry[project_id]', 0
62 62 end
63 63
64 64 def test_new_without_project_should_prefill_the_form
65 65 @request.session[:user_id] = 3
66 66 get :new, :time_entry => {:project_id => '1'}
67 67 assert_response :success
68 68 assert_template 'new'
69 69 assert_select 'select[name=?]', 'time_entry[project_id]' do
70 70 assert_select 'option[value="1"][selected=selected]'
71 71 end
72 72 end
73 73
74 74 def test_new_without_project_should_deny_without_permission
75 75 Role.all.each {|role| role.remove_permission! :log_time}
76 76 @request.session[:user_id] = 3
77 77
78 78 get :new
79 79 assert_response 403
80 80 end
81 81
82 82 def test_new_should_select_default_activity
83 83 @request.session[:user_id] = 3
84 84 get :new, :project_id => 1
85 85 assert_response :success
86 86 assert_select 'select[name=?]', 'time_entry[activity_id]' do
87 87 assert_select 'option[selected=selected]', :text => 'Development'
88 88 end
89 89 end
90 90
91 91 def test_new_should_only_show_active_time_entry_activities
92 92 @request.session[:user_id] = 3
93 93 get :new, :project_id => 1
94 94 assert_response :success
95 95 assert_select 'option', :text => 'Inactive Activity', :count => 0
96 96 end
97 97
98 98 def test_post_new_as_js_should_update_activity_options
99 99 @request.session[:user_id] = 3
100 100 post :new, :time_entry => {:project_id => 1}, :format => 'js'
101 101 assert_response :success
102 102 assert_include '#time_entry_activity_id', response.body
103 103 end
104 104
105 105 def test_get_edit_existing_time
106 106 @request.session[:user_id] = 2
107 107 get :edit, :id => 2, :project_id => nil
108 108 assert_response :success
109 109 assert_template 'edit'
110 110 assert_select 'form[action=?]', '/time_entries/2'
111 111 end
112 112
113 113 def test_get_edit_with_an_existing_time_entry_with_inactive_activity
114 114 te = TimeEntry.find(1)
115 115 te.activity = TimeEntryActivity.find_by_name("Inactive Activity")
116 116 te.save!(:validate => false)
117 117
118 118 @request.session[:user_id] = 1
119 119 get :edit, :project_id => 1, :id => 1
120 120 assert_response :success
121 121 assert_template 'edit'
122 122 # Blank option since nothing is pre-selected
123 123 assert_select 'option', :text => '--- Please select ---'
124 124 end
125 125
126 126 def test_post_create
127 127 @request.session[:user_id] = 3
128 128 assert_difference 'TimeEntry.count' do
129 129 post :create, :project_id => 1,
130 130 :time_entry => {:comments => 'Some work on TimelogControllerTest',
131 131 # Not the default activity
132 132 :activity_id => '11',
133 133 :spent_on => '2008-03-14',
134 134 :issue_id => '1',
135 135 :hours => '7.3'}
136 136 assert_redirected_to '/projects/ecookbook/time_entries'
137 137 end
138 138
139 139 t = TimeEntry.order('id DESC').first
140 140 assert_not_nil t
141 141 assert_equal 'Some work on TimelogControllerTest', t.comments
142 142 assert_equal 1, t.project_id
143 143 assert_equal 1, t.issue_id
144 144 assert_equal 11, t.activity_id
145 145 assert_equal 7.3, t.hours
146 146 assert_equal 3, t.user_id
147 147 end
148 148
149 149 def test_post_create_with_blank_issue
150 150 @request.session[:user_id] = 3
151 151 assert_difference 'TimeEntry.count' do
152 152 post :create, :project_id => 1,
153 153 :time_entry => {:comments => 'Some work on TimelogControllerTest',
154 154 # Not the default activity
155 155 :activity_id => '11',
156 156 :issue_id => '',
157 157 :spent_on => '2008-03-14',
158 158 :hours => '7.3'}
159 159 assert_redirected_to '/projects/ecookbook/time_entries'
160 160 end
161 161
162 162 t = TimeEntry.order('id DESC').first
163 163 assert_not_nil t
164 164 assert_equal 'Some work on TimelogControllerTest', t.comments
165 165 assert_equal 1, t.project_id
166 166 assert_nil t.issue_id
167 167 assert_equal 11, t.activity_id
168 168 assert_equal 7.3, t.hours
169 169 assert_equal 3, t.user_id
170 170 end
171 171
172 172 def test_create_on_project_with_time_tracking_disabled_should_fail
173 173 Project.find(1).disable_module! :time_tracking
174 174
175 175 @request.session[:user_id] = 2
176 176 assert_no_difference 'TimeEntry.count' do
177 177 post :create, :time_entry => {
178 178 :project_id => '1', :issue_id => '',
179 179 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
180 180 }
181 181 end
182 182 end
183 183
184 184 def test_create_on_project_without_permission_should_fail
185 185 Role.find(1).remove_permission! :log_time
186 186
187 187 @request.session[:user_id] = 2
188 188 assert_no_difference 'TimeEntry.count' do
189 189 post :create, :time_entry => {
190 190 :project_id => '1', :issue_id => '',
191 191 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
192 192 }
193 193 end
194 194 end
195 195
196 196 def test_create_on_issue_in_project_with_time_tracking_disabled_should_fail
197 197 Project.find(1).disable_module! :time_tracking
198 198
199 199 @request.session[:user_id] = 2
200 200 assert_no_difference 'TimeEntry.count' do
201 201 post :create, :time_entry => {
202 202 :project_id => '', :issue_id => '1',
203 203 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
204 204 }
205 205 assert_select_error /Issue is invalid/
206 206 end
207 207 end
208 208
209 209 def test_create_on_issue_in_project_without_permission_should_fail
210 210 Role.find(1).remove_permission! :log_time
211 211
212 212 @request.session[:user_id] = 2
213 213 assert_no_difference 'TimeEntry.count' do
214 214 post :create, :time_entry => {
215 215 :project_id => '', :issue_id => '1',
216 216 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
217 217 }
218 218 assert_select_error /Issue is invalid/
219 219 end
220 220 end
221 221
222 222 def test_create_on_issue_that_is_not_visible_should_not_disclose_subject
223 223 issue = Issue.generate!(:subject => "issue_that_is_not_visible", :is_private => true)
224 224 assert !issue.visible?(User.find(3))
225 225
226 226 @request.session[:user_id] = 3
227 227 assert_no_difference 'TimeEntry.count' do
228 228 post :create, :time_entry => {
229 229 :project_id => '', :issue_id => issue.id.to_s,
230 230 :activity_id => '11', :spent_on => '2008-03-14', :hours => '7.3'
231 231 }
232 232 end
233 233 assert_select_error /Issue is invalid/
234 234 assert_select "input[name=?][value=?]", "time_entry[issue_id]", issue.id.to_s
235 235 assert_select "#time_entry_issue", 0
236 236 assert !response.body.include?('issue_that_is_not_visible')
237 237 end
238 238
239 239 def test_create_and_continue_at_project_level
240 240 @request.session[:user_id] = 2
241 241 assert_difference 'TimeEntry.count' do
242 242 post :create, :time_entry => {:project_id => '1',
243 243 :activity_id => '11',
244 244 :issue_id => '',
245 245 :spent_on => '2008-03-14',
246 246 :hours => '7.3'},
247 247 :continue => '1'
248 248 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D=1'
249 249 end
250 250 end
251 251
252 252 def test_create_and_continue_at_issue_level
253 253 @request.session[:user_id] = 2
254 254 assert_difference 'TimeEntry.count' do
255 255 post :create, :time_entry => {:project_id => '',
256 256 :activity_id => '11',
257 257 :issue_id => '1',
258 258 :spent_on => '2008-03-14',
259 259 :hours => '7.3'},
260 260 :continue => '1'
261 261 assert_redirected_to '/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
262 262 end
263 263 end
264 264
265 265 def test_create_and_continue_with_project_id
266 266 @request.session[:user_id] = 2
267 267 assert_difference 'TimeEntry.count' do
268 268 post :create, :project_id => 1,
269 269 :time_entry => {:activity_id => '11',
270 270 :issue_id => '',
271 271 :spent_on => '2008-03-14',
272 272 :hours => '7.3'},
273 273 :continue => '1'
274 274 assert_redirected_to '/projects/ecookbook/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=&time_entry%5Bproject_id%5D='
275 275 end
276 276 end
277 277
278 278 def test_create_and_continue_with_issue_id
279 279 @request.session[:user_id] = 2
280 280 assert_difference 'TimeEntry.count' do
281 281 post :create, :issue_id => 1,
282 282 :time_entry => {:activity_id => '11',
283 283 :issue_id => '1',
284 284 :spent_on => '2008-03-14',
285 285 :hours => '7.3'},
286 286 :continue => '1'
287 287 assert_redirected_to '/issues/1/time_entries/new?time_entry%5Bactivity_id%5D=11&time_entry%5Bissue_id%5D=1&time_entry%5Bproject_id%5D='
288 288 end
289 289 end
290 290
291 291 def test_create_without_log_time_permission_should_be_denied
292 292 @request.session[:user_id] = 2
293 293 Role.find_by_name('Manager').remove_permission! :log_time
294 294 post :create, :project_id => 1,
295 295 :time_entry => {:activity_id => '11',
296 296 :issue_id => '',
297 297 :spent_on => '2008-03-14',
298 298 :hours => '7.3'}
299 299
300 300 assert_response 403
301 301 end
302 302
303 303 def test_create_without_project_and_issue_should_fail
304 304 @request.session[:user_id] = 2
305 305 post :create, :time_entry => {:issue_id => ''}
306 306
307 307 assert_response :success
308 308 assert_template 'new'
309 309 end
310 310
311 311 def test_create_with_failure
312 312 @request.session[:user_id] = 2
313 313 post :create, :project_id => 1,
314 314 :time_entry => {:activity_id => '',
315 315 :issue_id => '',
316 316 :spent_on => '2008-03-14',
317 317 :hours => '7.3'}
318 318
319 319 assert_response :success
320 320 assert_template 'new'
321 321 end
322 322
323 323 def test_create_without_project
324 324 @request.session[:user_id] = 2
325 325 assert_difference 'TimeEntry.count' do
326 326 post :create, :time_entry => {:project_id => '1',
327 327 :activity_id => '11',
328 328 :issue_id => '',
329 329 :spent_on => '2008-03-14',
330 330 :hours => '7.3'}
331 331 end
332 332
333 333 assert_redirected_to '/projects/ecookbook/time_entries'
334 334 time_entry = TimeEntry.order('id DESC').first
335 335 assert_equal 1, time_entry.project_id
336 336 end
337 337
338 338 def test_create_without_project_should_fail_with_issue_not_inside_project
339 339 @request.session[:user_id] = 2
340 340 assert_no_difference 'TimeEntry.count' do
341 341 post :create, :time_entry => {:project_id => '1',
342 342 :activity_id => '11',
343 343 :issue_id => '5',
344 344 :spent_on => '2008-03-14',
345 345 :hours => '7.3'}
346 346 end
347 347
348 348 assert_response :success
349 349 assert assigns(:time_entry).errors[:issue_id].present?
350 350 end
351 351
352 352 def test_create_without_project_should_deny_without_permission
353 353 @request.session[:user_id] = 2
354 354 Project.find(3).disable_module!(:time_tracking)
355 355
356 356 assert_no_difference 'TimeEntry.count' do
357 357 post :create, :time_entry => {:project_id => '3',
358 358 :activity_id => '11',
359 359 :issue_id => '',
360 360 :spent_on => '2008-03-14',
361 361 :hours => '7.3'}
362 362 end
363 363
364 364 assert_response 403
365 365 end
366 366
367 367 def test_create_without_project_with_failure
368 368 @request.session[:user_id] = 2
369 369 assert_no_difference 'TimeEntry.count' do
370 370 post :create, :time_entry => {:project_id => '1',
371 371 :activity_id => '11',
372 372 :issue_id => '',
373 373 :spent_on => '2008-03-14',
374 374 :hours => ''}
375 375 end
376 376
377 377 assert_response :success
378 378 assert_select 'select[name=?]', 'time_entry[project_id]' do
379 379 assert_select 'option[value="1"][selected=selected]'
380 380 end
381 381 end
382 382
383 383 def test_update
384 384 entry = TimeEntry.find(1)
385 385 assert_equal 1, entry.issue_id
386 386 assert_equal 2, entry.user_id
387 387
388 388 @request.session[:user_id] = 1
389 389 put :update, :id => 1,
390 390 :time_entry => {:issue_id => '2',
391 391 :hours => '8'}
392 392 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
393 393 entry.reload
394 394
395 395 assert_equal 8, entry.hours
396 396 assert_equal 2, entry.issue_id
397 397 assert_equal 2, entry.user_id
398 398 end
399 399
400 400 def test_update_should_allow_to_change_issue_to_another_project
401 401 entry = TimeEntry.generate!(:issue_id => 1)
402 402
403 403 @request.session[:user_id] = 1
404 404 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
405 405 assert_response 302
406 406 entry.reload
407 407
408 408 assert_equal 5, entry.issue_id
409 409 assert_equal 3, entry.project_id
410 410 end
411 411
412 412 def test_update_should_not_allow_to_change_issue_to_an_invalid_project
413 413 entry = TimeEntry.generate!(:issue_id => 1)
414 414 Project.find(3).disable_module!(:time_tracking)
415 415
416 416 @request.session[:user_id] = 1
417 417 put :update, :id => entry.id, :time_entry => {:issue_id => '5'}
418 418 assert_response 200
419 419 assert_include "Issue is invalid", assigns(:time_entry).errors.full_messages
420 420 end
421 421
422 422 def test_get_bulk_edit
423 423 @request.session[:user_id] = 2
424 424 get :bulk_edit, :ids => [1, 2]
425 425 assert_response :success
426 426 assert_template 'bulk_edit'
427 427
428 428 assert_select 'ul#bulk-selection' do
429 429 assert_select 'li', 2
430 430 assert_select 'li a', :text => '03/23/2007 - eCookbook: 4.25 hours'
431 431 end
432 432
433 433 assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do
434 434 # System wide custom field
435 435 assert_select 'select[name=?]', 'time_entry[custom_field_values][10]'
436 436
437 437 # Activities
438 438 assert_select 'select[name=?]', 'time_entry[activity_id]' do
439 439 assert_select 'option[value=""]', :text => '(No change)'
440 440 assert_select 'option[value="9"]', :text => 'Design'
441 441 end
442 442 end
443 443 end
444 444
445 445 def test_get_bulk_edit_on_different_projects
446 446 @request.session[:user_id] = 2
447 447 get :bulk_edit, :ids => [1, 2, 6]
448 448 assert_response :success
449 449 assert_template 'bulk_edit'
450 450 end
451 451
452 452 def test_bulk_edit_with_edit_own_time_entries_permission
453 453 @request.session[:user_id] = 2
454 454 Role.find_by_name('Manager').remove_permission! :edit_time_entries
455 455 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
456 456 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
457 457
458 458 get :bulk_edit, :ids => ids
459 459 assert_response :success
460 460 end
461 461
462 462 def test_bulk_update
463 463 @request.session[:user_id] = 2
464 464 # update time entry activity
465 465 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9}
466 466
467 467 assert_response 302
468 468 # check that the issues were updated
469 469 assert_equal [9, 9], TimeEntry.where(:id => [1, 2]).collect {|i| i.activity_id}
470 470 end
471 471
472 472 def test_bulk_update_with_failure
473 473 @request.session[:user_id] = 2
474 474 post :bulk_update, :ids => [1, 2], :time_entry => { :hours => 'A'}
475 475
476 476 assert_response 302
477 477 assert_match /Failed to save 2 time entrie/, flash[:error]
478 478 end
479 479
480 480 def test_bulk_update_on_different_projects
481 481 @request.session[:user_id] = 2
482 482 # makes user a manager on the other project
483 483 Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1])
484 484
485 485 # update time entry activity
486 486 post :bulk_update, :ids => [1, 2, 4], :time_entry => { :activity_id => 9 }
487 487
488 488 assert_response 302
489 489 # check that the issues were updated
490 490 assert_equal [9, 9, 9], TimeEntry.where(:id => [1, 2, 4]).collect {|i| i.activity_id}
491 491 end
492 492
493 493 def test_bulk_update_on_different_projects_without_rights
494 494 @request.session[:user_id] = 3
495 495 user = User.find(3)
496 496 action = { :controller => "timelog", :action => "bulk_update" }
497 497 assert user.allowed_to?(action, TimeEntry.find(1).project)
498 498 assert ! user.allowed_to?(action, TimeEntry.find(5).project)
499 499 post :bulk_update, :ids => [1, 5], :time_entry => { :activity_id => 9 }
500 500 assert_response 403
501 501 end
502 502
503 503 def test_bulk_update_with_edit_own_time_entries_permission
504 504 @request.session[:user_id] = 2
505 505 Role.find_by_name('Manager').remove_permission! :edit_time_entries
506 506 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
507 507 ids = (0..1).map {TimeEntry.generate!(:user => User.find(2)).id}
508 508
509 509 post :bulk_update, :ids => ids, :time_entry => { :activity_id => 9 }
510 510 assert_response 302
511 511 end
512 512
513 513 def test_bulk_update_with_edit_own_time_entries_permissions_should_be_denied_for_time_entries_of_other_user
514 514 @request.session[:user_id] = 2
515 515 Role.find_by_name('Manager').remove_permission! :edit_time_entries
516 516 Role.find_by_name('Manager').add_permission! :edit_own_time_entries
517 517
518 518 post :bulk_update, :ids => [1, 2], :time_entry => { :activity_id => 9 }
519 519 assert_response 403
520 520 end
521 521
522 522 def test_bulk_update_custom_field
523 523 @request.session[:user_id] = 2
524 524 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {'10' => '0'} }
525 525
526 526 assert_response 302
527 527 assert_equal ["0", "0"], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(10).value}
528 528 end
529 529
530 530 def test_bulk_update_clear_custom_field
531 531 field = TimeEntryCustomField.generate!(:field_format => 'string')
532 532 @request.session[:user_id] = 2
533 533 post :bulk_update, :ids => [1, 2], :time_entry => { :custom_field_values => {field.id.to_s => '__none__'} }
534 534
535 535 assert_response 302
536 536 assert_equal ["", ""], TimeEntry.where(:id => [1, 2]).collect {|i| i.custom_value_for(field).value}
537 537 end
538 538
539 539 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
540 540 @request.session[:user_id] = 2
541 541 post :bulk_update, :ids => [1,2], :back_url => '/time_entries'
542 542
543 543 assert_response :redirect
544 544 assert_redirected_to '/time_entries'
545 545 end
546 546
547 547 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
548 548 @request.session[:user_id] = 2
549 549 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
550 550
551 551 assert_response :redirect
552 552 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => Project.find(1).identifier
553 553 end
554 554
555 555 def test_post_bulk_update_without_edit_permission_should_be_denied
556 556 @request.session[:user_id] = 2
557 557 Role.find_by_name('Manager').remove_permission! :edit_time_entries
558 558 post :bulk_update, :ids => [1,2]
559 559
560 560 assert_response 403
561 561 end
562 562
563 563 def test_destroy
564 564 @request.session[:user_id] = 2
565 565 delete :destroy, :id => 1
566 566 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
567 567 assert_equal I18n.t(:notice_successful_delete), flash[:notice]
568 568 assert_nil TimeEntry.find_by_id(1)
569 569 end
570 570
571 571 def test_destroy_should_fail
572 572 # simulate that this fails (e.g. due to a plugin), see #5700
573 573 TimeEntry.any_instance.expects(:destroy).returns(false)
574 574
575 575 @request.session[:user_id] = 2
576 576 delete :destroy, :id => 1
577 577 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
578 578 assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error]
579 579 assert_not_nil TimeEntry.find_by_id(1)
580 580 end
581 581
582 def test_destroy_should_redirect_to_referer
583 referer = 'http://test.host/time_entries?utf8=βœ“&set_filter=1&&f%5B%5D=user_id&op%5Buser_id%5D=%3D&v%5Buser_id%5D%5B%5D=me'
584 @request.env["HTTP_REFERER"] = referer
585 @request.session[:user_id] = 2
586
587 delete :destroy, :params => {:id => 1}
588 assert_redirected_to referer
589 end
590
582 591 def test_index_all_projects
583 592 get :index
584 593 assert_response :success
585 594 assert_template 'index'
586 595 assert_not_nil assigns(:total_hours)
587 596 assert_equal "162.90", "%.2f" % assigns(:total_hours)
588 597 assert_select 'form#query_form[action=?]', '/time_entries'
589 598 end
590 599
591 600 def test_index_all_projects_should_show_log_time_link
592 601 @request.session[:user_id] = 2
593 602 get :index
594 603 assert_response :success
595 604 assert_template 'index'
596 605 assert_select 'a[href=?]', '/time_entries/new', :text => /Log time/
597 606 end
598 607
599 608 def test_index_my_spent_time
600 609 @request.session[:user_id] = 2
601 610 get :index, :user_id => 'me'
602 611 assert_response :success
603 612 assert_template 'index'
604 613 assert assigns(:entries).all? {|entry| entry.user_id == 2}
605 614 end
606 615
607 616 def test_index_at_project_level
608 617 get :index, :project_id => 'ecookbook'
609 618 assert_response :success
610 619 assert_template 'index'
611 620 assert_not_nil assigns(:entries)
612 621 assert_equal 4, assigns(:entries).size
613 622 # project and subproject
614 623 assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort
615 624 assert_not_nil assigns(:total_hours)
616 625 assert_equal "162.90", "%.2f" % assigns(:total_hours)
617 626 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
618 627 end
619 628
620 629 def test_index_with_display_subprojects_issues_to_false_should_not_include_subproject_entries
621 630 entry = TimeEntry.generate!(:project => Project.find(3))
622 631
623 632 with_settings :display_subprojects_issues => '0' do
624 633 get :index, :project_id => 'ecookbook'
625 634 assert_response :success
626 635 assert_template 'index'
627 636 assert_not_include entry, assigns(:entries)
628 637 end
629 638 end
630 639
631 640 def test_index_with_display_subprojects_issues_to_false_and_subproject_filter_should_include_subproject_entries
632 641 entry = TimeEntry.generate!(:project => Project.find(3))
633 642
634 643 with_settings :display_subprojects_issues => '0' do
635 644 get :index, :project_id => 'ecookbook', :subproject_id => 3
636 645 assert_response :success
637 646 assert_template 'index'
638 647 assert_include entry, assigns(:entries)
639 648 end
640 649 end
641 650
642 651 def test_index_at_project_level_with_date_range
643 652 get :index, :project_id => 'ecookbook',
644 653 :f => ['spent_on'],
645 654 :op => {'spent_on' => '><'},
646 655 :v => {'spent_on' => ['2007-03-20', '2007-04-30']}
647 656 assert_response :success
648 657 assert_template 'index'
649 658 assert_not_nil assigns(:entries)
650 659 assert_equal 3, assigns(:entries).size
651 660 assert_not_nil assigns(:total_hours)
652 661 assert_equal "12.90", "%.2f" % assigns(:total_hours)
653 662 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
654 663 end
655 664
656 665 def test_index_at_project_level_with_date_range_using_from_and_to_params
657 666 get :index, :project_id => 'ecookbook', :from => '2007-03-20', :to => '2007-04-30'
658 667 assert_response :success
659 668 assert_template 'index'
660 669 assert_not_nil assigns(:entries)
661 670 assert_equal 3, assigns(:entries).size
662 671 assert_not_nil assigns(:total_hours)
663 672 assert_equal "12.90", "%.2f" % assigns(:total_hours)
664 673 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
665 674 end
666 675
667 676 def test_index_at_project_level_with_period
668 677 get :index, :project_id => 'ecookbook',
669 678 :f => ['spent_on'],
670 679 :op => {'spent_on' => '>t-'},
671 680 :v => {'spent_on' => ['7']}
672 681 assert_response :success
673 682 assert_template 'index'
674 683 assert_not_nil assigns(:entries)
675 684 assert_not_nil assigns(:total_hours)
676 685 assert_select 'form#query_form[action=?]', '/projects/ecookbook/time_entries'
677 686 end
678 687
679 688 def test_index_at_issue_level
680 689 get :index, :issue_id => 1
681 690 assert_response :success
682 691 assert_template 'index'
683 692 assert_not_nil assigns(:entries)
684 693 assert_equal 2, assigns(:entries).size
685 694 assert_not_nil assigns(:total_hours)
686 695 assert_equal 154.25, assigns(:total_hours)
687 696 # display all time
688 697 assert_nil assigns(:from)
689 698 assert_nil assigns(:to)
690 699 assert_select 'form#query_form[action=?]', '/issues/1/time_entries'
691 700 end
692 701
693 702 def test_index_should_sort_by_spent_on_and_created_on
694 703 t1 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:00:00', :activity_id => 10)
695 704 t2 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-16', :created_on => '2012-06-16 20:05:00', :activity_id => 10)
696 705 t3 = TimeEntry.create!(:user => User.find(1), :project => Project.find(1), :hours => 1, :spent_on => '2012-06-15', :created_on => '2012-06-16 20:10:00', :activity_id => 10)
697 706
698 707 get :index, :project_id => 1,
699 708 :f => ['spent_on'],
700 709 :op => {'spent_on' => '><'},
701 710 :v => {'spent_on' => ['2012-06-15', '2012-06-16']}
702 711 assert_response :success
703 712 assert_equal [t2, t1, t3], assigns(:entries)
704 713
705 714 get :index, :project_id => 1,
706 715 :f => ['spent_on'],
707 716 :op => {'spent_on' => '><'},
708 717 :v => {'spent_on' => ['2012-06-15', '2012-06-16']},
709 718 :sort => 'spent_on'
710 719 assert_response :success
711 720 assert_equal [t3, t1, t2], assigns(:entries)
712 721 end
713 722
714 723 def test_index_with_filter_on_issue_custom_field
715 724 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
716 725 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
717 726
718 727 get :index, :f => ['issue.cf_2'], :op => {'issue.cf_2' => '='}, :v => {'issue.cf_2' => ['filter_on_issue_custom_field']}
719 728 assert_response :success
720 729 assert_equal [entry], assigns(:entries)
721 730 end
722 731
723 732 def test_index_with_issue_custom_field_column
724 733 issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {2 => 'filter_on_issue_custom_field'})
725 734 entry = TimeEntry.generate!(:issue => issue, :hours => 2.5)
726 735
727 736 get :index, :c => %w(project spent_on issue comments hours issue.cf_2)
728 737 assert_response :success
729 738 assert_include :'issue.cf_2', assigns(:query).column_names
730 739 assert_select 'td.issue_cf_2', :text => 'filter_on_issue_custom_field'
731 740 end
732 741
733 742 def test_index_with_time_entry_custom_field_column
734 743 field = TimeEntryCustomField.generate!(:field_format => 'string')
735 744 entry = TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value'})
736 745 field_name = "cf_#{field.id}"
737 746
738 747 get :index, :c => ["hours", field_name]
739 748 assert_response :success
740 749 assert_include field_name.to_sym, assigns(:query).column_names
741 750 assert_select "td.#{field_name}", :text => 'CF Value'
742 751 end
743 752
744 753 def test_index_with_time_entry_custom_field_sorting
745 754 field = TimeEntryCustomField.generate!(:field_format => 'string', :name => 'String Field')
746 755 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 1'})
747 756 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 3'})
748 757 TimeEntry.generate!(:hours => 2.5, :custom_field_values => {field.id => 'CF Value 2'})
749 758 field_name = "cf_#{field.id}"
750 759
751 760 get :index, :c => ["hours", field_name], :sort => field_name
752 761 assert_response :success
753 762 assert_include field_name.to_sym, assigns(:query).column_names
754 763 assert_select "th a.sort", :text => 'String Field'
755 764
756 765 # Make sure that values are properly sorted
757 766 values = assigns(:entries).map {|e| e.custom_field_value(field)}.compact
758 767 assert_equal 3, values.size
759 768 assert_equal values.sort, values
760 769 end
761 770
762 771 def test_index_atom_feed
763 772 get :index, :project_id => 1, :format => 'atom'
764 773 assert_response :success
765 774 assert_equal 'application/atom+xml', @response.content_type
766 775 assert_not_nil assigns(:items)
767 776 assert assigns(:items).first.is_a?(TimeEntry)
768 777 end
769 778
770 779 def test_index_at_project_level_should_include_csv_export_dialog
771 780 get :index, :project_id => 'ecookbook',
772 781 :f => ['spent_on'],
773 782 :op => {'spent_on' => '>='},
774 783 :v => {'spent_on' => ['2007-04-01']},
775 784 :c => ['spent_on', 'user']
776 785 assert_response :success
777 786
778 787 assert_select '#csv-export-options' do
779 788 assert_select 'form[action=?][method=get]', '/projects/ecookbook/time_entries.csv' do
780 789 # filter
781 790 assert_select 'input[name=?][value=?]', 'f[]', 'spent_on'
782 791 assert_select 'input[name=?][value=?]', 'op[spent_on]', '>='
783 792 assert_select 'input[name=?][value=?]', 'v[spent_on][]', '2007-04-01'
784 793 # columns
785 794 assert_select 'input[name=?][value=?]', 'c[]', 'spent_on'
786 795 assert_select 'input[name=?][value=?]', 'c[]', 'user'
787 796 assert_select 'input[name=?]', 'c[]', 2
788 797 end
789 798 end
790 799 end
791 800
792 801 def test_index_cross_project_should_include_csv_export_dialog
793 802 get :index
794 803 assert_response :success
795 804
796 805 assert_select '#csv-export-options' do
797 806 assert_select 'form[action=?][method=get]', '/time_entries.csv'
798 807 end
799 808 end
800 809
801 810 def test_index_at_issue_level_should_include_csv_export_dialog
802 811 get :index, :issue_id => 3
803 812 assert_response :success
804 813
805 814 assert_select '#csv-export-options' do
806 815 assert_select 'form[action=?][method=get]', '/issues/3/time_entries.csv'
807 816 end
808 817 end
809 818
810 819 def test_index_csv_all_projects
811 820 with_settings :date_format => '%m/%d/%Y' do
812 821 get :index, :format => 'csv'
813 822 assert_response :success
814 823 assert_equal 'text/csv; header=present', response.content_type
815 824 end
816 825 end
817 826
818 827 def test_index_csv
819 828 with_settings :date_format => '%m/%d/%Y' do
820 829 get :index, :project_id => 1, :format => 'csv'
821 830 assert_response :success
822 831 assert_equal 'text/csv; header=present', response.content_type
823 832 end
824 833 end
825 834
826 835 def test_index_csv_should_fill_issue_column_with_tracker_id_and_subject
827 836 issue = Issue.find(1)
828 837 entry = TimeEntry.generate!(:issue => issue, :comments => "Issue column content test")
829 838
830 839 get :index, :format => 'csv'
831 840 line = response.body.split("\n").detect {|l| l.include?(entry.comments)}
832 841 assert_not_nil line
833 842 assert_include "#{issue.tracker} #1: #{issue.subject}", line
834 843 end
835 844 end
General Comments 0
You need to be logged in to leave comments. Login now