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