Project

General

Profile

Defect #44299 » project_copy_skipped_issues.patch

Vincent Robert, 2026-07-30 15:15

View differences:

app/controllers/projects_controller.rb
149 149
      Mailer.with_deliveries(params[:notifications] == '1') do
150 150
        @project = Project.new
151 151
        @project.safe_attributes = params[:project]
152
        if @project.copy(@source_project, :only => params[:only])
152
        copied = @project.copy(@source_project, :only => params[:only])
153
        if @project.issues_not_copied.present?
154
          flash[:warning] = l(:warning_issues_not_copied, @project.issues_not_copied.size)
155
        end
156
        if copied
153 157
          flash[:notice] = l(:notice_successful_create)
154 158
          redirect_to settings_project_path(@project)
155 159
        elsif !@project.new_record?
156 160
          # Project was created
157 161
          # But some objects were not copied due to validation failures
158
          # (eg. issues from disabled trackers)
159 162
          # TODO: inform about that
160 163
          redirect_to settings_project_path(@project)
161 164
        end
app/models/project.rb
917 917
    p.nil? ? nil : p.identifier.to_s.succ
918 918
  end
919 919

  
920
  # Returns the issues of the source project that could not be copied
921
  # by the last call to #copy
922
  attr_reader :issues_not_copied
923

  
920 924
  # Copies and saves the Project instance based on the +project+.
921 925
  # Duplicates the source project's:
922 926
  # * Wiki
......
934 938
  #   project.copy(1, :only => ['members', 'versions'])  # => copies members and versions
935 939
  def copy(project, options={})
936 940
    project = Project.find(project) unless project.is_a?(Project)
941
    @issues_not_copied = []
937 942

  
938 943
    to_be_copied = %w(members wiki versions issue_categories issues queries boards documents)
939 944
    to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
......
1199 1204
              "#{new_issue.errors.full_messages}"
1200 1205
          )
1201 1206
        end
1207
        @issues_not_copied << issue
1208
        # Remove the issue that could not be saved from the association target,
1209
        # otherwise it would make the project invalid and prevent any further save
1210
        self.issues.target.delete(new_issue)
1202 1211
      else
1203 1212
        issues_map[issue.id] = new_issue unless new_issue.new_record?
1204 1213
      end
......
1230 1239
      end
1231 1240

  
1232 1241
      issue.relations_to.each do |source_relation|
1242
        # Relations between two copied issues have already been copied above
1243
        next if issues_map[source_relation.issue_from_id]
1244

  
1233 1245
        new_issue_relation = IssueRelation.new
1234 1246
        new_issue_relation.attributes =
1235 1247
          source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
config/locales/de.yml
1159 1159
  version_status_open: offen
1160 1160

  
1161 1161
  warning_attachments_not_saved: "%{count} Datei(en) konnten nicht gespeichert werden."
1162
  warning_issues_not_copied: "%{count} Ticket(s) konnten nicht kopiert werden."
1162 1163
  label_search_attachments_yes: Namen und Beschreibungen von Anhängen durchsuchen
1163 1164
  label_search_attachments_no: Keine Anhänge suchen
1164 1165
  label_search_attachments_only: Nur Anhänge suchen
config/locales/en.yml
227 227
  error_session_expired: "Your session has expired. Please login again."
228 228
  error_token_expired: "This password recovery link has expired, please try again."
229 229
  warning_attachments_not_saved: "%{count} file(s) could not be saved."
230
  warning_issues_not_copied: "%{count} issue(s) could not be copied."
230 231
  error_password_expired: "Your password has expired or the administrator requires you to change it."
231 232
  error_invalid_file_encoding: "The file is not a valid %{encoding} encoded file"
232 233
  error_invalid_csv_file_or_settings: "The file is not a CSV file or does not match the settings below (%{value})"
config/locales/fr.yml
231 231
  error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
232 232
  error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
233 233
  warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
234
  warning_issues_not_copied: "%{count} demande(s) n'ont pas pu être copiées."
234 235
  error_password_expired: "Votre mot de passe a expiré ou nécessite d'être changé."
235 236
  error_invalid_file_encoding: "Le fichier n'est pas un fichier %{encoding} valide"
236 237
  error_invalid_csv_file_or_settings: "Le fichier n'est pas un fichier CSV ou n'est pas conforme aux paramètres sélectionnés (%{value})"
config/locales/ja.yml
223 223
  error_can_not_delete_tracker_html: 'このトラッカーは使用中です。削除できません。<p>以下のプロジェクトにこのトラッカーを使用しているチケットがあります:<br>%{projects}</p>'
