Project

General

Profile

Feature #13315 » 13315.patch

Yuichi HARADA, 2021-10-12 08:23

View differences:

app/controllers/context_menus_controller.rb
20 20
class ContextMenusController < ApplicationController
21 21
  helper :watchers
22 22
  helper :issues
23
  helper :projects
23 24

  
24 25
  before_action :find_issues, :only => :issues
25 26

  
......
95 96

  
96 97
    render :layout => false
97 98
  end
99

  
100
  def versions
101
    @versions = Version.where(:id => params[:ids]).preload(:project).to_a
102

  
103
    (render_404; return) unless @versions.present?
104
    if @versions.size == 1
105
      @version = @versions.first
106
    end
107

  
108
    @version_ids = @versions.collect(&:id).sort
109

  
110
    @allowed_statuses = Version::VERSION_STATUSES
111
    @allowed_sharings = Version::VERSION_SHARINGS
112

  
113
    projects = @versions.collect(&:project).compact.uniq
114
    project = projects.first if projects.size == 1
115
    edit_allowed =
116
      if project
117
        User.current.allowed_to?(:manage_versions, project) && @versions.all?{|version| version.project == project}
118
      else
119
        false
120
      end
121
    @can = {:edit => edit_allowed, :delete => edit_allowed}
122
    @back = back_url
123

  
124
    render :layout => false
125
  end
98 126
end
app/controllers/versions_controller.rb
20 20
class VersionsController < ApplicationController
21 21
  menu_item :roadmap
22 22
  model_object Version
23
  before_action :find_model_object, :except => [:index, :new, :create, :close_completed]
24
  before_action :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23
  before_action :find_model_object, :except => [:index, :new, :create, :close_completed, :destroy, :bulk_update]
24
  before_action :find_project_from_association, :except => [:index, :new, :create, :close_completed, :destroy, :bulk_update]
25 25
  before_action :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
26
  before_action :find_versions, :only => [:destroy, :bulk_update]
26 27
  before_action :authorize
27 28

  
28 29
  accept_api_auth :index, :show, :create, :update, :destroy
......
149 150
  end
150 151

  
151 152
  def destroy
152
    if @version.deletable?
153
      @version.destroy
154
      respond_to do |format|
155
        format.html {redirect_back_or_default settings_project_path(@project, :tab => 'versions')}
156
        format.api  {render_api_ok}
153
    destroyed = Version.transaction do
154
      @versions.each do |v|
155
        unless v.deletable?
156
          raise ActiveRecord::Rollback
157
        end
158
        unless v.destroy && v.destroyed?
159
          raise ActiveRecord::Rollback
160
        end
157 161
      end
158
    else
159
      respond_to do |format|
160
        format.html do
162
    end
163

  
164
    respond_to do |format|
165
      format.html do
166
        if destroyed
167
          flash[:notice] = l(:notice_successful_delete)
168
        else
161 169
          flash[:error] = l(:notice_unable_delete_version)
162
          redirect_to settings_project_path(@project, :tab => 'versions')
163 170
        end
164
        format.api  {head :unprocessable_entity}
171
        redirect_back_or_default settings_project_path(@project, :tab => 'versions')
172
      end
173
      format.api do
174
        if destroyed
175
          render_api_ok
176
        else
177
          head :unprocessable_entity
178
        end
165 179
      end
166 180
    end
167 181
  end
......
173 187
    end
174 188
  end
175 189

  
190
  def bulk_update
191
    attributes = parse_params_for_bulk_update(params[:version])
192

  
193
    unsaved_versions = []
194
    saved_versions = []
195

  
196
    Version.transaction do
197
      @versions.each do |version|
198
        version.reload
199
        attrs = attributes.dup
200
        attrs.delete('sharing') unless version.allowed_sharings.include?(attrs['sharing'])
201
        version.safe_attributes = attrs
202
        next unless version.changed?
203

  
204
        call_hook(
205
          :controller_versions_bulk_update_before_save,
206
          {:params => params, :version => version}
207
        )
208
        if version.save
209
          saved_versions << version
210
        else
211
          flash[:error] = version.errors.full_messages.join(', ') unless flash[:error]
212
          unsaved_versions << version
213
          raise ActiveRecord::Rollback
214
        end
215
      end
216
    end
217

  
218
    if unsaved_versions.empty? && saved_versions.present?
