Feature #24277 » 02_add_remaining_time_to_log_time_3.4.1.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| |
... | ... | |
45 | 46 |
validates_presence_of :issue_id, :if => lambda { Setting.timelog_required_fields.include?('issue_id') } |
46 | 47 |
validates_presence_of :comments, :if => lambda { Setting.timelog_required_fields.include?('comments') } |
47 | 48 |
validates_numericality_of :hours, :allow_nil => true, :message => :invalid |
49 |
validates :remaining_time_action, :inclusion => { :in => ['auto', 'set', 'nothing'] }, :allow_nil => true |
|
50 |
validates :remaining_time_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_blank => true, :message => :invalid} |
|
51 | ||
48 | 52 |
validates_length_of :comments, :maximum => 1024, :allow_nil => true |
49 | 53 |
validates :spent_on, :date => true |
50 | 54 |
before_validation :set_project_if_nil |
51 | 55 |
validate :validate_time_entry |
52 | 56 | |
57 |
after_save :update_issue_remaining_hours |
|
58 | ||
53 | 59 |
scope :visible, lambda {|*args| |
54 | 60 |
joins(:project). |
55 | 61 |
where(TimeEntry.visible_condition(args.shift || User.current, *args)) |
... | ... | |
62 | 68 |
where("#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}") |
63 | 69 |
} |
64 | 70 | |
65 |
safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields' |
|
71 |
safe_attributes 'hours', 'comments', 'project_id', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields', 'remaining_time_action', 'remaining_time_hours'
|
|
66 | 72 | |
67 | 73 |
# Returns a SQL conditions string used to find all time entries visible by the specified user |
68 | 74 |
def self.visible_condition(user, options={}) |
... | ... | |
126 | 132 |
errors.add :project_id, :invalid if project.nil? |
127 | 133 |
errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project) || @invalid_issue_id |
128 | 134 |
errors.add :activity_id, :inclusion if activity_id_changed? && project && !project.activities.include?(activity) |
135 |
errors.add :remaining_time_hours, :invalid if (remaining_time_action == 'set' && remaining_time_hours.blank?) |
|
129 | 136 |
end |
130 | 137 | |
131 | 138 |
def hours=(h) |
... | ... | |
166 | 173 |
def editable_custom_fields(user=nil) |
167 | 174 |
editable_custom_field_values(user).map(&:custom_field).uniq |
168 | 175 |
end |
176 | ||
177 |
def remaining_time_action |
|
178 |
@remaining_time_action |
|
179 |
end |
|
180 | ||
181 |
def remaining_time_hours |
|
182 |
@remaining_time_hours |
|
183 |
end |
|
184 | ||
185 |
def update_issue_remaining_hours |
|
186 |
issue = self.issue |
|
187 |
return unless issue |
|
188 | ||
189 |
h = (remaining_time_hours.is_a?(String)) ? remaining_time_hours.to_hours : remaining_time_hours |
|
190 | ||
191 |
case remaining_time_action |
|
192 |
when "auto" |
|
193 |
new_remaining_hours = issue.remaining_hours - self.hours unless issue.remaining_hours.nil? |
|
194 |
when "set" |
|
195 |
new_remaining_hours = h |
|
196 |
when "nothing" |
|
197 |
return |
|
198 |
end |
|
199 | ||
200 |
issue.init_journal(User.current) |
|
201 |
issue.remaining_hours = new_remaining_hours |
|
202 |
issue.save |
|
203 |
end |
|
169 | 204 |
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 | ||
---|---|---|
23 | 23 |
<% @time_entry.custom_field_values.each do |value| %> |
24 | 24 |
<p><%= custom_field_tag_with_label :time_entry, value %></p> |
25 | 25 |
<% end %> |
26 |
<p id="issue_remaining_time"> |
|
27 |
<%= render :partial => 'remaining_time', :locals => { :issue => @time_entry.issue } %> |
|
28 |
</p> |
|
26 | 29 |
<%= call_hook(:view_timelog_edit_form_bottom, { :time_entry => @time_entry, :form => f }) %> |
27 | 30 |
</div> |
28 | 31 |
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 } %>'); |
|
2 | 3 |
$('#time_entry_issue').html('<%= escape_javascript link_to_issue(@time_entry.issue) if @time_entry.issue.try(:visible?) %>'); |
config/locales/en.yml | ||
---|---|---|
1021 | 1021 |
label_font_monospace: Monospaced font |
1022 | 1022 |
label_font_proportional: Proportional font |
1023 | 1023 |
label_last_notes: Last notes |
1024 |
label_issue_remaining_hours: Issue remaining time |
|
1025 |
label_remaining_time_action_auto: Adjust automatically |
|
1026 |
label_remaining_time_action_set: Set to |
|
1027 |
label_remaining_time_action_nothing: Do not update remaining time |
|
1024 | 1028 | |
1025 | 1029 |
button_login: Login |
1026 | 1030 |
button_submit: Submit |
public/stylesheets/application.css | ||
---|---|---|
699 | 699 |
width:auto; |
700 | 700 |
} |
701 | 701 |
input#time_entry_comments { width: 90%;} |
702 |
p#issue_remaining_time label.hours { display: inline-block; } |
|
703 | ||
702 | 704 | |
703 | 705 |
#preview fieldset {margin-top: 1em; background: url(../images/draft.png)} |
704 | 706 |
test/functional/timelog_controller_test.rb | ||
---|---|---|
61 | 61 |
assert_select 'input[name=?][type=hidden]', 'issue_id' |
62 | 62 |
assert_select 'a[href=?]', '/issues/2', :text => /Feature request #2/ |
63 | 63 |
assert_select 'select[name=?]', 'time_entry[project_id]', 0 |
64 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 3 |
|
65 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 1 |
|
64 | 66 |
end |
65 | 67 | |
66 | 68 |
def test_new_without_project_should_prefill_the_form |
... | ... | |
97 | 99 |
assert_select 'option', :text => 'Inactive Activity', :count => 0 |
98 | 100 |
end |
99 | 101 | |
102 |
def test_new_on_issue_with_remaining_time_disabled_should_not_show_the_update_issue_remaining_time_section |
|
103 |
tracker = Tracker.find(2) |
|
104 |
tracker.core_fields = tracker.core_fields - %w(remaining_hours) |
|
105 |
tracker.save! |
|
106 | ||
107 |
@request.session[:user_id] = 3 |
|
108 |
get :new, :issue_id => 2 |
|
109 |
assert_response :success |
|
110 |
assert_template 'new' |
|
111 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 0 |
|
112 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 0 |
|
113 |
end |
|
114 | ||
100 | 115 |
def test_post_new_as_js_should_update_activity_options |
101 | 116 |
@request.session[:user_id] = 3 |
102 | 117 |
post :new, :params => {:time_entry => {:project_id => 1}, :format => 'js'} |
... | ... | |
112 | 127 |
assert_select 'form[action=?]', '/time_entries/2' |
113 | 128 |
end |
114 | 129 | |
130 |
def test_get_should_not_show_the_adjust_automatically_option_in_issue_remaining_time_section |
|
131 |
@request.session[:user_id] = 2 |
|
132 |
get :edit, :id => 2, :project_id => nil |
|
133 |
assert_response :success |
|
134 |
assert_template 'edit' |
|
135 |
assert_select 'input[type=radio][name=?]', 'time_entry[remaining_time_action]', 2 |
|
136 |
assert_select 'input[type=text][name=?]', 'time_entry[remaining_time_hours]', 1 |
|
137 |
assert_select 'input[type=radio][id=?]', 'time_entry_remaining_time_action_auto]', 0 |
|
138 |
end |
|
139 | ||
115 | 140 |
def test_get_edit_with_an_existing_time_entry_with_inactive_activity |
116 | 141 |
te = TimeEntry.find(1) |
117 | 142 |
te.activity = TimeEntryActivity.find_by_name("Inactive Activity") |
test/integration/api_test/issues_test.rb | ||
---|---|---|
387 | 387 |
parent.update_columns :estimated_hours => 2.0 |
388 | 388 |
child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0) |
389 | 389 |
TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, |
390 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) |
|
390 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id, :remaining_time_action => 'nothing')
|
|
391 | 391 |
get '/issues/3.xml' |
392 | 392 | |
393 | 393 |
assert_equal 'application/xml', response.content_type |
... | ... | |
443 | 443 |
parent.update_columns :estimated_hours => 2.0 |
444 | 444 |
child = Issue.generate!(:parent_issue_id => parent.id, :estimated_hours => 3.0, :remaining_hours => 1.0) |
445 | 445 |
TimeEntry.create!(:project => child.project, :issue => child, :user => child.author, :spent_on => child.author.today, |
446 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id) |
|
446 |
:hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id, :remaining_time_action => 'nothing')
|
|
447 | 447 |
get '/issues/3.json' |
448 | 448 | |
449 | 449 |
assert_equal 'application/json', response.content_type |
test/unit/time_entry_test.rb | ||
---|---|---|
184 | 184 |
assert_equal ["Comment cannot be blank", "Issue cannot be blank"], entry.errors.full_messages.sort |
185 | 185 |
end |
186 | 186 |
end |
187 | ||
188 |
def test_time_entry_decrease_issue_remaining_time_with_logged_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 => "auto") |
|
195 | ||
196 |
assert te.save |
|
197 |
assert_equal 2, issue.remaining_hours |
|
198 |
end |
|
199 | ||
200 |
def test_time_entry_set_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 => "set", :remaining_time_hours => 4) |
|
207 | ||
208 |
assert te.save |
|
209 |
assert_equal 4, issue.remaining_hours |
|
210 |
end |
|
211 | ||
212 |
def test_time_entry_do_not_update_issue_remaining_time |
|
213 |
issue = Issue.find(1) |
|
214 |
issue.update_attribute(:remaining_hours, 3) |
|
215 | ||
216 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
217 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
218 |
:remaining_time_action => "nothing", :remaining_time_hours => 4) |
|
219 |
assert te.save |
|
220 |
assert_equal 3, issue.remaining_hours |
|
221 |
end |
|
222 | ||
223 |
def test_time_entry_do_not_decrease_issue_remaining_time_when_issue_remaining_time_is_nil |
|
224 |
issue = Issue.find(1) |
|
225 |
issue.update_attribute(:remaining_hours, '') |
|
226 | ||
227 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => issue, |
|
228 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
229 |
:remaining_time_action => "auto", :remaining_time_hours => 4) |
|
230 |
assert te.save |
|
231 |
assert_nil issue.remaining_hours |
|
232 |
end |
|
233 | ||
234 |
def test_validate_time_entry_remaining_time_action |
|
235 |
te = TimeEntry.new(:spent_on => '2010-01-01', :hours => 1, :issue => Issue.find(1), |
|
236 |
:user => User.find(1), :activity => TimeEntryActivity.first, |
|
237 |
:remaining_time_action => "test", :remaining_time_hours => 'one') |
|
238 |
assert !te.valid? |
|
239 |
assert_equal 2, te.errors.count |
|
240 |
end |
|
187 | 241 |
end |