Project

General

Profile

Feature #8691 » mid_air_collision_patch-20110131-trunk_r8743.patch

patch for trunk r8743 - Nayuta Taga, 2012-01-31 10:25

View differences:

app/models/issue.rb (working copy)
693 693
        rescue ActiveRecord::StaleObjectError
694 694
          attachments[:files].each(&:destroy)
695 695
          errors.add :base, l(:notice_locking_conflict)
696
          raise ActiveRecord::Rollback
696
          raise ActiveRecord::StaleObjectError
697 697
        end
698 698
      end
699 699
    end
app/controllers/issues_controller.rb (working copy)
107 107
  def show
108 108
    @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
109 109
    @journals.each_with_index {|j,i| j.indice = i+1}
110
    @latest_journal = @journals.last
110 111
    @journals.reverse! if User.current.wants_comments_in_reverse_order?
111 112

  
112 113
    if User.current.allowed_to?(:view_changesets, @project)
......
184 185
  end
185 186

  
186 187
  def update
188
    if params[:conflict_resolution].present?
189
      case params[:conflict_resolution]
190
      when 'none'
191
        redirect_to :controller => 'issues', :action => 'show', :id => @issue
192
        return
193
      when 'notes'
194
        # update notes only
195
        params.delete(:attachments)
196
        params.delete(:issue)
197
        params.delete(:time_entry)
198
      when 'all'
199
        ;
200
      end
201
    end
202
    
187 203
    update_issue_from_params
188 204

  
189
    if @issue.save_issue_with_child_records(params, @time_entry)
190
      render_attachment_warning_if_needed(@issue)
191
      flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192

  
193
      respond_to do |format|
194
        format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
195
        format.api  { head :ok }
205
    begin
206
      if @issue.save_issue_with_child_records(params, @time_entry)
207
        render_attachment_warning_if_needed(@issue)
208
        flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
209
        
210
        respond_to do |format|
211
          format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
212
          format.api  { head :ok }
213
        end
214
      else
215
        render_attachment_warning_if_needed(@issue)
216
        flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
217
        @journal = @issue.current_journal
218
        
219
        respond_to do |format|
220
          format.html { render :action => 'edit' }
221
          format.api  { render_validation_errors(@issue) }
222
        end
196 223
      end
197
    else
224
    rescue ActiveRecord::StaleObjectError
225
      # mid-air conflict
226
      @conflict_detected = true
227
      
228
      @issue.reload
229
      params[:issue].delete(:lock_version) if params[:issue]
230
      update_issue_from_params
231
      
232
      @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
233
      @journals.each_with_index {|j,i| j.indice = i+1}
234
      @latest_journal = @journals.last
235
      if params[:latest_journal].present?
236
        latest_journal_id = params[:latest_journal].to_i
237
        @journals = @journals.select{|journal| latest_journal_id < journal.id }
238
      end
239
      @journals.reverse! if User.current.wants_comments_in_reverse_order?
240

  
198 241
      render_attachment_warning_if_needed(@issue)
199 242
      flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
200 243
      @journal = @issue.current_journal
201

  
244
      
202 245
      respond_to do |format|
203
        format.html { render :action => 'edit' }
246
        format.html { render :action => 'conflict' }
204 247
        format.api  { render_validation_errors(@issue) }
205 248
      end
206 249
    end
app/views/issues/_edit.html.erb (working copy)
1 1
<% labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 2
    <%= error_messages_for 'issue', 'time_entry' %>
3 3
    <div class="box">
4
    <% if @conflict_detected %>
5
        <fieldset><legend><%= l(:label_conflict_resolution) %></legend>
6
            <label>
7
                <%= radio_button_tag 'conflict_resolution', 'none' %>
8
                <%= l(:label_conflict_resolution_none,
9
                      :id => (link_to "##{@issue.id}", :controller => 'issues', :action => 'show', :id => @issue)) %>
10
            </label>
11
            <br />
12
            <% if @notes.present? %>
13
            <label><%= radio_button_tag 'conflict_resolution', 'notes', true %> <%= l(:label_conflict_resolution_notes) %></label>
14
            <br />
15
            <% end %>
16
            <label><%= radio_button_tag 'conflict_resolution', 'all', @notes.blank? %> <%= l(:label_conflict_resolution_all) %></label>
17
        </fieldset>
18
    <% end %>
19
    <% if @latest_journal %>
20
        <%= hidden_field_tag 'latest_journal', @latest_journal.id %>
21
    <% end %>
22

  
4 23
    <% if @edit_allowed || !@allowed_statuses.empty? %>
5
        <fieldset class="tabular"><legend><%= l(:label_change_properties) %></legend>
24
        <fieldset class="tabular conflict_resolution_all"><legend><%= l(:label_change_properties) %></legend>
6 25
				<div id="all_attributes">
7 26
        <%= render :partial => 'form', :locals => {:f => f} %>
8 27
				</div>
9 28
        </fieldset>
10 29
    <% end %>
11 30
    <% if User.current.allowed_to?(:log_time, @project) %>
12
        <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
31
        <fieldset class="tabular conflict_resolution_all"><legend><%= l(:button_log_time) %></legend>
13 32
        <% labelled_fields_for :time_entry, @time_entry do |time_entry| %>
