02_add_remaining_time_to_log_time_3.4.1.patch

Marius BALTEANU, 2017-07-10 19:50

Download (14.6 KB)

View differences:

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