219
      flash[:notice] = l(:notice_successful_update)
220
    end
221
    redirect_back_or_default settings_project_path(@project, :tab => :versions)
222
  end
223

  
176 224
  private
177 225

  
226
  def find_versions
227
    @versions = Version.where(:id => params[:id] || params[:ids]).preload(:project).to_a
228
    raise ActiveRecord::RecordNotFound if @versions.empty?
229

  
230
    projects = @versions.collect(&:project).compact.uniq
231
    @project = projects.first if projects.size == 1
232
  rescue ActiveRecord::RecordNotFound
233
    render_404
234
  end
235

  
178 236
  def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
179 237
    if ids = params[:tracker_ids]
180 238
      @selected_tracker_ids =
app/helpers/routes_helper.rb
97 97
    end
98 98
  end
99 99

  
100
  def _bulk_update_versions_path(version, *args)
101
    if version
102
      version_path(version, *args)
103
    else
104
      bulk_update_versions_path(*args)
105
    end
106
  end
107

  
100 108
  def board_path(board, *args)
101 109
    project_board_path(board.project, board, *args)
102 110
  end
app/views/context_menus/versions.html.erb
1
<ul>
2
  <%= call_hook(:view_versions_context_menu_start, {:versions => @versions, :can => @can, :back => @back }) %>
3

  
4
<% if @version -%>
5
  <li><%= context_menu_link l(:button_edit), edit_version_path(@version),
6
                            :class => 'icon icon-edit', :disabled => !@can[:edit] %></li>
7
<% end -%>
8

  
9
<% if @allowed_statuses.present? -%>
10
  <li class="folder">
11
    <a href="#" class="submenu"><%= l(:field_status) %></a>
12
    <ul>
13
  <% @allowed_statuses.each do |s| -%>
14
      <li>
15
        <%= context_menu_link(
16
              l("version_status_#{s}"),
17
              _bulk_update_versions_path(
18
                @version,
19
                :ids => @version_ids, :version => {:status => s}, :back_url => @back
20
              ),
21
              :method => :patch,
22
              :selected => (@version && s == @version.status), :disabled => !@can[:edit]
23
            ) %>
24
        </li>
25
  <% end -%>
26
    </ul>
27
  </li>
28
<% end -%>
29

  
30
<% if @allowed_sharings.present? -%>
31
  <li class="folder">
32
    <a href="#" class="submenu"><%= l(:field_sharing) %></a>
33
    <ul>
34
  <% @allowed_sharings.each do |s| -%>
35
      <li>
36
        <%= context_menu_link(
37
              format_version_sharing(s),
38
              _bulk_update_versions_path(
39
                @version,
40
                :ids => @version_ids, :version => {:sharing => s}, :back_url => @back
41
              ),
42
              :method => :patch,
43
              :selected => (@version && s == @version.sharing), :disabled => !@can[:edit]
44
            ) %>
45
        </li>
46
  <% end -%>
47
    </ul>
48
  </li>
49
<% end -%>
50

  
51
  <li><%= context_menu_link l(:button_delete), versions_path(:ids => @version_ids, :back_url => @back),
52
                            :method => :delete, :data => {:confirm => l(:text_versions_destroy_confirmation)}, :class => 'icon icon-del', :disabled => !@can[:delete] %></li>
53

  
54
  <%= call_hook(:view_versions_context_menu_end, {:versions => @versions, :can => @can, :back => @back }) %>
55
</ul>
app/views/projects/settings.html.erb
3 3
<%= render_tabs project_settings_tabs %>
4 4

  
5 5
<% html_title(l(:label_settings)) -%>
6

  
7
<%= context_menu %>
app/views/projects/settings/_versions.html.erb
20 20
&nbsp;
21 21

  
22 22
<% if @versions.present? %>
23
<%= form_tag({}, :data => {:cm_url => versions_context_menu_path}) do %>
24
  <% is_allowed_manage_versions = User.current.allowed_to?(:manage_versions, @project) %>
23 25
<table class="list versions">
24 26
  <thead><tr>
27
    <th class="checkbox hide-when-print">
28
  <% if is_allowed_manage_versions && @versions.any?{|version| version.project == @project} %>
29
      <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
30
            :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
31
  <% end %>
32
    </th>