14 33
        <div class="splitcontentleft">
15 34
        <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
......
25 44
    </fieldset>
26 45
    <% end %>
27 46

  
28
    <fieldset><legend><%= l(:field_notes) %></legend>
47
    <fieldset class="conflict_resolution_all conflict_resolution_notes"><legend><%= l(:field_notes) %></legend>
29 48
    <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
30 49
    <%= wikitoolbar_for 'notes' %>
31 50
    <%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
32 51

  
33
    <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
52
    <p class="conflict_resolution_all"><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
34 53
    </fieldset>
35 54
    </div>
36 55

  
37 56
    <%= f.hidden_field :lock_version %>
38
    <%= submit_tag l(:button_submit) %>
57
    <%= submit_tag l(:button_submit), :class => 'conflict_resolution_notes conflict_resolution_all' %>
58
    <% if @conflict_detected %>
59
       <%= submit_tag l(:button_conflict_resolution_none, :id => "##{@issue.id}"), :class => 'conflict_resolution_none' %>
60
    <% end %>
39 61
    <%= link_to_remote l(:label_preview),
40 62
                       { :url => preview_issue_path(:project_id => @project, :id => @issue),
41 63
                         :method => 'post',
42 64
                         :update => 'preview',
43 65
                         :with => 'Form.serialize("issue-form")',
44 66
                         :complete => "Element.scrollTo('preview')"
45
                       }, :accesskey => accesskey(:preview) %>
67
                       }, :accesskey => accesskey(:preview), :class => 'conflict_resolution_notes conflict_resolution_all' %>
46 68
<% end %>
47 69

  
48
<div id="preview" class="wiki"></div>
70
<div id="preview" class="wiki conflict_resolution_notes conflict_resolution_all"></div>
71

  
72
<% if @conflict_detected %>
73
  <%= javascript_tag 'setupShowHideByRadio("conflict_resolution_", [ "none", "notes", "all" ])' %>
74
<% 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)
515 515
}
516 516

  
517 517
Event.observe(window, 'load', hideOnLoad);
518

  
519
/*
520
 * Setup observers of radio buttons to show/hide associated elements
521
 * automatically.
522
 *
523
 * id_base:
524
 *   A base name of radio button ids to observe.
525
 * id_suffixes:
526
 *   An array of radio button id suffixes to observe.
527
 *   id_base + id_suffixes[i] is an id.
528
 * css_class_base:
529
 *   A base name of css classes to show/hide.
530
 *   css_class_base + css_class_suffixes[i] is a class.
531
 *   When a radio button is clicked,
532
 *   its corresponding css class is shown, and the others are hidden.
533
 *   This parameter is an option, and defaults to id_base.
534
 * css_class_suffixes:
535
 *   An array of css class suffixes to show/hide.
536
 *   This parameter is an option, and defaults to id_suffixes.
537
 */
538
function setupShowHideByRadio(
539
  id_base, id_suffixes, css_class_base, css_class_suffixes) {
540
  if (css_class_base === undefined) {
541
    css_class_base = id_base;
542
  }
543
  if (css_class_suffixes === undefined) {
544
    css_class_suffixes = id_suffixes;
545
  }
546
  function f() {
547
    /* find a checked radio button */
548
    selected_index = -1;
549
    id_suffixes.each(function(id_suffix, index){
550
      radio = $(id_base + id_suffix);
551
      if (radio && radio.checked) {
552
        selected_index = index;
553
        throw $break;
554
      }
555
    });
556
    /* hide unselected */
557
    css_class_suffixes.each(function(css_class_suffix, index){
558
      if (index != selected_index) {
559
        $$('.' + css_class_base + css_class_suffix).each(
560
          function(ele) { ele.hide(); });
561
      }
562
    })
563
    /* show selected */
564
    if (0 <= selected_index) {
565
      $$('.' + css_class_base + css_class_suffixes[selected_index]).each(
566
        function(ele) { ele.show(); });
567
    }
568
  }
569
  /* observe radio buttons */
570
  id_suffixes.each(function(id_suffix, index){
571
    radio = $(id_base + id_suffix)
572
    if (radio) {
573
      Event.observe(radio, 'change', f);
574
    }
575
  })
576
  f();
577
}
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

  
165

  
81 166
  def test_index_should_rescue_invalid_sql_query
82 167
    Query.any_instance.stubs(:statement).returns("INVALID STATEMENT")
83 168

  
config/locales/en.yml (working copy)
840 840
  label_copy_attachments: Copy attachments
841 841
  label_item_position: %{position} of %{count}
842 842
  label_completed_versions: Completed versions
843
  label_conflict_resolution: Conflict Resolution
844
  label_conflict_resolution_none: "Throw away my changes, and show %{id}"
845
  label_conflict_resolution_notes: Submit only my new notes
846
  label_conflict_resolution_all: Edit again
847
  label_conflict_history: Another user's updates
843 848

  
844 849
  button_login: Login
845 850
  button_submit: Submit
......
889 894
  button_show: Show
890 895
  button_edit_section: Edit this section
891 896
  button_export: Export
897
  button_conflict_resolution_none: "Show %{id}"
892 898

  
893 899
  status_active: active
894 900
  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: 登録
(2-2/5)