Project

General

Profile

Feature #44254 » 0001-Allow-updating-the-delay-of-an-existing-precedes-fol.patch

[Agileware]Shun(ji) Nishitani, 2026-07-10 14:27

View differences:

app/controllers/issue_relations_controller.rb
21 21
  helper :issues
22 22

  
23 23
  before_action :find_issue, :authorize, :only => [:index, :create]
24
  before_action :find_relation, :only => [:show, :destroy]
24
  before_action :find_relation, :only => [:show, :update, :destroy]
25 25

  
26
  accept_api_auth :index, :show, :create, :destroy
26
  accept_api_auth :index, :show, :create, :update, :destroy
27 27

  
28 28
  def index
29 29
    @relations = @issue.relations
......
81 81
    end
82 82
  end
83 83

  
84
  def update
85
    raise Unauthorized unless @relation.editable?
86

  
87
    @issue =
88
      [@relation.issue_from, @relation.issue_to].compact.detect do |issue|
89
        issue.id.to_s == params[:issue_id].to_s
90
      end || @relation.issue_from
91
    @project = @issue.project
92

  
93
    @relation.init_journals(User.current)
94
    # Only the delay can be updated, other attributes require the relation
95
    # to be recreated
96
    @relation.safe_attributes = params.expect(:relation => [:delay])
97
    saved = @relation.save
98

  
99
    respond_to do |format|
100
      format.html {redirect_to issue_path(@issue)}
101
      format.js do
102
        @relations = select_relations(@issue)
103
      end
104
      format.api do
105
        if saved
106
          render_api_ok
107
        else
108
          render_validation_errors(@relation)
109
        end
110
      end
111
    end
112
  end
113

  
84 114
  def destroy
85 115
    raise Unauthorized unless @relation.deletable?
86 116

  
app/helpers/issues_helper.rb
222 222
    relations.each do |relation|
223 223
      other_issue = relation.other_issue(issue)
224 224
      css = "issue hascontextmenu #{other_issue.css_classes} #{relation.css_classes_for(other_issue)}"
225
      buttons =
226
        if manage_relations