25 33
    <th><%= l(:label_version) %></th>
26 34
    <th><%= l(:field_default_version) %></th>
27 35
    <th><%= l(:field_effective_date) %></th>
......
33 41
    </tr></thead>
34 42
  <tbody>
35 43
<% @versions.each do |version| %>
36
    <tr class="version <%=h version.status %> <%= 'shared' if version.project != @project %>">
44
  <% is_manage_version = (version.project == @project && is_allowed_manage_versions) %>
45
    <tr class="version <%=h version.status %><%= ' shared' if version.project != @project %><%= ' hascontextmenu' if is_manage_version %>">
46
    <td class="checkbox hide-when-print">
47
  <% if is_manage_version %>
48
      <%= check_box_tag("ids[]", version.id, false, :id => nil) %>
49
  <% end %>
50
    </td>
37 51
    <td class="name <%= 'icon icon-shared' if version.project != @project %>"><%= link_to_version version %></td>
38 52
    <td class="tick"><%= checked_image(version.id == @project.default_version_id) %></td>
39 53
    <td class="date"><%= format_date(version.effective_date) %></td>
......
42 56
    <td class="sharing"><%=h format_version_sharing(version.sharing) %></td>
43 57
    <td><%= link_to_if_authorized(version.wiki_page_title, {:controller => 'wiki', :action => 'show', :project_id => version.project, :id => Wiki.titleize(version.wiki_page_title)}) || h(version.wiki_page_title) unless version.wiki_page_title.blank? || version.project.wiki.nil? %></td>
44 58
    <td class="buttons">
45
      <% if version.project == @project && User.current.allowed_to?(:manage_versions, @project) %>
59
      <% if is_manage_version %>
46 60
        <%= link_to l(:button_edit), edit_version_path(version), :class => 'icon icon-edit' %>
47 61
        <%= delete_link version_path(version) %>
48 62
      <% end %>
......
51 65
<% end %>
52 66
    </tbody>
53 67
</table>
68
<% end %>
54 69
<% else %>
55 70
<p class="nodata"><%= l(:label_no_data) %></p>
56 71
<% end %>
config/locales/en.yml
1288 1288
  text_no_subject: no subject
1289 1289
  text_allowed_queries_to_select: Public (to any users) queries only selectable
1290 1290
  text_setting_config_change: You can configure the behaviour in config/configuration.yml. Please restart the application after editing it.
1291
  text_versions_destroy_confirmation: 'Are you sure you want to delete the selected version(s)?'
1291 1292

  
1292 1293
  default_role_manager: Manager
1293 1294
  default_role_developer: Developer
config/routes.rb
233 233
  match '/news/:id/comments', :to => 'comments#create', :via => :post
234 234
  match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
235 235

  
236
  match '/versions/context_menu', :to => 'context_menus#versions', :as => :versions_context_menu, :via => [:get, :post]
237
  delete '/versions', :to => 'versions#destroy'
236 238
  resources :versions, :only => [:show, :edit, :update, :destroy] do
237 239
    post 'status_by', :on => :member
240
    collection do
241
      patch 'bulk_update'
242
    end
238 243
  end
239 244

  
240 245
  resources :documents, :only => [:show, :edit, :update, :destroy] do
lib/redmine.rb
94 94
  map.permission :select_project_modules, {:projects => :modules}, :require => :member
95 95
  map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
96 96
  map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :edit, :update, :destroy, :autocomplete]}, :require => :member
97
  map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
97
  map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy, :bulk_update]}, :require => :member
98 98
  map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
99 99
  # Queries
100 100
  map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
test/functional/context_menus_controller_test.rb
470 470

  
471 471
    assert_select 'a.disabled', :text => 'Edit'
472 472
  end
473

  
474
  def test_context_menu_one_version_should_link_to_version_path
475
    @request.session[:user_id] = 2
476
    get(
477
      :versions,
478
      :params => {
479
        :ids => [2]
480
      }
481
    )
482
    assert_response :success
483

  
484
    assert_select 'a.icon-edit[href=?]', '/versions/2/edit', :text => 'Edit'
485
    assert_select 'a.icon-del[href=?]', '/versions?ids%5B%5D=2', :text => 'Delete'
486

  
487
    # Statuses
488
    assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bstatus%5D=open', :text => 'open'
