Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/models/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions app/models/work_package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
36 changes: 36 additions & 0 deletions app/models/work_package_version.rb
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could name this "WorkPackage::Version". (Then the file goes to app/models/work_package/version). This helps to keep the global namespace overseeable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of scoping models, but I don't like having two models called version.rb. That and the fact that WorkPackage & Version are both core models of OpenProject. We could discuss for example if the model should be scoped the other way around Version::WorKPackage.

So at this moment I'd prefer to keep it work_package_version.

Open to being convinced otherwise, though.

@thykel thykel Jun 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since WorkPackageVersion is a join table, I don't think the namespacing semantics apply here, and we're better off keeping it as-is.

With that said, it would be nice to pin down a single specific convention in place for these join tables -- AFAIK this is one of the two that are currently in use (= just concatenating the names of both models).

enum :kind, { target: "target", observed_in: "observed_in" }, validate: true

belongs_to :work_package
belongs_to :version
end
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
48 changes: 48 additions & 0 deletions db/migrate/20260622100448_create_work_package_versions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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_enum :work_package_version_kind, ["target", "observed_in"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL! Nice one, cheers Tom! #23841 (comment)


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.enum :kind, enum_type: :work_package_version_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
4 changes: 4 additions & 0 deletions spec/models/work_package_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions spec/models/work_package_version_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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 do
expect(subject).to define_enum_for(:kind)
.with_values(target: "target", observed_in: "observed_in")
.backed_by_column_of_type(:enum)
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
Loading