Feature #24277 » 02_add_remaining_time_to_log_time_5.1.2.patch
app/models/time_entry.rb | ||
---|---|---|
27 | 27 |
belongs_to :author, :class_name => 'User' |
28 | 28 |
belongs_to :activity, :class_name => 'TimeEntryActivity' |
29 | 29 | |
30 |
attr_accessor :remaining_time_action, :remaining_time_hours |
|
31 | ||
30 | 32 |
acts_as_customizable |
31 | 33 |
acts_as_event( |
32 | 34 |
:title => |
... | ... | |
51 | 53 |
validates_presence_of :issue_id, :if => lambda {Setting.timelog_required_fields.include?('issue_id')} |
52 | 54 |
validates_presence_of :comments, :if => lambda {Setting.timelog_required_fields.include?('comments')} |
53 | 55 |
validates_numericality_of :hours, :allow_nil => true, :message => :invalid |
56 |
validates :remaining_time_action, :inclusion => { :in => ['auto', 'set', 'nothing'] }, :allow_nil => true |
|
57 |
validates :remaining_time_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_blank => true, :message => :invalid} |
|
54 | 58 |
validates_length_of :comments, :maximum => 1024, :allow_nil => true |
55 | 59 |
validates :spent_on, :date => true |
56 | 60 |
before_validation :set_project_if_nil |
... | ... | |
58 | 62 |
before_validation :set_author_if_nil |
59 | 63 |
validate :validate_time_entry |
60 | 64 | |
65 |
after_save :update_issue_remaining_hours |
|
66 | ||
61 | 67 |
scope :visible, (lambda do |*args| |
62 | 68 |
joins(:project). |
63 | 69 |
where(TimeEntry.visible_condition(args.shift || User.current, *args)) |
... | ... | |
76 | 82 | |
77 | 83 |
safe_attributes 'user_id', 'hours', 'comments', 'project_id', |
78 | 84 |
'issue_id', 'activity_id', 'spent_on', |
79 |
'custom_field_values', 'custom_fields' |
|
85 |
'custom_field_values', 'custom_fields', |
|
86 |
'remaining_time_action', 'remaining_time_hours' |
|
80 | 87 | |
81 | 88 |
# Returns a SQL conditions string used to find all time entries visible by the specified user |
82 | 89 |
def self.visible_condition(user, options={}) |
... | ... | |
179 | 186 |
end |
180 | 187 |
errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project) || @invalid_issue_id |
181 | 188 |
errors.add :activity_id, :inclusion if activity_id_changed? && project && !project.activities.include?(activity) |
189 |
errors.add :remaining_time_hours, :invalid if (remaining_time_action == 'set' && remaining_time_hours.blank?) |
|
182 | 190 |
if spent_on && spent_on_changed? && user |
183 | 191 |
errors.add :base, I18n.t(:error_spent_on_future_date) if !Setting.timelog_accept_future_dates? && (spent_on > user.today) |
184 | 192 |
end |
... | ... | |
223 | 231 |
editable_custom_field_values(user).map(&:custom_field).uniq |
224 | 232 |
end |
225 | 233 | |
234 |
def remaining_time_action |
|
235 |
@remaining_time_action |
|
236 |
end |
|
237 | ||
238 |
def remaining_time_hours |
|
239 |
@remaining_time_hours |
|
240 |
end |
|
241 | ||
242 |
def update_issue_remaining_hours |
|
243 |
issue = self.issue |
|
244 |
return unless issue |
|
245 | ||
246 |
h = (remaining_time_hours.is_a?(String)) ? remaining_time_hours.to_hours : remaining_time_hours |
|
247 | ||
248 |
case remaining_time_action |
|
249 |
when "auto" |
|
250 |
new_remaining_hours = issue.remaining_hours - self.hours unless issue.remaining_hours.nil? |
|
251 |
when "set" |
|
252 |
new_remaining_hours = h |
|
253 |
when "nothing" |
|
254 |
return |
|
255 |
end |
|
256 | ||
257 |
issue.init_journal(User.current) |
|
258 |
issue.remaining_hours = new_remaining_hours |
|
259 |
issue.save |
|
260 |
end |
|
261 | ||
226 | 262 |
def visible_custom_field_values(user = nil) |
227 | 263 |
user ||= User.current |
228 | 264 |
custom_field_values.select do |value| |
app/views/issues/_edit.html.erb | ||
---|---|---|
24 | 24 |
<% @time_entry.editable_custom_field_values.each do |value| %> |
25 | 25 |
<p><%= custom_field_tag_with_label :time_entry, value %></p> |
26 | 26 |
<% end %> |
27 |
<p id="issue_remaining_time"> |
|
28 |
<%= render :partial => 'timelog/remaining_time', :locals => { :issue => @issue } %> |
|
29 |
</p> |
|
27 | 30 |
<% end %> |
28 | 31 |
</fieldset> |
29 | 32 |
<% end %> |
app/views/timelog/_form.html.erb | ||
---|---|---|
35 | 35 |
<%= wikitoolbar_for "time_entry_custom_field_values_#{value.custom_field_id}", preview_issue_path(:project_id => @project) %> |
36 | 36 |
<% end %> |
37 | 37 |
<% end %> |
38 |
<p id="issue_remaining_time"> |
|
39 |
<%= render :partial => 'remaining_time', :locals => { :issue => @time_entry.issue } %> |
|
40 |
</p> |
|
38 | 41 |
<%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %> |
39 | 42 |
</div> |
40 | 43 |
app/views/timelog/_remaining_time.html.erb | ||
---|---|---|
1 |
<% return if (!issue) || (!issue.safe_attribute? 'remaining_hours') %> |
|
2 | ||
3 |
<label for=""><%= l(:label_issue_remaining_hours) %></label> |
|
4 |
<span class="check_box_group"> |
|
5 |
<% if @time_entry.new_record? %> |
|
6 |
<label> |
|
7 |
<%= radio_button 'time_entry', 'remaining_time_action', 'auto' %> <%= l(:label_remaining_time_action_auto) %> |
|
8 |
</label> |
|
9 |
<% end %> |
|
10 |
<label class="hours"> |
|
11 |
<%= radio_button 'time_entry', 'remaining_time_action', "set" %> <%= l(:label_remaining_time_action_set) %> |
|
12 |
</label> |
|
13 |
<%= text_field 'time_entry', "remaining_time_hours", :size => "3", :disabled => true %> <%= l(:field_hours) %> |
|
14 |
<label> |
|
15 |
<%= radio_button 'time_entry', 'remaining_time_action', 'nothing' %> <%= l(:label_remaining_time_action_nothing) %> |
|
16 |
</label> |
|
17 |
</span> |
|
18 | ||
19 |
<%= javascript_tag do %> |
|
20 |
$(document).ready(function(){ |
|
21 |
var block = $("p#issue_remaining_time"); |
|
22 | ||
23 |
if (block.find('#time_entry_remaining_time_action_set').is(':checked')) { |
|
24 |
block.find('input#time_entry_remaining_time_hours').prop("disabled", false) |
|
25 |
} |
|
26 | ||
27 |
block.find("input[type=radio]").change(function(e){ |
|
28 |
block.find('input#time_entry_remaining_time_hours').prop("disabled", true) |
|
29 |
if ($(e.target).val() === 'set') { |
|
30 |
block.find('input#time_entry_remaining_time_hours').prop("disabled", false).focus() |
|
31 |
} |
|
32 |
}) |
|
33 |
}); |
|
34 | ||
35 |
<% unless params[:time_entry].present? %> |
|
36 |
<% if @time_entry.new_record? %> |
|
37 |
$('#time_entry_remaining_time_action_auto').prop('checked', true); |
|
38 |
<% else %> |
|
39 |
$('#time_entry_remaining_time_action_nothing').prop('checked', true); |
|
40 |
<% end %> |
|
41 |
<% end %> |
|
42 |
<% end %> |
app/views/timelog/new.js.erb | ||
---|---|---|
1 |
$('#time_entry_activity_id').html('<%= escape_javascript options_for_select(activity_collection_for_select_options(@time_entry), default_activity(@time_entry)) %>');
|
|
1 |
$('#new_time_entry p#issue_remaining_time').html('<%= escape_javascript render :partial => "remaining_time", :locals => { :issue => @time_entry.issue } %>');
|
|
2 | 2 |
$('#time_entry_issue').html('<%= escape_javascript link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %>'); |
config/locales/en.yml | ||
---|---|---|
1141 | 1141 |
label_default_query: Default query |
1142 | 1142 |
label_edited: Edited |
1143 | 1143 |
label_time_by_author: "%{time} by %{author}" |
1144 |
label_issue_remaining_hours: Issue remaining time |
|
1145 |
label_remaining_time_action_auto: Adjust automatically |
|
1146 |
label_remaining_time_action_set: Set to |
|
1147 |
label_remaining_time_action_nothing: Do not update remaining time |
|
1144 | 1148 | |
1145 | 1149 |
button_login: Login |
1146 | 1150 |
button_submit: Submit |
public/stylesheets/application.css | ||
---|---|---|
904 | 904 |
width:auto; |
905 | 905 |
} |
906 | 906 |
input#time_entry_comments { width: 90%;} |
907 |
p#issue_remaining_time label.hours { display: inline-block; } |
|
907 | 908 |
input#months { width: 46px; } |
908 | 909 | |
909 | 910 |
.jstBlock .jstTabs, .jstBlock .wiki-preview { width: 99%; } |
test/functional/timelog_controller_test.rb | ||
---|---|---|
69 | 69 |
assert_select 'input[name=?][type=hidden]', 'issue_id' |
70 | 70 |
assert_select 'a[href=?]', '/issues/2', :text => /Feature request #2/ |
71 | 71 |
assert_select 'select[name=?]', 'time_entry[project_id]', 0 |
72 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 3 |
|
73 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 1 |
|
72 | 74 |
end |
73 | 75 | |
74 | 76 |
def test_new_without_project_should_prefill_the_form |
... | ... | |
152 | 154 |
assert_select 'select[name=?]', 'time_entry[user_id]', 0 |
153 | 155 |
end |
154 | 156 | |
157 |
def test_new_on_issue_with_remaining_time_disabled_should_not_show_the_update_issue_remaining_time_section |
|
158 |
tracker = Tracker.find(2) |
|
159 |
tracker.core_fields = tracker.core_fields - %w(remaining_hours) |
|
160 |
tracker.save! |
|
161 | ||
162 |
@request.session[:user_id] = 3 |
|
163 |
get :new, :issue_id => 2 |
|
164 |
assert_response :success |
|
165 |
assert_template 'new' |
|
166 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 0 |
|
167 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 0 |
|
168 |
end |
|
169 | ||
155 | 170 |
def test_post_new_as_js_should_update_activity_options |
156 | 171 |
@request.session[:user_id] = 3 |
157 | 172 |
post :new, :params => {:time_entry => {:project_id => 1}, :format => 'js'} |
... | ... | |
172 | 187 |
assert_select 'a.user.active', :text => 'Redmine Admin' |
173 | 188 |
end |
174 | 189 | |
190 |
def test_get_should_not_show_the_adjust_automatically_option_in_issue_remaining_time_section |
|
191 |
@request.session[:user_id] = 2 |
|
192 |
get :edit, :id => 2, :project_id => nil |
|
193 |
assert_response :success |
|
194 |
assert_template 'edit' |
|
195 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 2 |
|
196 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 1 |
|
197 |
assert_select 'input[type=radio][id=?]', 'time_entry_remaining_time_action_auto]', 0 |
|
198 |
end |
|
199 | ||
175 | 200 |
def test_get_edit_with_an_existing_time_entry_with_inactive_activity |
176 | 201 |
te = TimeEntry.find(1) |
177 | 202 |
te.activity = TimeEntryActivity.find_by_name("Inactive Activity") |
test/integration/api_test/issues_test.rb | ||
---|---|---|
461 | 461 |
parent.update_columns :estimated_hours => 2.0 |
462 | 462 |
child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) |
463 | 463 |
TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, |
464 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) |
|
464 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id, :remaining_time_action => 'nothing')
|
|
465 | 465 |
get '/issues/3.xml' |
466 | 466 | |
467 | 467 |
assert_equal 'application/xml', response.media_type |
... | ... | |
515 | 515 |
parent.update_columns :estimated_hours => 2.0 |
516 | 516 |
child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) |
517 | 517 |
TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, |
518 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) |
|
518 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id, :remaining_time_action => 'nothing')
|
|
519 | 519 |
get '/issues/3.json' |
520 | 520 | |
521 | 521 |
assert_equal 'application/json', response.media_type |
test/unit/time_entry_test.rb | ||
---|---|---|
297 | 297 | |
298 | 298 |
assert_equal [2], time_entry.assignable_users.map(&:id) |
299 | 299 |
end |
300 | ||
301 |
def test_time_entry_decrease_issue_remaining_time_with_logged_time |
|
302 |
issue = Issue.find(1) |
|
303 |
issue.update_attribute(:remaining_hours, 3) |
|
304 | ||
305 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
306 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
307 |
:remaining_time_action => "auto") |
|
308 | ||
309 |
assert te.save |
|
310 |
assert_equal 2, issue.remaining_hours |
|
311 |
end |
|
312 | ||
313 |
def test_time_entry_set_issue_remaining_time |
|
314 |
issue = Issue.find(1) |
|
315 |
issue.update_attribute(:remaining_hours, 3) |
|
316 | ||
317 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
318 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
319 |
:remaining_time_action => "set", :remaining_time_hours => 4) |
|
320 | ||
321 |
assert te.save |
|
322 |
assert_equal 4, issue.remaining_hours |
|
323 |
end |
|
324 | ||
325 |
def test_time_entry_do_not_update_issue_remaining_time |
|
326 |
issue = Issue.find(1) |
|
327 |
issue.update_attribute(:remaining_hours, 3) |
|
328 | ||
329 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
330 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
331 |
:remaining_time_action => "nothing", :remaining_time_hours => 4) |
|
332 |
assert te.save |
|
333 |
assert_equal 3, issue.remaining_hours |
|
334 |
end |
|
335 | ||
336 |
def test_time_entry_do_not_decrease_issue_remaining_time_when_issue_remaining_time_is_nil |
|
337 |
issue = Issue.find(1) |
|
338 |
issue.update_attribute(:remaining_hours, '') |
|
339 | ||
340 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
341 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
342 |
:remaining_time_action => "auto", :remaining_time_hours => 4) |
|
343 |
assert te.save |
|
344 |
assert_nil issue.remaining_hours |
|
345 |
end |
|
346 | ||
347 |
def test_validate_time_entry_remaining_time_action |
|
348 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => Issue.find(1), |
|
349 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
350 |
:remaining_time_action => "test", :remaining_time_hours => 'one') |
|
351 |
assert !te.valid? |
|
352 |
assert_equal 2, te.errors.count |
|
353 |
end |
|
300 | 354 |
end |