489
    assert_select 'a.icon-checked.disabled[href=?]', '#', :text => 'locked'
490
    assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bstatus%5D=closed', :text => 'closed'
491
    # Sharings
492
    assert_select 'a.icon-checked.disabled[href=?]', '#', :text => 'Not shared'
493
    assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=descendants', :text => 'With subprojects'
494
    assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=hierarchy', :text => 'With project hierarchy'
495
    assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=tree', :text => 'With project tree'
496
    assert_select 'a[href=?][data-method="patch"]', '/versions/2?ids%5B%5D=2&version%5Bsharing%5D=system', :text => 'With all projects'
497
  end
498

  
499
  def test_context_menu_multiple_versions_should_link_to_bulk_update_versions_path
500
    @request.session[:user_id] = 2
501
    get(
502
      :versions,
503
      :params => {
504
        :ids => [2, 3]
505
      }
506
    )
507
    assert_response :success
508

  
509
    assert_select 'a.icon-edit', :text => 'Edit', :count => 0
510
    assert_select 'a.icon-del[href=?]', '/versions?ids%5B%5D=2&ids%5B%5D=3', :text => 'Delete'
511

  
512
    # Statuses
513
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bstatus%5D=open', :text => 'open'
514
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bstatus%5D=locked', :text => 'locked'
515
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bstatus%5D=closed', :text => 'closed'
516
    # Sharings
517
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=none', :text => 'Not shared'
518
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=descendants', :text => 'With subprojects'
519
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=hierarchy', :text => 'With project hierarchy'
520
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=tree', :text => 'With project tree'
521
    assert_select 'a[href=?][data-method="patch"]', '/versions/bulk_update?ids%5B%5D=2&ids%5B%5D=3&version%5Bsharing%5D=system', :text => 'With all projects'
522
  end
473 523
end
test/functional/projects_controller_test.rb
867 867
    end
868 868
  end
869 869

  
870
  def test_versions_in_settings_should_show_context_menu
871
    @request.session[:user_id] = 2
872
    get(
873
      :settings,
874
      :params => {
875
        :id => 'ecookbook',
876
        :tab => 'versions',
877
        :version_status => '',
878
        :version_name => '.1',
879
      }
880
    )
881
    assert_response :success
882
    assert_select 'table.versions' do
883
      assert_select 'thead' do
884
        assert_select 'th.checkbox input[type=checkbox][name=?]', 'check_all', 1
885
      end
886
      assert_select 'tbody tr.hascontextmenu' do
887
        assert_select 'td.checkbox input[type=checkbox][name=?]', 'ids[]', 1
888
        assert_select 'td.name', :text => '0.1'
889
      end
890
    end
891
  end
892

  
893
  def test_versions_in_settings_with_version_shared_by_other_project_should_not_show_context_menu
894
    @request.session[:user_id] = 2
895
    get(
896
      :settings,
897
      :params => {
898
        :id => 'ecookbook',
899
        :tab => 'versions',
900
        :version_status => '',
901
        :version_name => 'subproject',
902
      }
903
    )
904
    assert_response :success
905
    assert_select 'table.versions' do
906
      assert_select 'thead' do
907
        assert_select 'th.checkbox input[type=checkbox][name=?]', 'check_all', 0
908
      end
909
      assert_select 'tbody tr.shared' do
910
        assert_select 'td.checkbox input[type=checkbox][name=?]', 'ids[]', 0
911
        assert_select 'td.name', :text => 'Private child of eCookbook - Private Version of public subproject'
912
      end
913
    end
914
  end
915

  
870 916
  def test_settings_should_show_locked_members
871 917
    user = User.generate!
872 918
    member = User.add_to_project(user, Project.find(1))
test/functional/versions_controller_test.rb
348 348
    assert_include 'Assigned', response.body
349 349
    assert_include 'Closed', response.body
350 350
  end
351

  
352
  def test_bulk_update_on_status
353
    ids = [2, 3]
354
    update_attr = {:status => 'closed'}
355
    assert_equal 2, Version.where(:id => ids).where.not(update_attr).count
356

  
357
    @request.session[:user_id] = 2
358
    patch :bulk_update, :params => {
359
      :ids => ids,
360
      :version => update_attr
361
    }
362
    assert_redirected_to :controller => :projects, :action => :settings,
363
                        :tab => :versions, :id => :ecookbook
