Feature #8691 » mid_air_collision_patch-20110131-tags_1.3.0.patch
| app/models/issue.rb (working copy) | ||
|---|---|---|
| 635 | 635 |
rescue ActiveRecord::StaleObjectError |
| 636 | 636 |
attachments[:files].each(&:destroy) |
| 637 | 637 |
errors.add :base, l(:notice_locking_conflict) |
| 638 |
raise ActiveRecord::Rollback
|
|
| 638 |
raise ActiveRecord::StaleObjectError
|
|
| 639 | 639 |
end |
| 640 | 640 |
end |
| 641 | 641 |
end |
| app/controllers/issues_controller.rb (working copy) | ||
|---|---|---|
| 111 | 111 |
def show |
| 112 | 112 |
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
|
| 113 | 113 |
@journals.each_with_index {|j,i| j.indice = i+1}
|
| 114 |
@latest_journal = @journals.last |
|
| 114 | 115 |
@journals.reverse! if User.current.wants_comments_in_reverse_order? |
| 115 | 116 | |
| 116 | 117 |
if User.current.allowed_to?(:view_changesets, @project) |
| ... | ... | |
| 175 | 176 |
end |
| 176 | 177 | |
| 177 | 178 |
def update |
| 179 |
if params[:conflict_resolution].present? |
|
| 180 |
case params[:conflict_resolution] |
|
| 181 |
when 'none' |
|
| 182 |
redirect_to :controller => 'issues', :action => 'show', :id => @issue |
|
| 183 |
return |
|
| 184 |
when 'notes' |
|
| 185 |
# update notes only |
|
| 186 |
params.delete(:attachments) |
|
| 187 |
params.delete(:issue) |
|
| 188 |
params.delete(:time_entry) |
|
| 189 |
when 'all' |
|
| 190 |
; |
|
| 191 |
end |
|
| 192 |
end |
|
| 193 |
|
|
| 178 | 194 |
update_issue_from_params |
| 179 | 195 | |
| 180 |
if @issue.save_issue_with_child_records(params, @time_entry) |
|
| 181 |
render_attachment_warning_if_needed(@issue) |
|
| 182 |
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? |
|
| 183 | ||
| 184 |
respond_to do |format| |
|
| 185 |
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
|
|
| 186 |
format.api { head :ok }
|
|
| 196 |
begin |
|
| 197 |
if @issue.save_issue_with_child_records(params, @time_entry) |
|
| 198 |
render_attachment_warning_if_needed(@issue) |
|
| 199 |
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? |
|
| 200 |
|
|
| 201 |
respond_to do |format| |
|
| 202 |
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
|
|
| 203 |
format.api { head :ok }
|
|
| 204 |
end |
|
| 205 |
else |
|
| 206 |
render_attachment_warning_if_needed(@issue) |
|
| 207 |
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? |
|
| 208 |
@journal = @issue.current_journal |
|
| 209 |
|
|
| 210 |
respond_to do |format| |
|
| 211 |
format.html { render :action => 'edit' }
|
|
| 212 |
format.api { render_validation_errors(@issue) }
|
|
| 213 |
end |
|
| 187 | 214 |
end |
| 188 |
else |
|
| 215 |
rescue ActiveRecord::StaleObjectError |
|
| 216 |
# mid-air conflict |
|
| 217 |
@conflict_detected = true |
|
| 218 |
|
|
| 219 |
@issue.reload |
|
| 220 |
params[:issue].delete(:lock_version) if params[:issue] |
|
| 221 |
update_issue_from_params |
|
| 222 |
|
|
| 223 |
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
|
|
| 224 |
@journals.each_with_index {|j,i| j.indice = i+1}
|
|
| 225 |
@latest_journal = @journals.last |
|
| 226 |
if params[:latest_journal].present? |
|
| 227 |
latest_journal_id = params[:latest_journal].to_i |
|
| 228 |
@journals = @journals.select{|journal| latest_journal_id < journal.id }
|
|
| 229 |
end |
|
| 230 |
@journals.reverse! if User.current.wants_comments_in_reverse_order? |
|
| 231 | ||
| 189 | 232 |
render_attachment_warning_if_needed(@issue) |
| 190 | 233 |
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? |
| 191 | 234 |
@journal = @issue.current_journal |
| 192 | ||
| 235 |
|
|
| 193 | 236 |
respond_to do |format| |
| 194 |
format.html { render :action => 'edit' }
|
|
| 237 |
format.html { render :action => 'conflict' }
|
|
| 195 | 238 |
format.api { render_validation_errors(@issue) }
|
| 196 | 239 |
end |
| 197 | 240 |
end |
| app/views/issues/_edit.html.erb (working copy) | ||
|---|---|---|
| 6 | 6 |
:multipart => true} do |f| %> |
| 7 | 7 |
<%= error_messages_for 'issue', 'time_entry' %> |
| 8 | 8 |
<div class="box"> |
| 9 |
<% if @conflict_detected %> |
|
| 10 |
<fieldset><legend><%= l(:label_conflict_resolution) %></legend> |
|
| 11 |
<label> |
|
| 12 |
<%= radio_button_tag 'conflict_resolution', 'none' %> |
|
| 13 |
<%= l(:label_conflict_resolution_none, |
|
| 14 |
:id => (link_to "##{@issue.id}", :controller => 'issues', :action => 'show', :id => @issue)) %>
|
|
| 15 |
</label> |
|
| 16 |
<br /> |
|
| 17 |
<% if @notes.present? %> |
|
| 18 |
<label><%= radio_button_tag 'conflict_resolution', 'notes', true %> <%= l(:label_conflict_resolution_notes) %></label> |
|
| 19 |
<br /> |
|
| 20 |
<% end %> |
|
| 21 |
<label><%= radio_button_tag 'conflict_resolution', 'all', @notes.blank? %> <%= l(:label_conflict_resolution_all) %></label> |
|
| 22 |
</fieldset> |
|
| 23 |
<% end %> |
|
| 24 |
<% if @latest_journal %> |
|
| 25 |
<%= hidden_field_tag 'latest_journal', @latest_journal.id %> |
|
| 26 |
<% end %> |
|
| 27 | ||
| 9 | 28 |
<% if @edit_allowed || !@allowed_statuses.empty? %> |
| 10 |
<fieldset class="tabular"><legend><%= l(:label_change_properties) %> |
|
| 29 |
<fieldset class="tabular conflict_resolution_all"><legend><%= l(:label_change_properties) %>
|
|
| 11 | 30 |
<% if !@issue.new_record? && !@issue.errors.any? && @edit_allowed %> |
| 12 | 31 |
<small>(<%= link_to l(:label_more), {}, :onclick => 'Effect.toggle("issue_descr_fields", "appear", {duration:0.3}); return false;' %>)</small>
|
| 13 | 32 |
<% end %> |
| ... | ... | |
| 16 | 35 |
</fieldset> |
| 17 | 36 |
<% end %> |
| 18 | 37 |
<% if User.current.allowed_to?(:log_time, @project) %> |
| 19 |
<fieldset class="tabular"><legend><%= l(:button_log_time) %></legend> |
|
| 38 |
<fieldset class="tabular conflict_resolution_all"><legend><%= l(:button_log_time) %></legend>
|
|
| 20 | 39 |
<% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %>
|
| 21 | 40 |
<div class="splitcontentleft"> |
| 22 | 41 |
<p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p> |
| ... | ... | |
| 32 | 51 |
</fieldset> |
| 33 | 52 |
<% end %> |
| 34 | 53 | |
| 35 |
<fieldset><legend><%= l(:field_notes) %></legend> |
|
| 54 |
<fieldset class="conflict_resolution_all conflict_resolution_notes"><legend><%= l(:field_notes) %></legend>
|
|
| 36 | 55 |
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> |
| 37 | 56 |
<%= wikitoolbar_for 'notes' %> |
| 38 | 57 |
<%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
|
| 39 | 58 | |
| 40 |
<p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p> |
|
| 59 |
<p class="conflict_resolution_all"><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
|
|
| 41 | 60 |
</fieldset> |
| 42 | 61 |
</div> |
| 43 | 62 | |
| 44 | 63 |
<%= f.hidden_field :lock_version %> |
| 45 |
<%= submit_tag l(:button_submit) %> |
|
| 64 |
<%= submit_tag l(:button_submit), :class => 'conflict_resolution_notes conflict_resolution_all' %> |
|
| 65 |
<% if @conflict_detected %> |
|
| 66 |
<%= submit_tag l(:button_conflict_resolution_none, :id => "##{@issue.id}"), :class => 'conflict_resolution_none' %>
|
|
| 67 |
<% end %> |
|
| 46 | 68 |
<%= link_to_remote l(:label_preview), |
| 47 | 69 |
{ :url => preview_issue_path(:project_id => @project, :id => @issue),
|
| 48 | 70 |
:method => 'post', |
| 49 | 71 |
:update => 'preview', |
| 50 | 72 |
:with => 'Form.serialize("issue-form")',
|
| 51 | 73 |
:complete => "Element.scrollTo('preview')"
|
| 52 |
}, :accesskey => accesskey(:preview) %> |
|
| 74 |
}, :accesskey => accesskey(:preview), :class => 'conflict_resolution_notes conflict_resolution_all' %>
|
|
| 53 | 75 |
<% end %> |
| 54 | 76 | |
| 55 |
<div id="preview" class="wiki"></div> |
|
| 77 |
<div id="preview" class="wiki conflict_resolution_notes conflict_resolution_all"></div> |
|
| 78 | ||
| 79 |
<% if @conflict_detected %> |
|
| 80 |
<%= javascript_tag 'setupShowHideByRadio("conflict_resolution_", [ "none", "notes", "all" ])' %>
|
|
| 81 |
<% end %> |
|
| app/views/issues/conflict.html.erb (working copy) | ||
|---|---|---|
| 1 |
<h2><%=h "#{@issue.tracker.name} ##{@issue.id}" %></h2>
|
|
| 2 | ||
| 3 |
<% if @journals.present? %> |
|
| 4 |
<div id="history"> |
|
| 5 |
<h3><%=l(:label_conflict_history)%></h3> |
|
| 6 |
<%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
|
|
| 7 |
</div> |
|
| 8 |
<% end %> |
|
| 9 | ||
| 10 |
<%= render :partial => 'edit' %> |
|
| 11 |
<% content_for :header_tags do %> |
|
| 12 |
<%= robot_exclusion_tag %> |
|
| 13 |
<% end %> |
|
| public/javascripts/application.js (working copy) | ||
|---|---|---|
| 415 | 415 |
} |
| 416 | 416 | |
| 417 | 417 |
Event.observe(window, 'load', hideOnLoad); |
| 418 | ||
| 419 |
/* |
|
| 420 |
* Setup observers of radio buttons to show/hide associated elements |
|
| 421 |
* automatically. |
|
| 422 |
* |
|
| 423 |
* id_base: |
|
| 424 |
* A base name of radio button ids to observe. |
|
| 425 |
* id_suffixes: |
|
| 426 |
* An array of radio button id suffixes to observe. |
|
| 427 |
* id_base + id_suffixes[i] is an id. |
|
| 428 |
* css_class_base: |
|
| 429 |
* A base name of css classes to show/hide. |
|
| 430 |
* css_class_base + css_class_suffixes[i] is a class. |
|
| 431 |
* When a radio button is clicked, |
|
| 432 |
* its corresponding css class is shown, and the others are hidden. |
|
| 433 |
* This parameter is an option, and defaults to id_base. |
|
| 434 |
* css_class_suffixes: |
|
| 435 |
* An array of css class suffixes to show/hide. |
|
| 436 |
* This parameter is an option, and defaults to id_suffixes. |
|
| 437 |
*/ |
|
| 438 |
function setupShowHideByRadio( |
|
| 439 |
id_base, id_suffixes, css_class_base, css_class_suffixes) {
|
|
| 440 |
if (css_class_base === undefined) {
|
|
| 441 |
css_class_base = id_base; |
|
| 442 |
} |
|
| 443 |
if (css_class_suffixes === undefined) {
|
|
| 444 |
css_class_suffixes = id_suffixes; |
|
| 445 |
} |
|
| 446 |
function f() {
|
|
| 447 |
/* find a checked radio button */ |
|
| 448 |
selected_index = -1; |
|
| 449 |
id_suffixes.each(function(id_suffix, index){
|
|
| 450 |
radio = $(id_base + id_suffix); |
|
| 451 |
if (radio && radio.checked) {
|
|
| 452 |
selected_index = index; |
|
| 453 |
throw $break; |
|
| 454 |
} |
|
| 455 |
}); |
|
| 456 |
/* hide unselected */ |
|
| 457 |
css_class_suffixes.each(function(css_class_suffix, index){
|
|
| 458 |
if (index != selected_index) {
|
|
| 459 |
$$('.' + css_class_base + css_class_suffix).each(
|
|
| 460 |
function(ele) { ele.hide(); });
|
|
| 461 |
} |
|
| 462 |
}) |
|
| 463 |
/* show selected */ |
|
| 464 |
if (0 <= selected_index) {
|
|
| 465 |
$$('.' + css_class_base + css_class_suffixes[selected_index]).each(
|
|
| 466 |
function(ele) { ele.show(); });
|
|
| 467 |
} |
|
| 468 |
} |
|
| 469 |
/* observe radio buttons */ |
|
| 470 |
id_suffixes.each(function(id_suffix, index){
|
|
| 471 |
radio = $(id_base + id_suffix) |
|
| 472 |
if (radio) {
|
|
| 473 |
Event.observe(radio, 'change', f); |
|
| 474 |
} |
|
| 475 |
}) |
|
| 476 |
f(); |
|
| 477 |
} |
|
| test/functional/issues_controller_transaction_test.rb (working copy) | ||
|---|---|---|
| 53 | 53 |
end |
| 54 | 54 | |
| 55 | 55 |
def test_put_update_stale_issue |
| 56 |
assert_put_update_stale_issue('')
|
|
| 57 |
assert_no_tag :tag => 'input', :attributes => { :id => 'conflict_resolution_notes' }
|
|
| 58 |
assert_tag :tag => 'input', :attributes => { :id => 'conflict_resolution_all', :checked => 'checked' }
|
|
| 59 |
end |
|
| 60 | ||
| 61 |
def test_put_update_stale_issue_with_notes |
|
| 62 |
assert_put_update_stale_issue('test notes')
|
|
| 63 |
assert_tag :tag => 'input', :attributes => { :id => 'conflict_resolution_notes', :checked => 'checked' }
|
|
| 64 |
assert_tag :tag => 'input', :attributes => { :id => 'conflict_resolution_all' }
|
|
| 65 |
end |
|
| 66 | ||
| 67 |
def assert_put_update_stale_issue(notes) |
|
| 56 | 68 |
issue = Issue.find(2) |
| 57 | 69 |
@request.session[:user_id] = 2 |
| 58 | 70 | |
| ... | ... | |
| 65 | 77 |
:fixed_version_id => 4, |
| 66 | 78 |
:lock_version => (issue.lock_version - 1) |
| 67 | 79 |
}, |
| 68 |
:notes => '',
|
|
| 80 |
:notes => notes,
|
|
| 69 | 81 |
:attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
|
| 70 | 82 |
:time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
|
| 71 | 83 |
end |
| ... | ... | |
| 73 | 85 |
end |
| 74 | 86 | |
| 75 | 87 |
assert_response :success |
| 76 |
assert_template 'edit'
|
|
| 88 |
assert_template 'conflict'
|
|
| 77 | 89 |
assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' },
|
| 78 | 90 |
:content => /Data has been updated by another user/ |
| 91 |
assert_tag :tag => 'input', :attributes => { :id => 'issue_lock_version', :value => issue.lock_version }
|
|
| 92 |
assert_tag :tag => 'legend', :content => I18n.t(:label_conflict_resolution) |
|
| 93 | ||
| 94 |
journals = issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
|
|
| 95 |
assert_tag :tag => 'input', :attributes => { :id => 'latest_journal', :value => journals.last.id }
|
|
| 96 |
journals.each{|journal|
|
|
| 97 |
assert_tag :tag => 'div', :attributes => { :id => "change-#{journal.id}" }
|
|
| 98 |
} |
|
| 99 |
end |
|
| 100 | ||
| 101 |
def test_conflict_resolution_none |
|
| 102 |
issue = Issue.find(2) |
|
| 103 |
@request.session[:user_id] = 2 |
|
| 104 |
|
|
| 105 |
assert_no_difference 'Journal.count' do |
|
| 106 |
assert_no_difference 'TimeEntry.count' do |
|
| 107 |
assert_no_difference 'Attachment.count' do |
|
| 108 |
put_conflict_resolution(issue, 'none') |
|
| 109 |
end |
|
| 110 |
end |
|
| 111 |
end |
|
| 112 |
end |
|
| 113 | ||
| 114 |
def test_conflict_resolution_notes |
|
| 115 |
issue = Issue.find(2) |
|
| 116 |
@request.session[:user_id] = 2 |
|
| 117 |
prev_status = issue.status |
|
| 118 |
|
|
| 119 |
assert_difference 'Journal.count' do |
|
| 120 |
assert_no_difference 'TimeEntry.count' do |
|
| 121 |
assert_no_difference 'Attachment.count' do |
|
| 122 |
put_conflict_resolution(issue, 'notes') |
|
| 123 |
end |
|
| 124 |
end |
|
| 125 |
end |
|
| 126 | ||
| 127 |
issue.reload |
|
| 128 |
assert_equal prev_status, issue.status |
|
| 129 |
end |
|
| 130 | ||
| 131 |
def test_conflict_resolution_all |
|
| 132 |
issue = Issue.find(2) |
|
| 133 |
@request.session[:user_id] = 2 |
|
| 134 |
prev_status = issue.status |
|
| 135 |
|
|
| 136 |
assert_difference 'Journal.count' do |
|
| 137 |
assert_difference 'TimeEntry.count' do |
|
| 138 |
assert_difference 'Attachment.count' do |
|
| 139 |
put_conflict_resolution(issue, 'all') |
|
| 140 |
end |
|
| 141 |
end |
|
| 142 |
end |
|
| 143 | ||
| 144 |
issue.reload |
|
| 145 |
assert_not_equal prev_status, issue.status |
|
| 146 |
end |
|
| 147 | ||
| 148 |
def put_conflict_resolution(issue, conflict_resolution) |
|
| 149 |
set_tmp_attachments_directory |
|
| 150 |
put :update, |
|
| 151 |
:id => issue.id, |
|
| 152 |
:issue => {
|
|
| 153 |
:status_id => 3, |
|
| 154 |
:fixed_version_id => 4, |
|
| 155 |
:lock_version => issue.lock_version, |
|
| 156 |
}, |
|
| 157 |
:notes => 'test notes', |
|
| 158 |
:attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
|
|
| 159 |
:time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id },
|
|
| 160 |
:conflict_resolution => conflict_resolution |
|
| 161 |
|
|
| 162 |
assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id |
|
| 79 | 163 |
end |
| 80 | 164 |
end |
| config/locales/en.yml (working copy) | ||
|---|---|---|
| 829 | 829 |
label_parent_revision: Parent |
| 830 | 830 |
label_child_revision: Child |
| 831 | 831 |
label_export_options: %{export_format} export options
|
| 832 |
label_conflict_resolution: Conflict Resolution |
|
| 833 |
label_conflict_resolution_none: "Throw away my changes, and show %{id}"
|
|
| 834 |
label_conflict_resolution_notes: Submit only my new notes |
|
| 835 |
label_conflict_resolution_all: Edit again |
|
| 836 |
label_conflict_history: Another user's updates |
|
| 832 | 837 | |
| 833 | 838 |
button_login: Login |
| 834 | 839 |
button_submit: Submit |
| ... | ... | |
| 878 | 883 |
button_show: Show |
| 879 | 884 |
button_edit_section: Edit this section |
| 880 | 885 |
button_export: Export |
| 886 |
button_conflict_resolution_none: "Show %{id}"
|
|
| 881 | 887 | |
| 882 | 888 |
status_active: active |
| 883 | 889 |
status_registered: registered |
| config/locales/ja.yml (working copy) | ||
|---|---|---|
| 837 | 837 |
label_git_report_last_commit: ファイルとディレクトリの最新コミットを表示する |
| 838 | 838 |
label_parent_revision: 親 |
| 839 | 839 |
label_child_revision: 子 |
| 840 |
label_conflict_resolution: 自分の更新データの反映方法 |
|
| 841 |
label_conflict_resolution_none: "自分の更新データを <strong>破棄</strong> し %{id} を表示する"
|
|
| 842 |
label_conflict_resolution_notes: 自分の <strong>注記のみ</strong> を反映する |
|
| 843 |
label_conflict_resolution_all: 自分の更新データを修正する |
|
| 844 |
label_conflict_history: 別のユーザによる更新内容 |
|
| 840 | 845 | |
| 841 | 846 |
button_login: ログイン |
| 842 | 847 |
button_submit: 変更 |
| ... | ... | |
| 884 | 889 |
button_quote: 引用 |
| 885 | 890 |
button_duplicate: 複製 |
| 886 | 891 |
button_show: 表示 |
| 892 |
button_conflict_resolution_none: "%{id} を表示"
|
|
| 887 | 893 | |
| 888 | 894 |
status_active: 有効 |
| 889 | 895 |
status_registered: 登録 |