diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 2a42c99ed..f7065e130 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -96,14 +96,14 @@ class ProjectsController < ApplicationController
def new
@issue_custom_fields = IssueCustomField.sorted.to_a
- @trackers = Tracker.sorted.to_a
+ @trackers = Tracker.active.sorted.to_a
@project = Project.new
@project.safe_attributes = params[:project]
end
def create
@issue_custom_fields = IssueCustomField.sorted.to_a
- @trackers = Tracker.sorted.to_a
+ @trackers = Tracker.active.sorted.to_a
@project = Project.new
@project.safe_attributes = params[:project]
@@ -140,7 +140,7 @@ class ProjectsController < ApplicationController
def copy
@issue_custom_fields = IssueCustomField.sorted.to_a
- @trackers = Tracker.sorted.to_a
+ @trackers = Tracker.active.sorted.to_a
@source_project = Project.find(params[:id])
if request.get?
@project = Project.copy_from(@source_project)
@@ -201,7 +201,10 @@ class ProjectsController < ApplicationController
@issue_custom_fields = IssueCustomField.sorted.to_a
@issue_category ||= IssueCategory.new
@member ||= @project.members.new
- @trackers = Tracker.sorted.to_a
+ # Show active trackers plus any inactive trackers already assigned to this project
+ @trackers = Tracker.sorted.where(:active => true).or(
+ Tracker.sorted.where(:id => @project.tracker_ids)
+ ).sorted.to_a
@version_status = params[:version_status] || 'open'
@version_name = params[:version_name]
diff --git a/app/controllers/trackers_controller.rb b/app/controllers/trackers_controller.rb
index 4080df618..308b48b4a 100644
--- a/app/controllers/trackers_controller.rb
+++ b/app/controllers/trackers_controller.rb
@@ -86,6 +86,18 @@ class TrackersController < ApplicationController
end
end
+ def lock
+ @tracker = Tracker.find(params[:id])
+ @tracker.update!(:active => false)
+ redirect_to trackers_path
+ end
+
+ def unlock
+ @tracker = Tracker.find(params[:id])
+ @tracker.update!(:active => true)
+ redirect_to trackers_path
+ end
+
def destroy
@tracker = Tracker.find(params[:id])
if @tracker.issues.empty?
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 4c16fb6af..7e7d0d1d1 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -1695,7 +1695,13 @@ class Issue < ApplicationRecord
# Returns a scope of trackers that user can assign project issues to
def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
if project
- scope = project.trackers.sorted
+ scope = project.trackers.sorted.active
+ # Always include the current tracker even if it is inactive
+ if current_tracker
+ scope = Tracker.sorted.where(
+ :id => scope.pluck(:id) | [current_tracker]
+ )
+ end
unless user.admin?
roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
@@ -1810,7 +1816,9 @@ class Issue < ApplicationRecord
copy.author = author
copy.project = project
copy.parent_issue_id = copied_issue_ids[child.parent_id]
- unless child.fixed_version.present? && child.fixed_version.status == 'open'
+ if fixed_version_id != @copied_from.fixed_version_id
+ copy.fixed_version_id = fixed_version_id
+ elsif !(child.fixed_version.present? && child.fixed_version.status == 'open')
copy.fixed_version_id = nil
end
unless child.assigned_to_id.present? &&
diff --git a/app/models/project.rb b/app/models/project.rb
index d15c29882..55c66169d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -139,9 +139,9 @@ class Project < ApplicationRecord
if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
default = Setting.default_projects_tracker_ids
if default.is_a?(Array)
- self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
+ self.trackers = Tracker.where(:id => default.map(&:to_i)).active.sorted.to_a
else
- self.trackers = Tracker.sorted.to_a
+ self.trackers = Tracker.active.sorted.to_a
end
end
# rubocop:enable Style/NegatedIf
diff --git a/app/models/tracker.rb b/app/models/tracker.rb
index e33ec333c..4ec260fc6 100644
--- a/app/models/tracker.rb
+++ b/app/models/tracker.rb
@@ -46,6 +46,7 @@ class Tracker < ApplicationRecord
scope :sorted, lambda {order(:position)}
scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
+ scope :active, lambda {where(:active => true)}
# Returns the trackers that are visible by the user.
#
@@ -75,6 +76,7 @@ class Tracker < ApplicationRecord
'default_status_id',
'is_in_roadmap',
'private_by_default',
+ 'active',
'core_fields',
'position',
'custom_field_ids',
diff --git a/app/views/trackers/index.html.erb b/app/views/trackers/index.html.erb
index 12dc0aa4c..074f3f231 100644
--- a/app/views/trackers/index.html.erb
+++ b/app/views/trackers/index.html.erb
@@ -11,11 +11,11 @@
<%=l(:field_default_status)%> |
<%=l(:field_description)%> |
|
- |
+ |
<% for tracker in @trackers %>
-
+
| <%= link_to tracker.name, edit_tracker_path(tracker) %> |
<%= tracker.default_status.name %> |
<%= tracker.description %> |
@@ -26,9 +26,14 @@
<% end %>
-
+ |
<%= reorder_handle(tracker) %>
<%= link_to sprite_icon('copy', l(:button_copy)), new_tracker_path(:copy => tracker), :class => 'icon icon-copy' %>
+ <% if tracker.active? %>
+ <%= link_to sprite_icon('lock', l(:button_lock)), lock_tracker_path(tracker), :method => :put, :class => 'icon icon-lock' %>
+ <% else %>
+ <%= link_to sprite_icon('unlock', l(:button_unlock)), unlock_tracker_path(tracker), :method => :put, :class => 'icon icon-unlock' %>
+ <% end %>
<%= delete_link tracker_path(tracker) %>
|
@@ -36,6 +41,14 @@
+
+
<% html_title(l(:label_tracker_plural)) -%>
<%= javascript_tag do %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 2a30a9d3a..e48d40319 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -672,6 +672,7 @@ en:
label_tracker_plural: Trackers
label_tracker_all: All trackers
label_tracker_new: New tracker
+ label_tracker_locked: Locked
label_workflow: Workflow
label_issue_status: Issue status
label_issue_status_plural: Issue statuses
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 05b9ab47d..a23f96d6c 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -1201,6 +1201,7 @@ ja:
label_no_preview: このファイルはプレビューできません
error_no_tracker_allowed_for_new_issue_in_project: このプロジェクトにはチケットの追加が許可されているトラッカーがありません
label_tracker_all: すべてのトラッカー
+ label_tracker_locked: ロック済み
label_new_project_issue_tab_enabled: '"新しいチケット" タブを表示'
setting_new_item_menu_tab: 新規オブジェクト作成タブ
label_new_object_tab_enabled: '"+" ドロップダウンを表示'
diff --git a/config/routes.rb b/config/routes.rb
index 9edb2f855..7efa22646 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -350,6 +350,10 @@ Rails.application.routes.draw do
delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
resources :trackers, :except => :show do
+ member do
+ put 'lock'
+ put 'unlock'
+ end
collection do
match 'fields', :via => [:get, :post]
end
diff --git a/db/migrate/20260612000000_add_active_to_trackers.rb b/db/migrate/20260612000000_add_active_to_trackers.rb
new file mode 100644
index 000000000..c34404572
--- /dev/null
+++ b/db/migrate/20260612000000_add_active_to_trackers.rb
@@ -0,0 +1,5 @@
+class AddActiveToTrackers < ActiveRecord::Migration[8.1]
+ def change
+ add_column :trackers, :active, :boolean, :default => true, :null => false
+ end
+end
diff --git a/test/fixtures/trackers.yml b/test/fixtures/trackers.yml
index d5022da6b..312e66022 100644
--- a/test/fixtures/trackers.yml
+++ b/test/fixtures/trackers.yml
@@ -4,15 +4,18 @@ trackers_001:
id: 1
default_status_id: 1
position: 1
+ active: true
description: Description for Bug tracker
trackers_002:
name: Feature request
id: 2
default_status_id: 1
position: 2
+ active: true
description: Description for Feature request tracker
trackers_003:
name: Support request
id: 3
default_status_id: 1
position: 3
+ active: true
diff --git a/test/functional/trackers_controller_test.rb b/test/functional/trackers_controller_test.rb
index e55d8ffc8..879c572aa 100644
--- a/test/functional/trackers_controller_test.rb
+++ b/test/functional/trackers_controller_test.rb
@@ -304,6 +304,41 @@ class TrackersControllerTest < Redmine::ControllerTest
end
end
+ def test_lock
+ tracker = Tracker.find(1)
+ assert tracker.active?
+ put :lock, :params => {:id => 1}
+ assert_redirected_to :action => 'index'
+ assert_not tracker.reload.active?
+ end
+
+ def test_unlock
+ tracker = Tracker.find(1)
+ tracker.update!(active: false)
+ put :unlock, :params => {:id => 1}
+ assert_redirected_to :action => 'index'
+ assert tracker.reload.active?
+ end
+
+ def test_lock_requires_admin
+ @request.session[:user_id] = 2
+ put :lock, :params => {:id => 1}
+ assert_response :forbidden
+ end
+
+ def test_index_shows_lock_button_for_active_tracker
+ get :index
+ assert_response :success
+ assert_select 'a[href=?]', lock_tracker_path(1)
+ end
+
+ def test_index_shows_unlock_button_for_locked_tracker
+ Tracker.find(1).update!(active: false)
+ get :index
+ assert_response :success
+ assert_select 'a[href=?]', unlock_tracker_path(1)
+ end
+
def test_post_fields
post :fields, :params => {
:trackers => {
diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb
index a5c0bbb87..3ed2f7fd9 100644
--- a/test/unit/issue_test.rb
+++ b/test/unit/issue_test.rb
@@ -1539,6 +1539,22 @@ class IssueTest < ActiveSupport::TestCase
assert_equal [3, nil], copy.children.map(&:fixed_version_id)
end
+ def test_copy_should_propagate_version_change_to_subtasks
+ parent = Issue.generate!(:fixed_version_id => 3)
+ child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1', :fixed_version_id => 3)
+ child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2', :fixed_version_id => nil)
+
+ new_version = Version.generate!(:project => Project.find(1))
+ copy = parent.reload.copy(:fixed_version_id => new_version.id)
+
+ assert_difference 'Issue.count', 3 do
+ assert copy.save
+ end
+
+ assert_equal new_version.id, copy.fixed_version_id
+ assert_equal [new_version.id, new_version.id], copy.children.map(&:fixed_version_id)
+ end
+
def test_copy_should_clear_subtasks_assignee_if_is_locked
user = User.find(2)
@@ -1909,6 +1925,23 @@ class IssueTest < ActiveSupport::TestCase
assert_equal [], issue.allowed_target_trackers(User.find(2)).ids
end
+ def test_allowed_target_trackers_excludes_locked_trackers
+ Tracker.find(1).update!(active: false)
+ issue = Issue.new(:project => Project.find(1))
+ ids = Issue.allowed_target_trackers(Project.find(1), User.find(1)).ids
+ assert_not_includes ids, 1
+ assert_includes ids, 2
+ assert_includes ids, 3
+ end
+
+ def test_allowed_target_trackers_includes_current_tracker_even_if_locked
+ tracker = Tracker.find(1)
+ tracker.update!(active: false)
+ issue = Issue.generate!(:project => Project.find(1), :tracker => tracker)
+ ids = issue.allowed_target_trackers(User.find(1)).ids
+ assert_includes ids, 1
+ end
+
def test_allowed_target_trackers_should_include_current_tracker
user = User.generate!
role = Role.generate!
diff --git a/test/unit/tracker_test.rb b/test/unit/tracker_test.rb
index a9728b9b2..5f65f48be 100644
--- a/test/unit/tracker_test.rb
+++ b/test/unit/tracker_test.rb
@@ -167,4 +167,31 @@ class TrackerTest < ActiveSupport::TestCase
assert tracker.respond_to?(:description)
assert_equal tracker.description, "Description for Bug tracker"
end
+
+ def test_active_scope_returns_only_active_trackers
+ Tracker.find(1).update!(active: false)
+ active_ids = Tracker.active.pluck(:id)
+ assert_not_includes active_ids, 1
+ assert_includes active_ids, 2
+ assert_includes active_ids, 3
+ end
+
+ def test_tracker_is_active_by_default
+ tracker = Tracker.new(:name => 'New tracker', :default_status_id => 1)
+ assert tracker.active?
+ end
+
+ def test_lock_tracker
+ tracker = Tracker.find(1)
+ assert tracker.active?
+ tracker.update!(active: false)
+ assert_not tracker.reload.active?
+ end
+
+ def test_unlock_tracker
+ tracker = Tracker.find(1)
+ tracker.update!(active: false)
+ tracker.update!(active: true)
+ assert tracker.reload.active?
+ end
end