Feature #44254 » 0001-Allow-updating-the-delay-of-an-existing-precedes-fol.patch
| 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'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')
|
- « Previous
- 1
- 2
- Next »