Feature #24277 » 02_add_remaining_time_to_log_time_15945.patch
app/models/time_entry.rb | ||
---|---|---|
25 | 25 |
belongs_to :activity, :class_name => 'TimeEntryActivity' |
26 | 26 | |
27 | 27 |
attr_protected :user_id, :tyear, :tmonth, :tweek |
28 |
attr_accessor :remaining_time_action, :remaining_time_hours |
|
28 | 29 | |
29 | 30 |
acts_as_customizable |
30 | 31 |
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"}, |
... | ... | |
39 | 40 | |
40 | 41 |
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on |
41 | 42 |
validates_numericality_of :hours, :allow_nil => true, :message => :invalid |
43 |
validates :remaining_time_action, :inclusion => { :in => ['auto', 'set', 'nothing'] }, :allow_nil => true |
|
44 |
validates :remaining_time_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_blank => true, :message => :invalid} |
|
45 | ||
42 | 46 |
validates_length_of :comments, :maximum => 1024, :allow_nil => true |
43 | 47 |
validates :spent_on, :date => true |
44 | 48 |
before_validation :set_project_if_nil |
45 | 49 |
validate :validate_time_entry |
46 | 50 | |
51 |
after_save :update_issue_remaining_hours |
|
52 | ||
47 | 53 |
scope :visible, lambda {|*args| |
48 | 54 |
joins(:project). |
49 | 55 |
where(TimeEntry.visible_condition(args.shift || User.current, *args)) |
... | ... | |
53 | 59 |
where("#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}") |
54 | 60 |
} |
55 | 61 | |
56 |
safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields' |
|
62 |
safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields', 'remaining_time_action', 'remaining_time_hours'
|
|
57 | 63 | |
58 | 64 |
# Returns a SQL conditions string used to find all time entries visible by the specified user |
59 | 65 |
def self.visible_condition(user, options={}) |
... | ... | |
117 | 123 |
errors.add :project_id, :invalid if project.nil? |
118 | 124 |
errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project) || @invalid_issue_id |
119 | 125 |
errors.add :activity_id, :inclusion if activity_id_changed? && project && !project.activities.include?(activity) |
126 |
errors.add :remaining_time_hours, :invalid if (remaining_time_action == 'set' && remaining_time_hours.blank?) |
|
120 | 127 |
end |
121 | 128 | |
122 | 129 |
def hours=(h) |
... | ... | |
157 | 164 |
def editable_custom_fields(user=nil) |
158 | 165 |
editable_custom_field_values(user).map(&:custom_field).uniq |
159 | 166 |
end |
167 | ||
168 |
def remaining_time_action |
|
169 |
@remaining_time_action |
|
170 |
end |
|
171 | ||
172 |
def remaining_time_hours |
|
173 |
@remaining_time_hours |
|
174 |
end |
|
175 | ||
176 |
def update_issue_remaining_hours |
|
177 |
issue = self.issue |
|
178 |
return unless issue |
|
179 | ||
180 |
h = (remaining_time_hours.is_a?(String)) ? remaining_time_hours.to_hours : remaining_time_hours |
|
181 | ||
182 |
case remaining_time_action |
|
183 |
when "auto" |
|
184 |
new_remaining_hours = issue.remaining_hours - self.hours unless issue.remaining_hours.nil? |
|
185 |
when "set" |
|
186 |
new_remaining_hours = h |
|
187 |
when "nothing" |
|
188 |
return |
|
189 |
end |
|
190 | ||
191 |
issue.init_journal(User.current) |
|
192 |
issue.remaining_hours = new_remaining_hours |
|
193 |
issue.save |
|
194 |
end |
|
160 | 195 |
end |
app/views/issues/_edit.html.erb | ||
---|---|---|
24 | 24 |
<% @time_entry.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 | ||
---|---|---|
24 | 24 |
<% @time_entry.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 => 'remaining_time', :locals => { :issue => @time_entry.issue } %> |
|
29 |
</p> |
|
27 | 30 |
<%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %> |
28 | 31 |
</div> |
29 | 32 |
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 %> |
|
43 |
app/views/timelog/new.js.erb | ||
---|---|---|
1 | 1 |
$('#time_entry_activity_id').html('<%= escape_javascript options_for_select(activity_collection_for_select_options(@time_entry), @time_entry.activity_id) %>'); |
2 |
$('#new_time_entry p#issue_remaining_time').html('<%= escape_javascript render :partial => "remaining_time", :locals => { :issue => @time_entry.issue } %>'); |
config/locales/en.yml | ||
---|---|---|
1004 | 1004 |
label_font_default: Default font |
1005 | 1005 |
label_font_monospace: Monospaced font |
1006 | 1006 |
label_font_proportional: Proportional font |
1007 |
label_issue_remaining_hours: Issue remaining time |
|
1008 |
label_remaining_time_action_auto: Adjust automatically |
|
1009 |
label_remaining_time_action_set: Set to |
|
1010 |
label_remaining_time_action_nothing: Do not update remaining time |
|
1007 | 1011 | |
1008 | 1012 |
button_login: Login |
1009 | 1013 |
button_submit: Submit |
public/stylesheets/application.css | ||
---|---|---|
658 | 658 |
width:auto; |
659 | 659 |
} |
660 | 660 |
input#time_entry_comments { width: 90%;} |
661 |
p#issue_remaining_time label.hours { display: inline-block; } |
|
662 | ||
661 | 663 | |
662 | 664 |
#preview fieldset {margin-top: 1em; background: url(../images/draft.png)} |
663 | 665 |
test/functional/timelog_controller_test.rb | ||
---|---|---|
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 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 3 |
|
63 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 1 |
|
62 | 64 |
end |
63 | 65 | |
64 | 66 |
def test_new_without_project_should_prefill_the_form |
... | ... | |
95 | 97 |
assert_select 'option', :text => 'Inactive Activity', :count => 0 |
96 | 98 |
end |
97 | 99 | |
100 |
def test_new_on_issue_with_remaining_time_disabled_should_not_show_the_update_issue_remaining_time_section |
|
101 |
tracker = Tracker.find(2) |
|
102 |
tracker.core_fields = tracker.core_fields - %w(remaining_hours) |
|
103 |
tracker.save! |
|
104 | ||
105 |
@request.session[:user_id] = 3 |
|
106 |
get :new, :issue_id => 2 |
|
107 |
assert_response :success |
|
108 |
assert_template 'new' |
|
109 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 0 |
|
110 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 0 |
|
111 |
end |
|
112 | ||
98 | 113 |
def test_post_new_as_js_should_update_activity_options |
99 | 114 |
@request.session[:user_id] = 3 |
100 | 115 |
post :new, :params => {:time_entry => {:project_id => 1}, :format => 'js'} |
... | ... | |
110 | 125 |
assert_select 'form[action=?]', '/time_entries/2' |
111 | 126 |
end |
112 | 127 | |
128 |
def test_get_should_not_show_the_adjust_automatically_option_in_issue_remaining_time_section |
|
129 |
@request.session[:user_id] = 2 |
|
130 |
get :edit, :id => 2, :project_id => nil |
|
131 |
assert_response :success |
|
132 |
assert_template 'edit' |
|
133 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 2 |
|
134 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 1 |
|
135 |
assert_select 'input[type=radio][id=?]', 'time_entry_remaining_time_action_auto]', 0 |
|
136 |
end |
|
137 | ||
113 | 138 |
def test_get_edit_with_an_existing_time_entry_with_inactive_activity |
114 | 139 |
te = TimeEntry.find(1) |
115 | 140 |
te.activity = TimeEntryActivity.find_by_name("Inactive Activity") |
... | ... | |
501 | 526 |
assert_select 'form#bulk_edit_form[action=?]', '/time_entries/bulk_update' do |
502 | 527 |
# System wide custom field |
503 | 528 |
assert_select 'select[name=?]', 'time_entry[custom_field_values][10]' |
504 |
|
|
529 | ||
505 | 530 |
# Activities |
506 | 531 |
assert_select 'select[name=?]', 'time_entry[activity_id]' do |
507 | 532 |
assert_select 'option[value=""]', :text => '(No change)' |
... | ... | |
549 | 574 |
@request.session[:user_id] = 2 |
550 | 575 |
# makes user a manager on the other project |
551 | 576 |
Member.create!(:user_id => 2, :project_id => 3, :role_ids => [1]) |
552 |
|
|
577 | ||
553 | 578 |
# update time entry activity |
554 | 579 |
post :bulk_update, :params => {:ids => [1, 2, 4], :time_entry => { :activity_id => 9 }} |
555 | 580 |
test/integration/api_test/issues_test.rb | ||
---|---|---|
358 | 358 |
parent = Issue.find(3) |
359 | 359 |
child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) |
360 | 360 |
TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, |
361 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) |
|
361 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id, :remaining_time_action => 'nothing')
|
|
362 | 362 |
get '/issues/3.xml' |
363 | 363 | |
364 | 364 |
assert_equal 'application/xml', response.content_type |
... | ... | |
395 | 395 |
parent = Issue.find(3) |
396 | 396 |
child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) |
397 | 397 |
TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, |
398 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) |
|
398 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id, :remaining_time_action => 'nothing')
|
|
399 | 399 |
get '/issues/3.json' |
400 | 400 | |
401 | 401 |
assert_equal 'application/json', response.content_type |
test/unit/time_entry_test.rb | ||
---|---|---|
172 | 172 |
:activity => activity) |
173 | 173 |
assert_equal project.id, te.project.id |
174 | 174 |
end |
175 | ||
176 |
def test_time_entry_decrease_issue_remaining_time_with_logged_time |
|
177 |
issue = Issue.find(1) |
|
178 |
issue.update_attribute(:remaining_hours, 3) |
|
179 | ||
180 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
181 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
182 |
:remaining_time_action => "auto") |
|
183 | ||
184 |
assert te.save |
|
185 |
assert_equal 2, issue.remaining_hours |
|
186 |
end |
|
187 | ||
188 |
def test_time_entry_set_issue_remaining_time |
|
189 |
issue = Issue.find(1) |
|
190 |
issue.update_attribute(:remaining_hours, 3) |
|
191 | ||
192 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
193 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
194 |
:remaining_time_action => "set", :remaining_time_hours => 4) |
|
195 | ||
196 |
assert te.save |
|
197 |
assert_equal 4, issue.remaining_hours |
|
198 |
end |
|
199 | ||
200 |
def test_time_entry_do_not_update_issue_remaining_time |
|
201 |
issue = Issue.find(1) |
|
202 |
issue.update_attribute(:remaining_hours, 3) |
|
203 | ||
204 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
205 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
206 |
:remaining_time_action => "nothing", :remaining_time_hours => 4) |
|
207 |
assert te.save |
|
208 |
assert_equal 3, issue.remaining_hours |
|
209 |
end |
|
210 | ||
211 |
def test_time_entry_do_not_decrease_issue_remaining_time_when_issue_remaining_time_is_nil |
|
212 |
issue = Issue.find(1) |
|
213 |
issue.update_attribute(:remaining_hours, '') |
|
214 | ||
215 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
216 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
217 |
:remaining_time_action => "auto", :remaining_time_hours => 4) |
|
218 |
assert te.save |
|
219 |
assert_nil issue.remaining_hours |
|
220 |
end |
|
221 | ||
222 |
def test_validate_time_entry_remaining_time_action |
|
223 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => Issue.find(1), |
|
224 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
225 |
:remaining_time_action => "test", :remaining_time_hours => 'one') |
|
226 |
assert !te.valid? |
|
227 |
assert_equal 2, te.errors.count |
|
228 |
end |
|
175 | 229 |
end |