364
    assert_equal 'Successful update.', flash[:notice]
365

  
366
    assert_equal 2, Version.where(:id => ids).where(update_attr).count
367
  end
368

  
369
  def test_bulk_update_on_sharing
370
    ids = [2, 3]
371
    update_attr = {:sharing => 'descendants'}
372
    assert_equal 2, Version.where(:id => ids).where.not(update_attr).count
373

  
374
    @request.session[:user_id] = 2
375
    patch :bulk_update, :params => {
376
      :ids => ids,
377
      :version => update_attr
378
    }
379
    assert_redirected_to :controller => :projects, :action => :settings,
380
                        :tab => :versions, :id => :ecookbook
381
    assert_equal 'Successful update.', flash[:notice]
382

  
383
    assert_equal 2, Version.where(:id => ids).where(update_attr).count
384
  end
385

  
386
  def test_bulk_update_with_validation_failure
387
    ids = [2, 3]
388
    versions = Version.where(:id => ids).reorder(:id => :asc)
389
    assert_equal 2, versions.count
390

  
391
    @request.session[:user_id] = 2
392
    patch :bulk_update, :params => {
393
      :ids => ids,
394
      :version => {
395
        :status => 'invalid'
396
      }
397
    }
398
    assert_redirected_to :controller => :projects, :action => :settings,
399
                        :tab => :versions, :id => :ecookbook
400
    assert_equal 'Status is not included in the list', flash[:error]
401

  
402
    assert_equal versions, Version.where(:id => ids).reorder(:id => :asc)
403
  end
404

  
405
  def test_bulk_destroy
406
    Issue.update(:fixed_version_id => nil)
407
    @request.session[:user_id] = 2
408
    assert_difference 'Version.count', -2 do
409
      delete :destroy, :params => {:ids => [2, 3]}
410
    end
411
    assert_redirected_to :controller => :projects, :action => :settings,
412
                         :tab => :versions, :id => :ecookbook
413
    assert_equal 'Successful deletion.', flash[:notice]
414
  end
415

  
416
  def test_bulk_destroy_with_version_in_use_should_fail
417
    @request.session[:user_id] = 2
418
    assert_no_difference 'Version.count' do
419
      delete :destroy, :params => {:ids => [2, 3]}
420
    end
421
    assert_redirected_to :controller => :projects, :action => :settings,
422
                         :tab => :versions, :id => :ecookbook
423
    assert_equal 'Unable to delete version.', flash[:error]
424
  end
351 425
end
test/helpers/routes_helper_test.rb
20 20
require File.expand_path('../../test_helper', __FILE__)
21 21

  
22 22
class RoutesHelperTest < Redmine::HelperTest
23
  fixtures :projects, :issues
23
  fixtures :projects, :issues, :versions
24 24

  
25 25
  include Rails.application.routes.url_helpers
26 26

  
......
47 47
    assert_equal 'http://test.host/projects/ecookbook/issues?set_filter=1', _project_issues_url(Project.find(1), set_filter: 1)
48 48
    assert_equal 'http://test.host/issues?set_filter=1', _project_issues_url(nil, set_filter: 1)
49 49
  end
50

  
51
  def test_bulk_update_versions_path
52
    assert_equal '/versions/bulk_update', _bulk_update_versions_path(nil, nil)
53
    assert_equal '/versions/1', _bulk_update_versions_path(Version.find(1), nil)
54
  end
50 55
end
test/integration/routing/context_menus_test.rb
29 29
    should_route 'GET /issues/context_menu' => 'context_menus#issues'
30 30
    should_route 'POST /issues/context_menu' => 'context_menus#issues'
31 31
  end
32

  
33
  def test_context_menus_versions
34
    should_route 'GET /versions/context_menu' => 'context_menus#versions'
35
    should_route 'POST /versions/context_menu' => 'context_menus#versions'
36
  end
32 37
end
test/integration/routing/versions_test.rb
35 35

  
36 36
    should_route 'POST /versions/1/status_by' => 'versions#status_by', :id => '1'
37 37
  end
38

  
39
  def test_versions_bulk_edit
40
    should_route 'PATCH /versions/bulk_update' => 'versions#bulk_update'
41
    should_route 'DELETE /versions' => 'versions#destroy'
42
  end
38 43
end
(2-2/2)