Project

General

Profile

Feature #44166 » tracker_lock_feature.patch

Seiji Shozuki, 2026-06-12 12:57

View differences:

app/controllers/projects_controller.rb
96 96

  
97 97
  def new
98 98
    @issue_custom_fields = IssueCustomField.sorted.to_a
99
    @trackers = Tracker.sorted.to_a
99
    @trackers = Tracker.active.sorted.to_a
100 100
    @project = Project.new
101 101
    @project.safe_attributes = params[:project]
102 102
  end
103 103

  
104 104
  def create
105 105
    @issue_custom_fields = IssueCustomField.sorted.to_a
106
    @trackers = Tracker.sorted.to_a
106
    @trackers = Tracker.active.sorted.to_a
107 107
    @project = Project.new
108 108
    @project.safe_attributes = params[:project]
109 109

  
......
140 140

  
141 141
  def copy
142 142
    @issue_custom_fields = IssueCustomField.sorted.to_a
143
    @trackers = Tracker.sorted.to_a
143
    @trackers = Tracker.active.sorted.to_a
144 144
    @source_project = Project.find(params[:id])
145 145
    if request.get?
146 146
      @project = Project.copy_from(@source_project)
......
201 201
    @issue_custom_fields = IssueCustomField.sorted.to_a
202 202
    @issue_category ||= IssueCategory.new
203 203
    @member ||= @project.members.new
204
    @trackers = Tracker.sorted.to_a
204
    # Show active trackers plus any inactive trackers already assigned to this project
205
    @trackers = Tracker.sorted.where(:active => true).or(
206
      Tracker.sorted.where(:id => @project.tracker_ids)
207
    ).sorted.to_a
205 208

  
206 209
    @version_status = params[:version_status] || 'open'
207 210
    @version_name = params[:version_name]
app/controllers/trackers_controller.rb
86 86
    end
87 87
  end
88 88

  
89
  def lock
90
    @tracker = Tracker.find(params[:id])
91
    @tracker.update!(:active => false)
92
    redirect_to trackers_path
93
  end
94

  
95
  def unlock
96
    @tracker = Tracker.find(params[:id])
97
    @tracker.update!(:active => true)
98
    redirect_to trackers_path
99
  end
100

  
89 101
  def destroy
90 102
    @tracker = Tracker.find(params[:id])
91 103
    if @tracker.issues.empty?
app/models/issue.rb
1695 1695
  # Returns a scope of trackers that user can assign project issues to
1696 1696
  def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1697 1697
    if project
1698
      scope = project.trackers.sorted
1698
      scope = project.trackers.sorted.active
1699
      # Always include the current tracker even if it is inactive
1700
      if current_tracker
1701
        scope = Tracker.sorted.where(
1702
          :id => scope.pluck(:id) | [current_tracker]
1703
        )
1704
      end
1699 1705
      unless user.admin?
1700 1706
        roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1701 1707
        unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
......
1810 1816
        copy.author = author
1811 1817
        copy.project = project
1812 1818
        copy.parent_issue_id = copied_issue_ids[child.parent_id]
1813
        unless child.fixed_version.present? && child.fixed_version.status == 'open'
1819
        if fixed_version_id != @copied_from.fixed_version_id
1820
          copy.fixed_version_id = fixed_version_id
1821
        elsif !(child.fixed_version.present? && child.fixed_version.status == 'open')
1814 1822
          copy.fixed_version_id = nil
1815 1823
        end
1816 1824
        unless child.assigned_to_id.present? &&
app/models/project.rb
139 139
    if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
140 140
      default = Setting.default_projects_tracker_ids
141 141
      if default.is_a?(Array)
142
        self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
142
        self.trackers = Tracker.where(:id => default.map(&:to_i)).active.sorted.to_a
143 143
      else
144
        self.trackers = Tracker.sorted.to_a
144
        self.trackers = Tracker.active.sorted.to_a
145 145
      end
146 146
    end
147 147
    # rubocop:enable Style/NegatedIf
app/models/tracker.rb
46 46

  
47 47
  scope :sorted, lambda {order(:position)}
48 48
  scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
49
  scope :active, lambda {where(:active => true)}
49 50

  
50 51
  # Returns the trackers that are visible by the user.
51 52
  #
......
75 76
    'default_status_id',
76 77
    'is_in_roadmap',
77 78
    'private_by_default',
79
    'active',
78 80
    'core_fields',
79 81
    'position',
80 82
    'custom_field_ids',
