Project

General

Profile

Feature #24277 » 02_add_remaining_time_to_log_time_5.1.2.patch

Radek Pesina, 2024-05-01 03:47

View differences:

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
(12-12/14)