From 4826e5257d7ce4cc3edf085b2307854211a26930 Mon Sep 17 00:00:00 2001 From: Bruno Pagno Date: Mon, 22 Jun 2026 14:25:34 +0200 Subject: [PATCH 1/2] include work package associated versions model --- app/models/version.rb | 7 ++ app/models/work_package.rb | 8 ++ app/models/work_package_version.rb | 36 ++++++++ config/locales/en.yml | 2 + ...0622100448_create_work_package_versions.rb | 46 ++++++++++ spec/models/work_package_spec.rb | 4 + spec/models/work_package_version_spec.rb | 86 +++++++++++++++++++ 7 files changed, 189 insertions(+) create mode 100644 app/models/work_package_version.rb create mode 100644 db/migrate/20260622100448_create_work_package_versions.rb create mode 100644 spec/models/work_package_version_spec.rb diff --git a/app/models/version.rb b/app/models/version.rb index 32349f211f85..1db3e5ef9303 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -34,6 +34,13 @@ class Version < ApplicationRecord belongs_to :project has_many :work_packages, dependent: :nullify + has_many :work_package_versions, dependent: :delete_all + has_many :targeted_work_packages, + -> { where(work_package_versions: { kind: "target" }) }, + through: :work_package_versions, source: :work_package + has_many :observed_in_work_packages, + -> { where(work_package_versions: { kind: "observed_in" }) }, + through: :work_package_versions, source: :work_package acts_as_customizable VERSION_STATUSES = %w(open locked closed).freeze diff --git a/app/models/work_package.rb b/app/models/work_package.rb index d4a110bbdd43..145b19d1161b 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -65,6 +65,14 @@ class WorkPackage < ApplicationRecord has_many :time_entries, dependent: :delete_all, inverse_of: :entity, as: :entity has_many :file_links, dependent: :delete_all, class_name: "Storages::FileLink", as: :container has_many :storages, through: :project + has_many :work_package_versions, dependent: :delete_all + has_many :versions, through: :work_package_versions, source: :version + has_many :target_versions, + -> { where(work_package_versions: { kind: "target" }) }, + through: :work_package_versions, source: :version + has_many :observed_in_versions, + -> { where(work_package_versions: { kind: "observed_in" }) }, + through: :work_package_versions, source: :version has_and_belongs_to_many :changesets, -> { # rubocop:disable Rails/HasAndBelongsToMany order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC") diff --git a/app/models/work_package_version.rb b/app/models/work_package_version.rb new file mode 100644 index 000000000000..cb0eda8bee4d --- /dev/null +++ b/app/models/work_package_version.rb @@ -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 WorkPackageVersion < ApplicationRecord + enum :kind, { target: 0, observed_in: 1 }, validate: true + + belongs_to :work_package + belongs_to :version +end diff --git a/config/locales/en.yml b/config/locales/en.yml index aea3d9e537ff..962fd5663cbd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2164,6 +2164,8 @@ en: type: "Type" version: "Version" watcher: "Watcher" + work_package_version: + kind: "Kind" ordered_persisted_query_entity: persisted_query: "Persisted query" entity: "Entity" diff --git a/db/migrate/20260622100448_create_work_package_versions.rb b/db/migrate/20260622100448_create_work_package_versions.rb new file mode 100644 index 000000000000..b3ade44bb66d --- /dev/null +++ b/db/migrate/20260622100448_create_work_package_versions.rb @@ -0,0 +1,46 @@ +# 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 CreateWorkPackageVersions < ActiveRecord::Migration[8.1] + def change + create_table :work_package_versions do |t| + t.references :work_package, null: false, foreign_key: { on_delete: :cascade }, index: false + t.references :version, null: false, foreign_key: { on_delete: :cascade }, index: false + t.integer :kind, null: false + t.timestamps + end + + add_index :work_package_versions, %i[work_package_id version_id kind], + unique: true, + name: "idx_wp_versions_on_wp_version_kind" + add_index :work_package_versions, :version_id, + name: "idx_wp_versions_on_version" + end +end diff --git a/spec/models/work_package_spec.rb b/spec/models/work_package_spec.rb index 29e53e57671b..4c92f053b472 100644 --- a/spec/models/work_package_spec.rb +++ b/spec/models/work_package_spec.rb @@ -84,6 +84,10 @@ it { is_expected.to have_many(:member_principals).through(:members).class_name("Principal").source(:principal) } it { is_expected.to have_many(:meeting_agenda_items) } it { is_expected.to have_many(:meetings).through(:meeting_agenda_items).source(:meeting) } + it { is_expected.to have_many(:work_package_versions).dependent(:delete_all) } + it { is_expected.to have_many(:versions).through(:work_package_versions).source(:version) } + it { is_expected.to have_many(:target_versions).through(:work_package_versions).source(:version) } + it { is_expected.to have_many(:observed_in_versions).through(:work_package_versions).source(:version) } end describe ".new" do diff --git a/spec/models/work_package_version_spec.rb b/spec/models/work_package_version_spec.rb new file mode 100644 index 000000000000..6ca8b94d42ff --- /dev/null +++ b/spec/models/work_package_version_spec.rb @@ -0,0 +1,86 @@ +# 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 WorkPackageVersion do + subject(:record) { described_class.new(work_package:, version:, kind: "target") } + + let(:work_package) { create(:work_package) } + let(:version) { create(:version, project: work_package.project) } + + describe "associations" do + it { is_expected.to belong_to(:work_package) } + it { is_expected.to belong_to(:version) } + end + + describe "validations" do + it { is_expected.to be_valid } + + it "is invalid with an unknown kind" do + record.kind = "unknown" + expect(record).not_to be_valid + expect(record.errors[:kind]).to be_present + end + + it "is valid with kind 'target'" do + record.kind = "target" + expect(record).to be_valid + end + + it "is valid with kind 'observed_in'" do + record.kind = "observed_in" + expect(record).to be_valid + end + end + + describe "kind scoping via through associations" do + let(:other_version) { create(:version, project: work_package.project) } + + before do + described_class.create!(work_package:, version:, kind: "target") + described_class.create!(work_package:, version: other_version, kind: "observed_in") + end + + it "target version appears in target_versions but not observed_in_versions" do + expect(work_package.target_versions).to include(version) + expect(work_package.observed_in_versions).not_to include(version) + end + + it "observed_in version appears in observed_in_versions but not target_versions" do + expect(work_package.observed_in_versions).to include(other_version) + expect(work_package.target_versions).not_to include(other_version) + end + + it "all versions appear in work_package.versions" do + expect(work_package.versions).to include(version, other_version) + end + end +end From d375c364392d6f1ec8562421a84f32552811756c Mon Sep 17 00:00:00 2001 From: Bruno Pagno Date: Tue, 23 Jun 2026 11:09:02 +0200 Subject: [PATCH 2/2] use text field to persist enum data --- app/models/work_package_version.rb | 2 +- .../20260622100448_create_work_package_versions.rb | 4 +++- spec/models/work_package_version_spec.rb | 12 ++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/models/work_package_version.rb b/app/models/work_package_version.rb index cb0eda8bee4d..903105fa6b19 100644 --- a/app/models/work_package_version.rb +++ b/app/models/work_package_version.rb @@ -29,7 +29,7 @@ #++ class WorkPackageVersion < ApplicationRecord - enum :kind, { target: 0, observed_in: 1 }, validate: true + enum :kind, { target: "target", observed_in: "observed_in" }, validate: true belongs_to :work_package belongs_to :version diff --git a/db/migrate/20260622100448_create_work_package_versions.rb b/db/migrate/20260622100448_create_work_package_versions.rb index b3ade44bb66d..084d9712c8c8 100644 --- a/db/migrate/20260622100448_create_work_package_versions.rb +++ b/db/migrate/20260622100448_create_work_package_versions.rb @@ -30,10 +30,12 @@ class CreateWorkPackageVersions < ActiveRecord::Migration[8.1] def change + create_enum :work_package_version_kind, ["target", "observed_in"] + create_table :work_package_versions do |t| t.references :work_package, null: false, foreign_key: { on_delete: :cascade }, index: false t.references :version, null: false, foreign_key: { on_delete: :cascade }, index: false - t.integer :kind, null: false + t.enum :kind, enum_type: :work_package_version_kind, null: false t.timestamps end diff --git a/spec/models/work_package_version_spec.rb b/spec/models/work_package_version_spec.rb index 6ca8b94d42ff..c76318b2762b 100644 --- a/spec/models/work_package_version_spec.rb +++ b/spec/models/work_package_version_spec.rb @@ -50,14 +50,10 @@ expect(record.errors[:kind]).to be_present end - it "is valid with kind 'target'" do - record.kind = "target" - expect(record).to be_valid - end - - it "is valid with kind 'observed_in'" do - record.kind = "observed_in" - expect(record).to be_valid + it do + expect(subject).to define_enum_for(:kind) + .with_values(target: "target", observed_in: "observed_in") + .backed_by_column_of_type(:enum) end end