Skip to content
Draft
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
32 changes: 24 additions & 8 deletions app/models/journable/with_historic_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ def changes_at_timestamp(timestamp)

merge_target_versions_changes!(changes, historic_journable)
merge_observed_in_versions_changes!(changes, historic_journable)
merge_labels_changes!(changes, historic_journable)

changes
end
Expand All @@ -276,12 +277,12 @@ def merge_target_versions_changes!(changes, historic_journable)
end

def target_versions_changes(historic_journable)
old_ids = sorted_version_ids(historic_journable, :target_versions)
new_ids = sorted_version_ids(__getobj__, :target_versions)
old_ids = sorted_association_ids(historic_journable, :target_versions)
new_ids = sorted_association_ids(__getobj__, :target_versions)

{}.tap do |changes|
changes["version_id"] = [old_ids.first, new_ids.first] if old_ids.first != new_ids.first
changes["target_versions"] = [old_ids, new_ids].map { joined_version_ids(it) } if old_ids != new_ids
changes["target_versions"] = [old_ids, new_ids].map { joined_ids(it) } if old_ids != new_ids
end
end

Expand All @@ -292,19 +293,34 @@ def merge_observed_in_versions_changes!(changes, historic_journable)
end

def observed_in_versions_changes(historic_journable)
old_ids = sorted_version_ids(historic_journable, :observed_in_versions)
new_ids = sorted_version_ids(__getobj__, :observed_in_versions)
old_ids = sorted_association_ids(historic_journable, :observed_in_versions)
new_ids = sorted_association_ids(__getobj__, :observed_in_versions)

{}.tap do |changes|
changes["observed_in_versions"] = [old_ids, new_ids].map { joined_version_ids(it) } if old_ids != new_ids
changes["observed_in_versions"] = [old_ids, new_ids].map { joined_ids(it) } if old_ids != new_ids
end
end

def sorted_version_ids(work_package, association)
def merge_labels_changes!(changes, historic_journable)
return unless __getobj__.respond_to?(:labels)

changes.merge!(labels_changes(historic_journable))
end

def labels_changes(historic_journable)
old_ids = sorted_association_ids(historic_journable, :labels)
new_ids = sorted_association_ids(__getobj__, :labels)

{}.tap do |changes|
changes["labels"] = [old_ids, new_ids].map { joined_ids(it) } if old_ids != new_ids
end
end

def sorted_association_ids(work_package, association)
work_package.public_send(association).map(&:id).sort
end

def joined_version_ids(ids)
def joined_ids(ids)
ids.join(",").presence
end

Expand Down
29 changes: 24 additions & 5 deletions app/models/journable/with_historic_attributes/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def load_journal_associations(journalized = journables)
load_custom_value_associations(journalized, journal_ids)
load_versions_associations(journalized, journal_ids, kind: "target", association: :target_versions)
load_versions_associations(journalized, journal_ids, kind: "observed_in", association: :observed_in_versions)
load_labels_association(journalized, journal_ids)

journalized
end
Expand All @@ -91,7 +92,20 @@ def load_versions_associations(journalized, journal_ids, kind:, association:)

journalized.each do |work_package|
version_journals = Array(version_journals_by_journal_id[work_package.journal_id])
set_versions_association_from_journal!(work_package:, version_journals:, association:)
historic_versions = version_journals.filter_map(&:version).sort_by(&:id)
set_association_from_journal!(work_package:, records: historic_versions, association:)
end
end

def load_labels_association(journalized, journal_ids)
return unless journalized_class.method_defined?(:labels)

label_journals_by_journal_id = load_label_journals_by_journal_id(journal_ids)

journalized.each do |work_package|
label_journals = Array(label_journals_by_journal_id[work_package.journal_id])
historic_labels = label_journals.filter_map(&:label).sort_by(&:id)
set_association_from_journal!(work_package:, records: historic_labels, association: :labels)
end
end

Expand Down Expand Up @@ -144,6 +158,13 @@ def load_version_journals_by_journal_id(journal_ids, kind:)
.group_by(&:journal_id)
end

def load_label_journals_by_journal_id(journal_ids)
Journal::LabelJournal
.where(journal_id: journal_ids)
.includes(:label)
.group_by(&:journal_id)
end

