mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
[#71251] Migrate Versions to Sprints
https://community.openproject.org/work_packages/71251 - Validate dates only on active Agile::Sprints - Create journals for migrating Versions to Sprints - Create migration from Versions to Sprints
This commit is contained in:
@@ -29,10 +29,9 @@
|
||||
#++
|
||||
#
|
||||
class Journal::CausedBySystemUpdate < CauseOfChange::Base
|
||||
def initialize(feature:)
|
||||
additional = {
|
||||
"feature" => feature
|
||||
}
|
||||
super("system_update", additional)
|
||||
def initialize(feature:, **additional_attributes)
|
||||
system_update_attributes =
|
||||
{ "feature" => feature }.merge(additional_attributes.deep_stringify_keys)
|
||||
super("system_update", system_update_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3449,6 +3449,7 @@ en:
|
||||
totals_removed_from_childless_work_packages: >-
|
||||
Work and progress totals automatically removed for non-parent work packages with <a href="%{href}" target="_blank">version update</a>.
|
||||
This is a maintenance task and can be safely ignored.
|
||||
sprint_migration: "Version '%{version_name}' has been converted to a Sprint."
|
||||
total_percent_complete_mode_changed_to_work_weighted_average: >-
|
||||
Child work packages without Work are ignored.
|
||||
total_percent_complete_mode_changed_to_simple_average: >-
|
||||
|
||||
@@ -101,6 +101,8 @@ class OpenProject::JournalFormatter::Cause < JournalFormatter::Base
|
||||
{ href: OpenProject::Static::Links.url_for(:blog_article_progress_changes) }
|
||||
when "totals_removed_from_childless_work_packages"
|
||||
{ href: OpenProject::Static::Links.url_for(:release_notes_14_0_1) }
|
||||
when "sprint_migration"
|
||||
{ version_name: cause["version_name"] }
|
||||
else
|
||||
{}
|
||||
end
|
||||
|
||||
@@ -54,13 +54,11 @@ module Agile
|
||||
default: "in_planning",
|
||||
validate: true
|
||||
|
||||
validates :name, presence: true
|
||||
validates :project, presence: true
|
||||
validates :start_date, presence: true
|
||||
validates :finish_date, presence: true
|
||||
validates :name, :project, presence: true
|
||||
validates :start_date, :finish_date, presence: true, if: :active?
|
||||
validates :finish_date,
|
||||
comparison: { greater_than_or_equal_to: :start_date },
|
||||
if: :start_date?
|
||||
if: -> { start_date? && finish_date? }
|
||||
|
||||
validate :validate_only_one_active_sprint_per_project
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
# Writes a journal entry on each work package that was associated with a sprint
|
||||
# during the version-to-sprint migration. Runs asynchronously after the migration
|
||||
# so that the migration itself does not block on journal creation.
|
||||
module Backlogs
|
||||
class MigrateVersionSprintJournalsJob < ApplicationJob
|
||||
# wp_version_map: { work_package_id (Integer) => version_name (String) }
|
||||
def perform(wp_version_map)
|
||||
system_user = User.system
|
||||
|
||||
work_packages = WorkPackage.where(id: wp_version_map.keys).index_by(&:id)
|
||||
|
||||
Journal::NotificationConfiguration.with(false) do
|
||||
wp_version_map.each do |wp_id_str, version_name|
|
||||
wp_id = wp_id_str.to_i
|
||||
work_package = work_packages[wp_id]
|
||||
next unless work_package
|
||||
|
||||
Journals::CreateService
|
||||
.new(work_package, system_user)
|
||||
.call(cause: Journal::CausedBySystemUpdate.new(feature: "sprint_migration", version_name:))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class MakeSprintDatesNullable < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
change_column_null :sprints, :start_date, true
|
||||
change_column_null :sprints, :finish_date, true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class MigrateVersionsToSprints < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
wp_version_map = {}
|
||||
|
||||
sprint_versions_with_work_package_ids.each do |version, wp_ids|
|
||||
sprint = create_sprint(version)
|
||||
migrate_work_packages_to_sprint(sprint, wp_ids)
|
||||
wp_ids.each { |wp_id| wp_version_map[wp_id.to_s] = sprint.name }
|
||||
end
|
||||
|
||||
Backlogs::MigrateVersionSprintJournalsJob.perform_later(wp_version_map) if wp_version_map.any?
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sprint_versions_with_work_package_ids
|
||||
# Load versions used as sprints including work package ids associated with the version.
|
||||
# Since the same version can be used as a sprint in one project but not in another,
|
||||
# the work package ids are filtered by projects where the version is used as a sprint.
|
||||
Version.joins(:version_settings, :work_packages)
|
||||
.where(version_settings: { display: VersionSetting::DISPLAY_LEFT })
|
||||
.where("work_packages.project_id = version_settings.project_id")
|
||||
.includes(:project)
|
||||
.group("versions.id")
|
||||
.select("versions.*, array_agg(work_packages.id) AS wp_ids")
|
||||
.map { |version| [version, version.wp_ids] }
|
||||
end
|
||||
|
||||
def create_sprint(version)
|
||||
Agile::Sprint.create!(
|
||||
name: version.name,
|
||||
project: version.project,
|
||||
status: version.status == "open" ? "in_planning" : "completed",
|
||||
start_date: version.start_date,
|
||||
finish_date: version.effective_date
|
||||
)
|
||||
end
|
||||
|
||||
def migrate_work_packages_to_sprint(sprint, wp_ids)
|
||||
WorkPackage.where(id: wp_ids).update_all(sprint_id: sprint.id)
|
||||
end
|
||||
end
|
||||
@@ -89,13 +89,13 @@ RSpec.describe Sprints::CreateContract do
|
||||
context "when start_date is blank" do
|
||||
let(:sprint_start_date) { nil }
|
||||
|
||||
it_behaves_like "contract is invalid", start_date: :blank
|
||||
it_behaves_like "contract is valid"
|
||||
end
|
||||
|
||||
context "when finish_date is blank" do
|
||||
let(:sprint_finish_date) { nil }
|
||||
|
||||
it_behaves_like "contract is invalid", finish_date: %i[blank blank]
|
||||
it_behaves_like "contract is valid"
|
||||
end
|
||||
|
||||
context "when finish_date is before start_date" do
|
||||
@@ -105,6 +105,29 @@ RSpec.describe Sprints::CreateContract do
|
||||
it_behaves_like "contract is invalid", finish_date: %i[greater_than_or_equal_to]
|
||||
end
|
||||
|
||||
context "when the sprint is active" do
|
||||
let(:sprint_status) { "active" }
|
||||
|
||||
context "when start_date is blank" do
|
||||
let(:sprint_start_date) { nil }
|
||||
|
||||
it_behaves_like "contract is invalid", start_date: :blank
|
||||
end
|
||||
|
||||
context "when finish_date is blank" do
|
||||
let(:sprint_finish_date) { nil }
|
||||
|
||||
it_behaves_like "contract is invalid", finish_date: :blank
|
||||
end
|
||||
|
||||
context "when finish_date is before start_date" do
|
||||
let(:sprint_start_date) { Time.zone.today }
|
||||
let(:sprint_finish_date) { Time.zone.today - 1.day }
|
||||
|
||||
it_behaves_like "contract is invalid", finish_date: %i[greater_than_or_equal_to]
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is admin without project permission" do
|
||||
let(:user) { build_stubbed(:admin) }
|
||||
let(:permissions) { [] }
|
||||
|
||||
@@ -121,20 +121,6 @@ RSpec.describe "Create", :js do
|
||||
describe "validations" do
|
||||
let(:too_early_finish_date) { start_date - 1.day }
|
||||
|
||||
it "validates required fields are present" do
|
||||
backlogs_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
page.fill_in "Sprint name", with: ""
|
||||
|
||||
click_on "Create"
|
||||
|
||||
expect(page).to have_field "Sprint name", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Start date", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Finish date", validation_error: "can't be blank"
|
||||
end
|
||||
end
|
||||
|
||||
it "validates finish date is not before start date" do
|
||||
backlogs_page.open_create_sprint_dialog
|
||||
|
||||
|
||||
@@ -162,6 +162,29 @@ RSpec.describe "Edit", :js do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
context "when sprint status is active" do
|
||||
before { first_sprint.update!(status: "active") }
|
||||
|
||||
it "validates required fields are present" do
|
||||
backlogs_page.click_in_sprint_menu(first_sprint, "Edit sprint")
|
||||
backlogs_page.expect_sprint_dialog
|
||||
|
||||
within_dialog "Edit sprint" do
|
||||
page.fill_in "Sprint name", with: ""
|
||||
page.fill_in "Start date", with: ""
|
||||
page.fill_in "Finish date", with: ""
|
||||
|
||||
page.click_button "Save"
|
||||
|
||||
expect(page).to have_field "Sprint name", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Start date", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Finish date", validation_error: "can't be blank"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
require Rails.root.join("modules/backlogs/db/migrate/20260313164539_migrate_versions_to_sprints")
|
||||
|
||||
RSpec.describe MigrateVersionsToSprints, type: :model do
|
||||
subject(:migrate) { ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } }
|
||||
|
||||
let(:project) { create(:project) }
|
||||
let(:start_date) { nil }
|
||||
let(:effective_date) { nil }
|
||||
let(:status) { "open" }
|
||||
let(:version_type) { :sprint }
|
||||
let!(:version) do
|
||||
create(:version, project:, name: "Test Sprint", start_date:, effective_date:, status:)
|
||||
end
|
||||
let!(:wp1) { create(:work_package, version:, project:) }
|
||||
|
||||
def use_version(as:, version: self.version, project: self.project)
|
||||
display = as == :sprint ? VersionSetting::DISPLAY_LEFT : VersionSetting::DISPLAY_RIGHT
|
||||
create(:version_setting, version:, project:, display:)
|
||||
end
|
||||
|
||||
before { use_version(as: version_type) }
|
||||
|
||||
describe "qualification criteria" do
|
||||
context "when all criteria are met (used as sprint and work package present)" do
|
||||
let(:start_date) { Date.new(2026, 1, 1) }
|
||||
let(:effective_date) { Date.new(2026, 1, 14) }
|
||||
|
||||
it "creates one sprint" do
|
||||
expect { migrate }.to change(Agile::Sprint, :count).by(1)
|
||||
end
|
||||
|
||||
it "copies name, start_date and finish_date" do
|
||||
migrate
|
||||
sprint = Agile::Sprint.last
|
||||
expect(sprint.name).to eq("Test Sprint")
|
||||
expect(sprint.start_date).to eq(Date.new(2026, 1, 1))
|
||||
expect(sprint.finish_date).to eq(Date.new(2026, 1, 14))
|
||||
end
|
||||
end
|
||||
|
||||
context "when version is used as a backlog" do
|
||||
let(:version_type) { :backlog }
|
||||
|
||||
it "does not create a sprint" do
|
||||
expect { migrate }.not_to change(Agile::Sprint, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no work packages are associated with the version" do
|
||||
let!(:wp1) { nil }
|
||||
|
||||
it "does not create a sprint" do
|
||||
expect { migrate }.not_to change(Agile::Sprint, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "date handling" do
|
||||
context "when both start_date and effective_date are null" do
|
||||
it "creates a sprint with nil dates" do
|
||||
expect { migrate }.to change(Agile::Sprint, :count).by(1)
|
||||
sprint = Agile::Sprint.last
|
||||
expect(sprint.start_date).to be_nil
|
||||
expect(sprint.finish_date).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when only effective_date is set" do
|
||||
let(:effective_date) { Date.new(2026, 2, 28) }
|
||||
|
||||
it "sets effective_date for finish_date" do
|
||||
migrate
|
||||
sprint = Agile::Sprint.last
|
||||
expect(sprint.start_date).to be_nil
|
||||
expect(sprint.finish_date).to eq(Date.new(2026, 2, 28))
|
||||
end
|
||||
end
|
||||
|
||||
context "when only start_date is set" do
|
||||
let(:start_date) { Date.new(2026, 2, 1) }
|
||||
|
||||
it "sets start_date for start_date" do
|
||||
migrate
|
||||
sprint = Agile::Sprint.last
|
||||
expect(sprint.start_date).to eq(Date.new(2026, 2, 1))
|
||||
expect(sprint.finish_date).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "status mapping" do
|
||||
context "when version status is open" do
|
||||
let(:status) { "open" }
|
||||
|
||||
it "creates sprint with in_planning status" do
|
||||
migrate
|
||||
expect(Agile::Sprint.last.status).to eq("in_planning")
|
||||
end
|
||||
end
|
||||
|
||||
context "when version status is locked" do
|
||||
let(:status) { "locked" }
|
||||
|
||||
it "creates sprint with completed status" do
|
||||
migrate
|
||||
expect(Agile::Sprint.last.status).to eq("completed")
|
||||
end
|
||||
end
|
||||
|
||||
context "when version status is closed" do
|
||||
let(:status) { "closed" }
|
||||
|
||||
it "creates sprint with completed status" do
|
||||
migrate
|
||||
expect(Agile::Sprint.last.status).to eq("completed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "work package association" do
|
||||
let!(:wp2) { create(:work_package, version:, project:) }
|
||||
|
||||
it "sets sprint_id on all associated work packages" do
|
||||
migrate
|
||||
sprint = Agile::Sprint.last
|
||||
expect(wp1.reload.sprint_id).to eq(sprint.id)
|
||||
expect(wp2.reload.sprint_id).to eq(sprint.id)
|
||||
end
|
||||
|
||||
it "keeps the version_id on associated work packages" do
|
||||
migrate
|
||||
expect(wp1.reload.version_id).to eq(version.id)
|
||||
expect(wp2.reload.version_id).to eq(version.id)
|
||||
end
|
||||
|
||||
context "with multiple versions" do
|
||||
let!(:version2) { create(:version, project:, status: "closed") }
|
||||
let!(:wp3) { create(:work_package, version: version2, project:) }
|
||||
|
||||
before { use_version(as: :sprint, version: version2) }
|
||||
|
||||
it "assigns work packages to their respective sprints" do
|
||||
migrate
|
||||
sprints = Agile::Sprint.all.index_by(&:name)
|
||||
expect(wp1.reload.sprint_id).to eq(sprints[version.name].id)
|
||||
expect(wp2.reload.sprint_id).to eq(sprints[version.name].id)
|
||||
expect(wp3.reload.sprint_id).to eq(sprints[version2.name].id)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the version is shared with another project that displays it as non-sprint" do
|
||||
let(:other_project) { create(:project) }
|
||||
let!(:wp_in_other_project) { create(:work_package, version:, project: other_project) }
|
||||
|
||||
before { use_version(as: :backlog, project: other_project) }
|
||||
|
||||
it "only assigns work packages from the sprint project" do
|
||||
migrate
|
||||
sprint = Agile::Sprint.last
|
||||
expect(wp1.reload.sprint_id).to eq(sprint.id)
|
||||
expect(wp2.reload.sprint_id).to eq(sprint.id)
|
||||
expect(wp_in_other_project.reload.sprint_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "journal job" do
|
||||
it "enqueues MigrateVersionSprintJournalsJob with the work package mapping" do
|
||||
allow(Backlogs::MigrateVersionSprintJournalsJob).to receive(:perform_later)
|
||||
|
||||
migrate
|
||||
|
||||
expect(Backlogs::MigrateVersionSprintJournalsJob)
|
||||
.to have_received(:perform_later)
|
||||
.with({ wp1.id.to_s => "Test Sprint" })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -32,51 +32,66 @@ require "spec_helper"
|
||||
|
||||
RSpec.describe Agile::Sprint do
|
||||
let(:project) { create(:project) }
|
||||
let(:sprint_status) { "in_planning" }
|
||||
|
||||
subject(:sprint) do
|
||||
described_class.new(name: "Sprint 1",
|
||||
project:,
|
||||
start_date: Time.zone.today,
|
||||
finish_date: Time.zone.today + 14.days)
|
||||
finish_date: Time.zone.today + 14.days,
|
||||
status: sprint_status)
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
it { is_expected.to validate_presence_of(:start_date) }
|
||||
it { is_expected.to validate_presence_of(:finish_date) }
|
||||
it { is_expected.to validate_presence_of(:project) }
|
||||
it { is_expected.to validate_inclusion_of(:status).in_array(described_class.statuses.keys) }
|
||||
|
||||
it "validates finish_date is after or equal to start_date" do
|
||||
sprint.finish_date = sprint.start_date - 1.day
|
||||
expect(sprint).not_to be_valid
|
||||
expect(sprint.errors[:finish_date]).to include(/must be greater than or equal to/)
|
||||
end
|
||||
|
||||
it "does not validate finish_date comparison when start_date is nil" do
|
||||
sprint.start_date = nil
|
||||
sprint.finish_date = Time.zone.today
|
||||
expect(sprint).not_to be_valid
|
||||
expect(sprint.errors[:start_date]).to be_present
|
||||
expect(sprint.errors[:finish_date]).not_to include(/must be greater than or equal to/)
|
||||
end
|
||||
|
||||
it "still validates finish_date presence even when start_date is nil" do
|
||||
it "allows nil start and finish dates" do
|
||||
sprint.start_date = nil
|
||||
sprint.finish_date = nil
|
||||
expect(sprint).not_to be_valid
|
||||
expect(sprint.errors[:finish_date]).to be_present
|
||||
expect(sprint).to be_valid
|
||||
end
|
||||
|
||||
it "allows a nil finish date when start date is present" do
|
||||
sprint.start_date = Time.zone.today
|
||||
sprint.finish_date = nil
|
||||
expect(sprint).to be_valid
|
||||
end
|
||||
|
||||
context "with active sprint validation" do
|
||||
let(:sprint_status) { "active" }
|
||||
|
||||
it { is_expected.to validate_presence_of(:start_date) }
|
||||
it { is_expected.to validate_presence_of(:finish_date) }
|
||||
|
||||
it "validates finish_date is after or equal to start_date" do
|
||||
sprint.finish_date = sprint.start_date - 1.day
|
||||
expect(sprint).not_to be_valid
|
||||
expect(sprint.errors[:finish_date]).to include(/must be greater than or equal to/)
|
||||
end
|
||||
|
||||
it "does not validate finish_date comparison when start_date is nil" do
|
||||
sprint.start_date = nil
|
||||
sprint.finish_date = Time.zone.today
|
||||
expect(sprint).not_to be_valid
|
||||
expect(sprint.errors[:start_date]).to be_present
|
||||
expect(sprint.errors[:finish_date]).not_to include(/must be greater than or equal to/)
|
||||
end
|
||||
|
||||
it "still validates finish_date presence even when start_date is nil" do
|
||||
sprint.start_date = nil
|
||||
sprint.finish_date = nil
|
||||
expect(sprint).not_to be_valid
|
||||
expect(sprint.errors[:finish_date]).to be_present
|
||||
end
|
||||
|
||||
it "allows one active sprint per project" do
|
||||
sprint.status = "active"
|
||||
expect(sprint).to be_valid
|
||||
end
|
||||
|
||||
it "prevents multiple active sprints in the same project" do
|
||||
create(:agile_sprint, project:, status: "active")
|
||||
sprint.status = "active"
|
||||
expect(sprint).not_to be_valid
|
||||
expect(sprint.errors[:status]).to include("only one active sprint is allowed per project.")
|
||||
end
|
||||
@@ -84,12 +99,10 @@ RSpec.describe Agile::Sprint do
|
||||
it "allows multiple active sprints in different projects" do
|
||||
other_project = create(:project)
|
||||
create(:agile_sprint, project: other_project, status: "active")
|
||||
sprint.status = "active"
|
||||
expect(sprint).to be_valid
|
||||
end
|
||||
|
||||
it "allows updating an existing active sprint" do
|
||||
sprint.status = "active"
|
||||
sprint.save!
|
||||
sprint.name = "Updated Sprint"
|
||||
expect(sprint).to be_valid
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Backlogs::MigrateVersionSprintJournalsJob, type: :model do
|
||||
let(:project) { create(:project) }
|
||||
let(:wp1) { create(:work_package, project:) }
|
||||
let(:wp2) { create(:work_package, project:) }
|
||||
|
||||
subject(:perform) { described_class.new.perform(wp_version_map) }
|
||||
|
||||
describe "#perform" do
|
||||
context "with multiple work packages" do
|
||||
let(:wp_version_map) do
|
||||
{
|
||||
wp1.id.to_s => "Sprint A",
|
||||
wp2.id.to_s => "Sprint B"
|
||||
}
|
||||
end
|
||||
|
||||
it "creates a journal entry for each work package authored by the system user" do
|
||||
perform
|
||||
expect(wp1.reload.last_journal.user).to eq(User.system)
|
||||
expect(wp2.reload.last_journal.user).to eq(User.system)
|
||||
end
|
||||
|
||||
it "sets the cause type to system_update" do
|
||||
perform
|
||||
expect(wp1.reload.last_journal.cause_type).to eq("system_update")
|
||||
expect(wp2.reload.last_journal.cause_type).to eq("system_update")
|
||||
end
|
||||
|
||||
it "sets the cause feature to sprint_migration" do
|
||||
perform
|
||||
expect(wp1.reload.last_journal.cause_feature).to eq("sprint_migration")
|
||||
expect(wp2.reload.last_journal.cause_feature).to eq("sprint_migration")
|
||||
end
|
||||
|
||||
it "stores the originating version name in the cause" do
|
||||
perform
|
||||
expect(wp1.reload.last_journal.cause["version_name"]).to eq("Sprint A")
|
||||
expect(wp2.reload.last_journal.cause["version_name"]).to eq("Sprint B")
|
||||
end
|
||||
|
||||
it "suppresses journal notifications" do
|
||||
allow(Journal::NotificationConfiguration).to receive(:with).and_call_original
|
||||
perform
|
||||
expect(Journal::NotificationConfiguration).to have_received(:with).with(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the map contains an id that no longer exists" do
|
||||
let(:wp_version_map) do
|
||||
{
|
||||
"0" => "Ghost Sprint",
|
||||
wp1.id.to_s => "Sprint A"
|
||||
}
|
||||
end
|
||||
|
||||
it "skips the missing id and still journals the existing work package" do
|
||||
expect { perform }.not_to raise_error
|
||||
expect(wp1.reload.last_journal.cause["version_name"]).to eq("Sprint A")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -550,6 +550,30 @@ RSpec.describe OpenProject::JournalFormatter::Cause do
|
||||
end
|
||||
end
|
||||
|
||||
context "when the change was caused by a sprint migration" do
|
||||
subject(:cause) do
|
||||
{
|
||||
"type" => "system_update",
|
||||
"feature" => "sprint_migration",
|
||||
"version_name" => "Sprint 1"
|
||||
}
|
||||
end
|
||||
|
||||
it do
|
||||
expect(cause).to render_html_variant(
|
||||
"<strong>#{I18n.t('journals.caused_changes.system_update')}</strong> " \
|
||||
"#{I18n.t('journals.cause_descriptions.system_update.sprint_migration', version_name: 'Sprint 1')}"
|
||||
)
|
||||
end
|
||||
|
||||
it do
|
||||
expect(cause).to render_raw_variant(
|
||||
"#{I18n.t('journals.caused_changes.system_update')} " \
|
||||
"#{I18n.t('journals.cause_descriptions.system_update.sprint_migration', version_name: 'Sprint 1')}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the change was caused by an import" do
|
||||
subject(:cause) do
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user