224 224

  
225 225
  warning_attachments_not_saved: "%{count}個の添付ファイルが保存できませんでした。"
226
  warning_issues_not_copied: "%{count}件のチケットをコピーできませんでした。"
226 227

  
227 228
  mail_subject_lost_password: "%{value} パスワード再設定"
228 229
  mail_body_lost_password: 'パスワードを変更するには、以下のリンクをクリックしてください:'
test/functional/projects_controller_test.rb
1496 1496
    assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
1497 1497
  end
1498 1498

  
1499
  def test_post_copy_should_warn_about_issues_that_could_not_be_copied
1500
    @request.session[:user_id] = 1 # admin
1501
    # Issues of the source project can not be copied because this required
1502
    # custom field is blank
1503
    IssueCustomField.generate!(:field_format => 'string', :is_required => true,
1504
                               :is_for_all => true, :trackers => Tracker.all)
1505
    post(
1506
      :copy,
1507
      :params => {
1508
        :id => 1,
1509
        :project => {
1510
          :name => 'Copy',
1511
          :identifier => 'unique-copy',
1512
          :tracker_ids => ['1', '2', '3', ''],
1513
          :enabled_module_names => %w(issue_tracking)
1514
        },
1515
        :only => %w(issues)
1516
      }
1517
    )
1518
    assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
1519
    project = Project.find('unique-copy')
1520
    assert_equal 0, project.issues.count
1521
    assert_equal l(:warning_issues_not_copied, Project.find(1).issues.count), flash[:warning]
1522
  end
1523

  
1499 1524
  def test_post_copy_with_failure
1500 1525
    @request.session[:user_id] = 1
1501 1526
    post(
test/unit/project_copy_test.rb
66 66
    assert_equal "Closed", copied_issue.status.name
67 67
  end
68 68

  
69
  test "#copy should keep copying when an issue cannot be copied" do
70
    # This issue can not be copied because the required custom field below is blank
71
    Issue.generate!(:project => @source_project, :tracker_id => 2,
72
                    :subject => 'No required value')
73
    IssueCustomField.generate!(:field_format => 'string', :is_required => true,
74
                               :is_for_all => true, :trackers => [Tracker.find(2)])
75

  
76
    assert @project.copy(@source_project)
77
    assert @project.valid?
78
    assert_nil @project.issues.find_by(:subject => 'No required value')
79
    # The other issues and objects are copied even though an issue was skipped
80
    assert @project.issues.any?
81
    assert @project.versions.any?
82
    assert @project.members.any?
83
  end
84

  
85
  test "#copy should report the issues that could not be copied" do
86
    skipped_issue =
87
      Issue.generate!(:project => @source_project, :tracker_id => 2,
88
                      :subject => 'No required value')
89
    IssueCustomField.generate!(:field_format => 'string', :is_required => true,
90
                               :is_for_all => true, :trackers => [Tracker.find(2)])
91

  
92
    assert @project.copy(@source_project)
93
    assert_equal [skipped_issue], @project.issues_not_copied
94
  end
95

  
96
  test "#copy should not report any issue when they are all copied" do
97
    assert @project.copy(@source_project)
98
    assert_equal [], @project.issues_not_copied
99
  end
100

  
69 101
  test "#copy should copy issues custom values" do
70 102
    field = IssueCustomField.generate!(:is_for_all => true, :trackers => Tracker.all)
71 103
    issue = Issue.generate!(:project => @source_project, :subject => 'Custom field copy')
......
187 219
    end
188 220
  end
189 221

  
222
  test "#copy should copy a relation between two copied issues only once" do
223
    issue = Issue.generate!(:project => @source_project, :subject => 'Relation from')
224
    other_issue = Issue.generate!(:project => @source_project, :subject => 'Relation to')
225
    IssueRelation.create!(:issue_from => issue, :issue_to => other_issue,
226
                          :relation_type => 'relates')
227

  
228
    assert @project.copy(@source_project)
229
    # A second copy of the relation would be rejected by the uniqueness
230
    # validation and would leave the copied issue invalid
231
    assert @project.issues.all?(&:valid?)
232
    assert_equal(
233
      1,
234
      IssueRelation.where(:issue_from_id => @project.issues, :issue_to_id => @project.issues).count
235
    )
236
  end
237

  
190 238
  test "#copy should copy issue attachments" do
191 239
    set_tmp_attachments_directory
192 240
    issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
    (1-1/1)