##// END OF EJS Templates
Merged r15955 and r15956 (#24297)....
Jean-Philippe Lang -
r15618:35dff9428f72
parent child
Show More
@@ -1,290 +1,294
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 before_filter :check_editability, :only => [:edit, :update]
22 23 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 24 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
24 25
25 26 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
26 27 before_filter :authorize_global, :only => [:new, :create, :index, :report]
27 28
28 29 accept_rss_auth :index
29 30 accept_api_auth :index, :show, :create, :update, :destroy
30 31
31 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 33
33 34 helper :sort
34 35 include SortHelper
35 36 helper :issues
36 37 include TimelogHelper
37 38 helper :custom_fields
38 39 include CustomFieldsHelper
39 40 helper :queries
40 41 include QueriesHelper
41 42
42 43 def index
43 44 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
44 45
45 46 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
46 47 sort_update(@query.sortable_columns)
47 48 scope = time_entry_scope(:order => sort_clause).
48 49 includes(:project, :user, :issue).
49 50 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
50 51
51 52 respond_to do |format|
52 53 format.html {
53 54 @entry_count = scope.count
54 55 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
55 56 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
56 57 @total_hours = scope.sum(:hours).to_f
57 58
58 59 render :layout => !request.xhr?
59 60 }
60 61 format.api {
61 62 @entry_count = scope.count
62 63 @offset, @limit = api_offset_and_limit
63 64 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
64 65 }
65 66 format.atom {
66 67 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
67 68 render_feed(entries, :title => l(:label_spent_time))
68 69 }
69 70 format.csv {
70 71 # Export all entries
71 72 @entries = scope.to_a
72 73 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
73 74 }
74 75 end
75 76 end
76 77
77 78 def report
78 79 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
79 80 scope = time_entry_scope
80 81
81 82 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
82 83
83 84 respond_to do |format|
84 85 format.html { render :layout => !request.xhr? }
85 86 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
86 87 end
87 88 end
88 89
89 90 def show
90 91 respond_to do |format|
91 92 # TODO: Implement html response
92 93 format.html { render :nothing => true, :status => 406 }
93 94 format.api
94 95 end
95 96 end
96 97
97 98 def new
98 99 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
99 100 @time_entry.safe_attributes = params[:time_entry]
100 101 end
101 102
102 103 def create
103 104 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
104 105 @time_entry.safe_attributes = params[:time_entry]
105 106 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
106 107 render_403
107 108 return
108 109 end
109 110
110 111 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
111 112
112 113 if @time_entry.save
113 114 respond_to do |format|
114 115 format.html {
115 116 flash[:notice] = l(:notice_successful_create)
116 117 if params[:continue]
117 118 options = {
118 119 :time_entry => {
119 120 :project_id => params[:time_entry][:project_id],
120 121 :issue_id => @time_entry.issue_id,
121 122 :activity_id => @time_entry.activity_id
122 123 },
123 124 :back_url => params[:back_url]
124 125 }
125 126 if params[:project_id] && @time_entry.project
126 127 redirect_to new_project_time_entry_path(@time_entry.project, options)
127 128 elsif params[:issue_id] && @time_entry.issue
128 129 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
129 130 else
130 131 redirect_to new_time_entry_path(options)
131 132 end
132 133 else
133 134 redirect_back_or_default project_time_entries_path(@time_entry.project)
134 135 end
135 136 }
136 137 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
137 138 end
138 139 else
139 140 respond_to do |format|
140 141 format.html { render :action => 'new' }
141 142 format.api { render_validation_errors(@time_entry) }
142 143 end
143 144 end
144 145 end
145 146
146 147 def edit
147 148 @time_entry.safe_attributes = params[:time_entry]
148 149 end
149 150
150 151 def update
151 152 @time_entry.safe_attributes = params[:time_entry]
152 153
153 154 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
154 155
155 156 if @time_entry.save
156 157 respond_to do |format|
157 158 format.html {
158 159 flash[:notice] = l(:notice_successful_update)
159 160 redirect_back_or_default project_time_entries_path(@time_entry.project)
160 161 }
161 162 format.api { render_api_ok }
162 163 end
163 164 else
164 165 respond_to do |format|
165 166 format.html { render :action => 'edit' }
166 167 format.api { render_validation_errors(@time_entry) }
167 168 end
168 169 end
169 170 end
170 171
171 172 def bulk_edit
172 173 @available_activities = TimeEntryActivity.shared.active
173 174 @custom_fields = TimeEntry.first.available_custom_fields
174 175 end
175 176
176 177 def bulk_update
177 178 attributes = parse_params_for_bulk_time_entry_attributes(params)
178 179
179 180 unsaved_time_entry_ids = []
180 181 @time_entries.each do |time_entry|
181 182 time_entry.reload
182 183 time_entry.safe_attributes = attributes
183 184 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
184 185 unless time_entry.save
185 186 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
186 187 # Keep unsaved time_entry ids to display them in flash error
187 188 unsaved_time_entry_ids << time_entry.id
188 189 end
189 190 end
190 191 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
191 192 redirect_back_or_default project_time_entries_path(@projects.first)
192 193 end
193 194
194 195 def destroy
195 196 destroyed = TimeEntry.transaction do
196 197 @time_entries.each do |t|
197 198 unless t.destroy && t.destroyed?
198 199 raise ActiveRecord::Rollback
199 200 end
200 201 end
201 202 end
202 203
203 204 respond_to do |format|
204 205 format.html {
205 206 if destroyed
206 207 flash[:notice] = l(:notice_successful_delete)
207 208 else
208 209 flash[:error] = l(:notice_unable_delete_time_entry)
209 210 end
210 211 redirect_back_or_default project_time_entries_path(@projects.first)
211 212 }
212 213 format.api {
213 214 if destroyed
214 215 render_api_ok
215 216 else
216 217 render_validation_errors(@time_entries)
217 218 end
218 219 }
219 220 end
220 221 end
221 222
222 223 private
223 224 def find_time_entry
224 225 @time_entry = TimeEntry.find(params[:id])
226 @project = @time_entry.project
227 rescue ActiveRecord::RecordNotFound
228 render_404
229 end
230
231 def check_editability
225 232 unless @time_entry.editable_by?(User.current)
226 233 render_403
227 234 return false
228 235 end
229 @project = @time_entry.project
230 rescue ActiveRecord::RecordNotFound
231 render_404
232 236 end
233 237
234 238 def find_time_entries
235 239 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
236 240 raise ActiveRecord::RecordNotFound if @time_entries.empty?
237 241 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
238 242 @projects = @time_entries.collect(&:project).compact.uniq
239 243 @project = @projects.first if @projects.size == 1
240 244 rescue ActiveRecord::RecordNotFound
241 245 render_404
242 246 end
243 247
244 248 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
245 249 if unsaved_time_entry_ids.empty?
246 250 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
247 251 else
248 252 flash[:error] = l(:notice_failed_to_save_time_entries,
249 253 :count => unsaved_time_entry_ids.size,
250 254 :total => time_entries.size,
251 255 :ids => '#' + unsaved_time_entry_ids.join(', #'))
252 256 end
253 257 end
254 258
255 259 def find_optional_project
256 260 if params[:issue_id].present?
257 261 @issue = Issue.find(params[:issue_id])
258 262 @project = @issue.project
259 263 elsif params[:project_id].present?
260 264 @project = Project.find(params[:project_id])
261 265 end
262 266 rescue ActiveRecord::RecordNotFound
263 267 render_404
264 268 end
265 269
266 270 # Returns the TimeEntry scope for index and report actions
267 271 def time_entry_scope(options={})
268 272 scope = @query.results_scope(options)
269 273 if @issue
270 274 scope = scope.on_issue(@issue)
271 275 end
272 276 scope
273 277 end
274 278
275 279 def parse_params_for_bulk_time_entry_attributes(params)
276 280 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
277 281 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
278 282 if custom = attributes[:custom_field_values]
279 283 custom.reject! {|k,v| v.blank?}
280 284 custom.keys.each do |k|
281 285 if custom[k].is_a?(Array)
282 286 custom[k] << '' if custom[k].delete('__none__')
283 287 else
284 288 custom[k] = '' if custom[k] == '__none__'
285 289 end
286 290 end
287 291 end
288 292 attributes
289 293 end
290 294 end
@@ -1,135 +1,146
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 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class Redmine::ApiTest::TimeEntriesTest < Redmine::ApiTest::Base
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :enumerations, :users, :issue_categories,
23 23 :projects_trackers,
24 24 :roles,
25 25 :member_roles,
26 26 :members,
27 27 :enabled_modules,
28 28 :time_entries
29 29
30 30 test "GET /time_entries.xml should return time entries" do
31 31 get '/time_entries.xml', {}, credentials('jsmith')
32 32 assert_response :success
33 33 assert_equal 'application/xml', @response.content_type
34 34 assert_select 'time_entries[type=array] time_entry id', :text => '2'
35 35 end
36 36
37 37 test "GET /time_entries.xml with limit should return limited results" do
38 38 get '/time_entries.xml?limit=2', {}, credentials('jsmith')
39 39 assert_response :success
40 40 assert_equal 'application/xml', @response.content_type
41 41 assert_select 'time_entries[type=array] time_entry', 2
42 42 end
43 43
44 44 test "GET /time_entries/:id.xml should return the time entry" do
45 45 get '/time_entries/2.xml', {}, credentials('jsmith')
46 46 assert_response :success
47 47 assert_equal 'application/xml', @response.content_type
48 48 assert_select 'time_entry id', :text => '2'
49 49 end
50 50
51 test "GET /time_entries/:id.xml on closed project should return the time entry" do
52 project = TimeEntry.find(2).project
53 project.close
54 project.save!
55
56 get '/time_entries/2.xml', {}, credentials('jsmith')
57 assert_response :success
58 assert_equal 'application/xml', @response.content_type
59 assert_select 'time_entry id', :text => '2'
60 end
61
51 62 test "POST /time_entries.xml with issue_id should create time entry" do
52 63 assert_difference 'TimeEntry.count' do
53 64 post '/time_entries.xml', {:time_entry => {:issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
54 65 end
55 66 assert_response :created
56 67 assert_equal 'application/xml', @response.content_type
57 68
58 69 entry = TimeEntry.order('id DESC').first
59 70 assert_equal 'jsmith', entry.user.login
60 71 assert_equal Issue.find(1), entry.issue
61 72 assert_equal Project.find(1), entry.project
62 73 assert_equal Date.parse('2010-12-02'), entry.spent_on
63 74 assert_equal 3.5, entry.hours
64 75 assert_equal TimeEntryActivity.find(11), entry.activity
65 76 end
66 77
67 78 test "POST /time_entries.xml with issue_id should accept custom fields" do
68 79 field = TimeEntryCustomField.create!(:name => 'Test', :field_format => 'string')
69 80
70 81 assert_difference 'TimeEntry.count' do
71 82 post '/time_entries.xml', {:time_entry => {
72 83 :issue_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11', :custom_fields => [{:id => field.id.to_s, :value => 'accepted'}]
73 84 }}, credentials('jsmith')
74 85 end
75 86 assert_response :created
76 87 assert_equal 'application/xml', @response.content_type
77 88
78 89 entry = TimeEntry.order('id DESC').first
79 90 assert_equal 'accepted', entry.custom_field_value(field)
80 91 end
81 92
82 93 test "POST /time_entries.xml with project_id should create time entry" do
83 94 assert_difference 'TimeEntry.count' do
84 95 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :hours => '3.5', :activity_id => '11'}}, credentials('jsmith')
85 96 end
86 97 assert_response :created
87 98 assert_equal 'application/xml', @response.content_type
88 99
89 100 entry = TimeEntry.order('id DESC').first
90 101 assert_equal 'jsmith', entry.user.login
91 102 assert_nil entry.issue
92 103 assert_equal Project.find(1), entry.project
93 104 assert_equal Date.parse('2010-12-02'), entry.spent_on
94 105 assert_equal 3.5, entry.hours
95 106 assert_equal TimeEntryActivity.find(11), entry.activity
96 107 end
97 108
98 109 test "POST /time_entries.xml with invalid parameters should return errors" do
99 110 assert_no_difference 'TimeEntry.count' do
100 111 post '/time_entries.xml', {:time_entry => {:project_id => '1', :spent_on => '2010-12-02', :activity_id => '11'}}, credentials('jsmith')
101 112 end
102 113 assert_response :unprocessable_entity
103 114 assert_equal 'application/xml', @response.content_type
104 115
105 116 assert_select 'errors error', :text => "Hours cannot be blank"
106 117 end
107 118
108 119 test "PUT /time_entries/:id.xml with valid parameters should update time entry" do
109 120 assert_no_difference 'TimeEntry.count' do
110 121 put '/time_entries/2.xml', {:time_entry => {:comments => 'API Update'}}, credentials('jsmith')
111 122 end
112 123 assert_response :ok
113 124 assert_equal '', @response.body
114 125 assert_equal 'API Update', TimeEntry.find(2).comments
115 126 end
116 127
117 128 test "PUT /time_entries/:id.xml with invalid parameters should return errors" do
118 129 assert_no_difference 'TimeEntry.count' do
119 130 put '/time_entries/2.xml', {:time_entry => {:hours => '', :comments => 'API Update'}}, credentials('jsmith')
120 131 end
121 132 assert_response :unprocessable_entity
122 133 assert_equal 'application/xml', @response.content_type
123 134
124 135 assert_select 'errors error', :text => "Hours cannot be blank"
125 136 end
126 137
127 138 test "DELETE /time_entries/:id.xml should destroy time entry" do
128 139 assert_difference 'TimeEntry.count', -1 do
129 140 delete '/time_entries/2.xml', {}, credentials('jsmith')
130 141 end
131 142 assert_response :ok
132 143 assert_equal '', @response.body
133 144 assert_nil TimeEntry.find_by_id(2)
134 145 end
135 146 end
General Comments 0
You need to be logged in to leave comments. Login now