def set_custom_value_association_from_journal!(work_package:, customizable_journals:)
# Build the associated customizable_journals as custom values, this way the historic work packages
# will behave just as the normal ones. Additionally set the reverse customized association
Expand All @@ -156,11 +177,9 @@ def set_custom_value_association_from_journal!(work_package:, customizable_journ
work_package.association(:custom_values).target = historic_custom_values
end

def set_versions_association_from_journal!(work_package:, version_journals:, association:)
historic_versions = version_journals.filter_map(&:version).sort_by(&:id)

def set_association_from_journal!(work_package:, records:, association:)
work_package.association(association).loaded!
work_package.association(association).target = historic_versions
work_package.association(association).target = records
end

attr_accessor :journables
Expand Down
2 changes: 2 additions & 0 deletions app/models/journal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Journal < ApplicationRecord
register_journal_formatter OpenProject::JournalFormatter::Diff
register_journal_formatter OpenProject::JournalFormatter::FileLink
register_journal_formatter OpenProject::JournalFormatter::IgnoreNonWorkingDays
register_journal_formatter OpenProject::JournalFormatter::Labels
register_journal_formatter OpenProject::JournalFormatter::MeetingStartTime
register_journal_formatter OpenProject::JournalFormatter::MeetingState
register_journal_formatter OpenProject::JournalFormatter::MeetingWorkPackageId
Expand Down Expand Up @@ -133,6 +134,7 @@ class Journal < ApplicationRecord
has_many :custom_comment_journals, class_name: "Journal::CustomCommentJournal", dependent: :delete_all
has_many :project_phase_journals, class_name: "Journal::ProjectPhaseJournal", dependent: :delete_all
has_many :storable_journals, class_name: "Journal::StorableJournal", dependent: :delete_all
has_many :label_journals, class_name: "Journal::LabelJournal", dependent: :delete_all
has_many :work_package_version_journals, class_name: "Journal::WorkPackageVersionJournal", dependent: :delete_all
# Row lifecycle is owned by work_package_version_journals above.
# rubocop:disable Rails/HasManyOrHasOneDependent
Expand Down
35 changes: 35 additions & 0 deletions app/models/journal/label_journal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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 Journal::LabelJournal < Journal::AssociatedJournal
self.table_name = "label_journals"

belongs_to :label
end
1 change: 1 addition & 0 deletions app/models/work_package/journalized.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def self.event_url
register_journal_formatted_fields "project_phase_definition_id", formatter_key: :project_phase_definition
register_journal_formatted_fields "target_versions", formatter_key: :target_versions
register_journal_formatted_fields "observed_in_versions", formatter_key: :observed_in_versions
register_journal_formatted_fields "labels", formatter_key: :labels

# Joined
register_journal_formatted_fields :parent_id, :project_id,
Expand Down
2 changes: 1 addition & 1 deletion app/services/journals/create_service/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Association

# Core associations are defined here. Module-specific associations can be defined in engines
# using `Journals::CreateService::Association.register`.
@registry = Set.new(%i[Attachable CustomComment Customizable ProjectPhase WorkPackageVersion])
@registry = Set.new(%i[Attachable CustomComment Customizable Labelable ProjectPhase WorkPackageVersion])

class << self
def register(*names)
Expand Down
86 changes: 86 additions & 0 deletions app/services/journals/create_service/labelable.rb
Original file line number Diff line number Diff line change
@@ -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.
#++

class Journals::CreateService
class Labelable < Association
def associated?
journable.respond_to?(:labels)
end

def cleanup_predecessor(predecessor, notes, cause)
cleanup_predecessor_for(predecessor,
notes,
cause,
"label_journals",
:journal_id,
:id)
end

def insert_sql
sanitize(<<~SQL.squish, journable_id:, labelable_type: journable_class_name)
INSERT INTO
label_journals (
journal_id,
label_id
)
SELECT
#{id_from_inserted_journal_sql},
labelings.label_id
FROM labelings
WHERE
#{only_if_created_sql}
AND labelings.labelable_type = :labelable_type
AND labelings.labelable_id = :journable_id
SQL
end

def changes_sql
sanitize(<<~SQL.squish, journable_id:, labelable_type: journable_class_name)
SELECT
:journable_id AS journable_id
FROM
(
SELECT ARRAY_AGG(label_id ORDER BY label_id) AS labels
FROM labelings
WHERE labelings.labelable_type = :labelable_type
AND labelings.labelable_id = :journable_id
) current_labels
CROSS JOIN
(
SELECT ARRAY_AGG(label_id ORDER BY label_id) AS labels
FROM label_journals
WHERE journal_id IN (SELECT id FROM max_journals)
) journal_labels
WHERE
current_labels.labels IS DISTINCT FROM journal_labels.labels
SQL
end
end
end
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ en:
title: "Working days"
true: "include non-working days"
journal_internal: Internal Journal
labels: "Labels"
notify: "Notify" # used in custom actions
observed_in_versions: "Observed in versions"
parent: "Parent"
Expand Down Expand Up @@ -1163,6 +1164,9 @@ en:
issue_priority:
one: "Priority"
other: "Priorities"
label:
one: "Label"
other: "Labels"
ldap_auth_source:
one: "LDAP connection"
other: "LDAP connections"
Expand Down
39 changes: 39 additions & 0 deletions db/migrate/20260914130000_create_label_journals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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 CreateLabelJournals < ActiveRecord::Migration[8.1]
def change
create_table :label_journals do |t| # rubocop:disable Rails/CreateTableWithTimestamps
t.belongs_to :journal, null: false, foreign_key: true
# No foreign key: journal snapshots must outlive deleted labels.
t.belongs_to :label, null: false
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@
# See COPYRIGHT and LICENSE files for more details.
#++

# Base for formatters rendering the change to a set of versions referenced by a
# work package. Each value is the sorted, comma-joined version ids (see
# JournalChanges); every id is resolved to the version's name, dropping
# versions that have been deleted or are no longer visible to the reader.
class OpenProject::JournalFormatter::JoinedVersions < JournalFormatter::NamedAssociation
# Base for formatters rendering the change to a set of associated records
# referenced by the journable. Each value is the sorted, comma-joined ids;
# deleted or non-visible records are dropped.
class OpenProject::JournalFormatter::JoinedAssociation < JournalFormatter::NamedAssociation
private

def format_values(values, key)
Expand Down
34 changes: 34 additions & 0 deletions lib/open_project/journal_formatter/labels.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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.
#++

# Renders the change to the set of labels attached to a work package
# (see JournalChanges#get_labels_changes).
class OpenProject::JournalFormatter::Labels < OpenProject::JournalFormatter::JoinedAssociation
end
2 changes: 1 addition & 1 deletion lib/open_project/journal_formatter/observed_in_versions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@

# Renders the change to the set of versions a work package was observed in
# (see JournalChanges#get_observed_in_versions_changes).
class OpenProject::JournalFormatter::ObservedInVersions < OpenProject::JournalFormatter::JoinedVersions
class OpenProject::JournalFormatter::ObservedInVersions < OpenProject::JournalFormatter::JoinedAssociation
end
Loading
Loading