Project

General

Profile

Feature #8691 » mid_air_collision_patch-20110131-tags_1.3.0.patch

patch for tags/1.3.0 - Nayuta Taga, 2012-01-31 10:25

View differences:

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: 登録
(3-3/5)