227
          link_to(
228
            sprite_icon('link-break', l(:label_relation_remove)),
229
            relation_path(relation, issue_id: issue.id),
230
            :remote => true,
231
            :method => :delete,
232
            :data => {:confirm => l(:text_are_you_sure)},
233
            :title => l(:label_relation_remove),
234
            :class => 'icon-only icon-link-break'
225
      buttons = "".html_safe
226
      if manage_relations
227
        if relation.relation_type == IssueRelation::TYPE_PRECEDES
228
          buttons << link_to_function(
229
            sprite_icon('edit', l(:label_relation_edit_delay)),
230
            "$('.edit-relation-form').hide();" \
231
            "$('#edit-relation-#{relation.id}-form').show();" \
232
            "$('#relation_#{relation.id}_delay').focus();",
233
            :title => l(:label_relation_edit_delay),
234
            :class => 'icon-only icon-edit'
235 235
          )
236
        else
237
          "".html_safe
238 236
        end
237
        buttons << link_to(
238
          sprite_icon('link-break', l(:label_relation_remove)),
239
          relation_path(relation, issue_id: issue.id),
240
          :remote => true,
241
          :method => :delete,
242
          :data => {:confirm => l(:text_are_you_sure)},
243
          :title => l(:label_relation_remove),
244
          :class => 'icon-only icon-link-break'
245
        )
246
      end
239 247
      buttons << link_to_context_menu
240 248

  
241 249
      subject_content = relation.to_s(@issue) { |other| link_to_issue other, project: Setting.cross_project_issue_relations? }.html_safe
app/models/issue_relation.rb
110 110
        (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
111 111
  end
112 112

  
113
  def editable?(user=User.current)
114
    visible?(user) &&
115
      ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
116
        (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
117
  end
118

  
113 119
  def initialize(attributes=nil, *args)
114 120
    super
115 121
    if new_record?
app/views/issue_relations/update.js.erb
1
<% if @relation.errors.any? %>
2
alert('<%= escape_javascript(@relation.errors.full_messages.join("\n")) %>');
3
<% else %>
4
$('#relations').html('<%= escape_javascript(render :partial => 'issues/relations') %>');
5
$('#relation_delay').val('');
6
$('#relation_issue_to_id').val('');
7
<% end %>
app/views/issues/_relations.html.erb
15 15
    <%= render_issue_relations(@issue, @relations) %>
16 16
  </div>
17 17
<% end %>
18
<% if User.current.allowed_to?(:manage_issue_relations, @project) %>
19
  <% @relations.each do |relation| %>
20
    <% next unless relation.relation_type == IssueRelation::TYPE_PRECEDES && relation.other_issue(@issue) %>
21
    <%= form_for relation, {
22
                     :as => :relation, :remote => true,
23
                     :url => relation_path(relation, :issue_id => @issue.id),
24
                     :method => :put,
25
                     :html => {:id => "edit-relation-#{relation.id}-form",
26
                               :class => 'edit-relation-form',
27
                               :style => 'display: none;'}
28
                   } do |f| %>
29
      <p>
30
        <%= relation.to_s(@issue) {|other| "##{other.id}"} %>:
31
        <%= l(:field_delay) %> <%= f.text_field :delay, :size => 3, :id => "relation_#{relation.id}_delay" %> <%= l(:label_day_plural) %>
32
        <%= submit_tag l(:button_save) %>
33
        <%= link_to_function l(:button_cancel), "$('#edit-relation-#{relation.id}-form').hide();" %>
34
      </p>
35
    <% end %>
36
  <% end %>
37
<% end %>
18 38
<% end %>
19 39

  
20 40
<%= form_for @relation, {
config/locales/en.yml
917 917
  label_loading: Loading...
918 918
  label_relation_new: New relation
919 919
  label_relation_remove: Remove relation
920
  label_relation_edit_delay: Edit delay
920 921
  label_relates_to: Related to
921 922
  label_subtask_remove: Remove subtask
922 923
  label_duplicates: Is duplicate of
config/locales/ja.yml
1489 1489
  webhook_event_deleted: "%{object_name} を削除しました"
1490 1490
  label_auto_watch_on_issue_assigned_to_me: 自分が担当者になったチケット
1491 1491
  label_relation_remove: 関連を解除
1492
  label_relation_edit_delay: 遅延を変更
1492 1493
  label_subtask_remove: 子チケットを解除
1493 1494
  button_remove: 解除
1494 1495
  label_open_in_full_view: 全画面で表示
config/routes.rb
236 236
    end
237 237
    resources :time_entries, :controller => 'timelog', :only => [:new, :create]
238 238
    shallow do
239
      resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
239
      resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :update, :destroy]
240 240
    end
241 241
  end
242 242
  # Used when updating the form of a new issue outside a project
test/functional/issue_relations_controller_test.rb
283 283
    assert_include 'Related issue doesn&#39;t belong to the same project', response.body
284 284
  end
285 285

  
286
  def test_update
287
    relation = IssueRelation.create!(
288
      :issue_from => Issue.find(1), :issue_to => Issue.find(2),
289
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 2
290
    )
291

  
292
    assert_no_difference 'IssueRelation.count' do
293
      put(
294
        :update,
295
        :params => {
296
          :id => relation.id,
297
          :relation => {:delay => '5'}
298
        }
299
      )
300
    end
301
    assert_redirected_to '/issues/1'
302
    assert_equal 5, relation.reload.delay
303
  end
304

  
305
  def test_update_xhr
306
    relation = IssueRelation.create!(
307
      :issue_from => Issue.find(1), :issue_to => Issue.find(2),
308
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 2
309
    )
310

  
311
    put(
312
      :update,
313
      :params => {
314
        :id => relation.id,
315
        :issue_id => 1,
316
        :relation => {:delay => '5'}
317
      },
318
      :xhr => true
319
    )
320
    assert_response :success
321
    assert_equal 'text/javascript', response.media_type
322
    assert_include '#relations', response.body
323
    assert_include "id=\\\"edit-relation-#{relation.id}-form\\\"", response.body
324
    assert_equal 5, relation.reload.delay
325
  end
326

  
327
  def test_update_xhr_with_failure
328
    relation = IssueRelation.create!(
329
      :issue_from => Issue.find(1), :issue_to => Issue.find(2),
330
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 2
331
    )
332

  
333
    put(
334
      :update,
335
      :params => {
336
        :id => relation.id,
337
        :issue_id => 1,
338
        :relation => {:delay => 'foo'}
339
      },
340
      :xhr => true
341
    )
342
    assert_response :success
343
    assert_equal 'text/javascript', response.media_type
344
    assert_include 'Delay is not a number', response.body
345
    assert_equal 2, relation.reload.delay
346
  end
347

  
348
  def test_update_should_not_change_attributes_other_than_delay
349
    relation = IssueRelation.create!(
350
      :issue_from => Issue.find(1), :issue_to => Issue.find(2),
351
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 2
352
    )
353

  
354
    put(
355
      :update,
356
      :params => {
357
        :id => relation.id,
358
        :relation => {
359
          :delay => '5',
360
          :relation_type => 'blocks',
361
          :issue_to_id => '3'
362
        }
363
      }
364
    )
365
    relation.reload
366
    assert_equal 5, relation.delay
367
    assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type
368
    assert_equal 2, relation.issue_to_id
369
  end
370

  
371
  def test_update_should_reschedule_following_issue
372
    issue1 = Issue.generate!(:start_date => Date.parse('2026-07-01'),
373
                             :due_date => Date.parse('2026-07-02'))
374
    issue2 = Issue.generate!
375
    relation = IssueRelation.create!(
376
      :issue_from => issue1, :issue_to => issue2,
377
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 0
378
    )
379

  
380
    with_settings :non_working_week_days => [] do
381
      put(
382
        :update,
383
        :params => {
384
          :id => relation.id,
385
          :relation => {:delay => '2'}
386
        }
387
      )
388
    end
389
    assert_equal 2, relation.reload.delay
390
    assert_equal Date.parse('2026-07-05'), issue2.reload.start_date
391
  end
392

  
393
  def test_update_without_permission_should_fail
394
    relation = IssueRelation.create!(
395
      :issue_from => Issue.find(1), :issue_to => Issue.find(2),
396
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 2
397
    )
398
    Role.all.each {|r| r.remove_permission! :manage_issue_relations}
399

  
400
    put(
401
      :update,
402
      :params => {
403
        :id => relation.id,
404
        :relation => {:delay => '5'}
405
      }
406
    )
407
    assert_response :forbidden
408
    assert_equal 2, relation.reload.delay
409
  end
410

  
286 411
  def test_destroy
287 412
    assert_difference 'IssueRelation.count', -1 do
288 413
      delete(:destroy, :params => {:id => '2'})
test/integration/api_test/issue_relations_test.rb
90 90
    assert_select 'relation id', :text => '2'
91 91
  end
92 92

  
93
  test "PUT /relations/:id.xml should update the delay" do
94
    relation = IssueRelation.create!(
95
      :issue_from => Issue.find(1), :issue_to => Issue.find(2),
96
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 2
97
    )
98

  
99
    put(
100
      "/relations/#{relation.id}.xml",
101
      :params => {:relation => {:delay => 5}},
102
      :headers => credentials('jsmith')
103
    )
104

  
105
    assert_response :no_content
106
    assert_equal 5, relation.reload.delay
107
  end
108

  
109
  test "PUT /relations/:id.xml with invalid delay should return errors" do
110
    relation = IssueRelation.create!(
111
      :issue_from => Issue.find(1), :issue_to => Issue.find(2),
112
      :relation_type => IssueRelation::TYPE_PRECEDES, :delay => 2
113
    )
114

  
115
    put(
116
      "/relations/#{relation.id}.xml",
117
      :params => {:relation => {:delay => 'foo'}},
118
      :headers => credentials('jsmith')
119
    )
120

  
121
    assert_response :unprocessable_content
122
    assert_select 'errors error', :text => /Delay is not a number/
123
    assert_equal 2, relation.reload.delay
124
  end
125

  
93 126
  test "DELETE /relations/:id.xml should delete the relation" do
94 127
    assert_difference('IssueRelation.count', -1) do
95 128
      delete '/relations/2.xml', :headers => credentials('jsmith')
(2-2/2)