app/views/trackers/index.html.erb
11 11
  <th><%=l(:field_default_status)%></th>
12 12
  <th><%=l(:field_description)%></th>
13 13
  <th></th>
14
  <th></th>
14
  <th class="tracker-buttons-col"></th>
15 15
  </tr></thead>
16 16
  <tbody>
17 17
<% for tracker in @trackers %>
18
  <tr>
18
  <tr class="<%= tracker.active? ? '' : 'tracker-locked' %>">
19 19
  <td class="name"><%= link_to tracker.name, edit_tracker_path(tracker) %></td>
20 20
  <td><%= tracker.default_status.name %></td>
21 21
  <td class="description"><%= tracker.description %></td>
......
26 26
      </span>
27 27
    <% end %>
28 28
  </td>
29
  <td class="buttons">
29
  <td class="buttons tracker-buttons-col">
30 30
    <%= reorder_handle(tracker) %>
31 31
    <%= link_to sprite_icon('copy', l(:button_copy)), new_tracker_path(:copy => tracker), :class => 'icon icon-copy' %>
32
    <% if tracker.active? %>
33
      <%= link_to sprite_icon('lock', l(:button_lock)), lock_tracker_path(tracker), :method => :put, :class => 'icon icon-lock' %>
34
    <% else %>
35
      <%= link_to sprite_icon('unlock', l(:button_unlock)), unlock_tracker_path(tracker), :method => :put, :class => 'icon icon-unlock' %>
36
    <% end %>
32 37
    <%= delete_link tracker_path(tracker) %>
33 38
  </td>
34 39
  </tr>
......
36 41
  </tbody>
37 42
</table>
38 43

  
44
<style>
45
  tr.tracker-locked td { opacity: 0.6; }
46
  table.trackers th.tracker-buttons-col,
47
  table.trackers td.tracker-buttons-col { width: 1%; white-space: nowrap; }
48
  table.trackers td.tracker-buttons-col a.icon-lock,
49
  table.trackers td.tracker-buttons-col a.icon-unlock { display: inline-block; min-width: 82px; }
50
</style>
51

  
39 52
<% html_title(l(:label_tracker_plural)) -%>
40 53

  
41 54
<%= javascript_tag do %>
config/locales/en.yml
672 672
  label_tracker_plural: Trackers
673 673
  label_tracker_all: All trackers
674 674
  label_tracker_new: New tracker
675
  label_tracker_locked: Locked
675 676
  label_workflow: Workflow
676 677
  label_issue_status: Issue status
677 678
  label_issue_status_plural: Issue statuses
config/locales/ja.yml
1201 1201
  label_no_preview: このファイルはプレビューできません
1202 1202
  error_no_tracker_allowed_for_new_issue_in_project: このプロジェクトにはチケットの追加が許可されているトラッカーがありません
1203 1203
  label_tracker_all: すべてのトラッカー
1204
  label_tracker_locked: ロック済み
1204 1205
  label_new_project_issue_tab_enabled: '"新しいチケット" タブを表示'
1205 1206
  setting_new_item_menu_tab: 新規オブジェクト作成タブ
1206 1207
  label_new_object_tab_enabled: '"+" ドロップダウンを表示'
config/routes.rb
350 350
  delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
351 351

  
352 352
  resources :trackers, :except => :show do
353
    member do
354
      put 'lock'
355
      put 'unlock'
356
    end
353 357
    collection do
354 358
      match 'fields', :via => [:get, :post]
355 359
    end
db/migrate/20260612000000_add_active_to_trackers.rb
1
class AddActiveToTrackers < ActiveRecord::Migration[8.1]
2
  def change
3
    add_column :trackers, :active, :boolean, :default => true, :null => false
4
  end
5
end
test/fixtures/trackers.yml
4 4
  id: 1
5 5
  default_status_id: 1
6 6
  position: 1
7
  active: true
7 8
  description: Description for Bug tracker
8 9
trackers_002:
9 10
  name: Feature request
10 11
  id: 2
11 12
  default_status_id: 1
12 13
  position: 2
14
  active: true
13 15
  description: Description for Feature request tracker
14 16
trackers_003:
15 17
  name: Support request
16 18
  id: 3
17 19
  default_status_id: 1
18 20
  position: 3
21
  active: true
test/functional/trackers_controller_test.rb
304 304
    end
305 305
  end
306 306

  
307
  def test_lock
308
    tracker = Tracker.find(1)
309
    assert tracker.active?
