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 |