310
    put :lock, :params => {:id => 1}
311
    assert_redirected_to :action => 'index'
312
    assert_not tracker.reload.active?
313
  end
314

  
315
  def test_unlock
316
    tracker = Tracker.find(1)
317
    tracker.update!(active: false)
318
    put :unlock, :params => {:id => 1}
319
    assert_redirected_to :action => 'index'
320
    assert tracker.reload.active?
321
  end
322

  
323
  def test_lock_requires_admin
324
    @request.session[:user_id] = 2
325
    put :lock, :params => {:id => 1}
326
    assert_response :forbidden
327
  end
328

  
329
  def test_index_shows_lock_button_for_active_tracker
330
    get :index
331
    assert_response :success
332
    assert_select 'a[href=?]', lock_tracker_path(1)
333
  end
334

  
335
  def test_index_shows_unlock_button_for_locked_tracker
336
    Tracker.find(1).update!(active: false)
337
    get :index
338
    assert_response :success
339
    assert_select 'a[href=?]', unlock_tracker_path(1)
340
  end
341

  
307 342
  def test_post_fields
308 343
    post :fields, :params => {
309 344
      :trackers => {
test/unit/issue_test.rb
1539 1539
    assert_equal [3, nil], copy.children.map(&:fixed_version_id)
1540 1540
  end
1541 1541

  
1542
  def test_copy_should_propagate_version_change_to_subtasks
1543
    parent = Issue.generate!(:fixed_version_id => 3)
1544
    child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1', :fixed_version_id => 3)
1545
    child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2', :fixed_version_id => nil)
1546

  
1547
    new_version = Version.generate!(:project => Project.find(1))
1548
    copy = parent.reload.copy(:fixed_version_id => new_version.id)
1549

  
1550
    assert_difference 'Issue.count', 3 do
1551
      assert copy.save
1552
    end
1553

  
1554
    assert_equal new_version.id, copy.fixed_version_id
1555
    assert_equal [new_version.id, new_version.id], copy.children.map(&:fixed_version_id)
1556
  end
1557

  
1542 1558
  def test_copy_should_clear_subtasks_assignee_if_is_locked
1543 1559
    user = User.find(2)
1544 1560

  
......
1909 1925
    assert_equal [], issue.allowed_target_trackers(User.find(2)).ids
1910 1926
  end
1911 1927

  
1928
  def test_allowed_target_trackers_excludes_locked_trackers
1929
    Tracker.find(1).update!(active: false)
1930
    issue = Issue.new(:project => Project.find(1))
1931
    ids = Issue.allowed_target_trackers(Project.find(1), User.find(1)).ids
1932
    assert_not_includes ids, 1
1933
    assert_includes ids, 2
1934
    assert_includes ids, 3
1935
  end
1936

  
1937
  def test_allowed_target_trackers_includes_current_tracker_even_if_locked
1938
    tracker = Tracker.find(1)
1939
    tracker.update!(active: false)
1940
    issue = Issue.generate!(:project => Project.find(1), :tracker => tracker)
1941
    ids = issue.allowed_target_trackers(User.find(1)).ids
1942
    assert_includes ids, 1
1943
  end
1944

  
1912 1945
  def test_allowed_target_trackers_should_include_current_tracker
1913 1946
    user = User.generate!
1914 1947
    role = Role.generate!
test/unit/tracker_test.rb
167 167
    assert tracker.respond_to?(:description)
168 168
    assert_equal tracker.description, "Description for Bug tracker"
169 169
  end
170

  
171
  def test_active_scope_returns_only_active_trackers
172
    Tracker.find(1).update!(active: false)
173
    active_ids = Tracker.active.pluck(:id)
174
    assert_not_includes active_ids, 1
175
    assert_includes active_ids, 2
176
    assert_includes active_ids, 3
177
  end
178

  
179
  def test_tracker_is_active_by_default
180
    tracker = Tracker.new(:name => 'New tracker', :default_status_id => 1)
181
    assert tracker.active?
182
  end
183

  
184
  def test_lock_tracker
185
    tracker = Tracker.find(1)
186
    assert tracker.active?
187
    tracker.update!(active: false)
188
    assert_not tracker.reload.active?
189
  end
190

  
191
  def test_unlock_tracker
192
    tracker = Tracker.find(1)
193
    tracker.update!(active: false)
194
    tracker.update!(active: true)
195
    assert tracker.reload.active?
196
  end
170 197
end
    (1-1/1)