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
2 changes: 1 addition & 1 deletion app/models/activities/fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def journals_of_event_set(events)
journal_ids = events.map(&:event_id)

Journal
.includes(:data, :customizable_journals, :attachable_journals, :target_version_journals,
.includes(:data, :customizable_journals, :attachable_journals, :target_version_journals, :label_journals,
:observed_in_version_journals, :bcf_comment)
Comment on lines +126 to 127

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.

This is one of the unnecessariest things ever, but it feels incorrect to have :label between :target_version and :observed_in_version 😅

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Some say some distance is healthy in relationships 😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Also contemplating moving these values into a constant as this list is fairly large

.find(journal_ids)
.then { |journals| ::API::V3::Activities::ActivityEagerLoadingWrapper.wrap(journals) }
Expand Down
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 @@ -263,6 +263,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 @@ -275,12 +276,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 @@ -291,19 +292,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
Comment thread
akabiru marked this conversation as resolved.
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 @@ -68,6 +68,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 @@ -90,7 +91,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)
Comment thread
akabiru marked this conversation as resolved.
end
end

Expand Down Expand Up @@ -143,6 +157,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 @@ -155,11 +176,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 @@ -56,6 +56,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 @@ -136,6 +137,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 @@ -33,7 +33,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
2 changes: 1 addition & 1 deletion app/services/work_packages/activities_tab/paginator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def with_changesets(scope)
def page_journals(page_relation)
page_relation
.includes(:user, :customizable_journals, :attachable_journals, :storable_journals, :target_version_journals,
:observed_in_version_journals, :notifications, :attachments)
:label_journals, :observed_in_version_journals, :notifications, :attachments)
.to_a
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
# * Custom field changes (compares customizable_journals with predecessor)
# * File link changes (compares storages_file_links_journals with predecessor)
# * Target/observed-in version changes (compares work_package_version_journals with predecessor)
# * Label changes (compares label_journals with predecessor)
# * Cause metadata (system-triggered changes)
# * Attribute/data changes (compares work_package_journals columns with immediate predecessor)
#
Expand Down Expand Up @@ -87,6 +88,7 @@ def changes_condition_sql
OR EXISTS (#{custom_field_changes_condition_sql})
OR EXISTS (#{file_link_changes_condition_sql})
OR EXISTS (#{version_changes_condition_sql})
OR EXISTS (#{label_changes_condition_sql})
SQL
end

Expand Down Expand Up @@ -136,6 +138,14 @@ def version_changes_condition_sql
)
end

def label_changes_condition_sql
association_changes_condition_sql(
table: Journal::LabelJournal.table_name,
join_columns: ["label_id"],
value_columns: []
)
end

def data_changes_condition_sql
data_change_columns = Journal::WorkPackageJournal.column_names - ["id"]

Expand Down
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,9 @@ en:
issue_priority:
one: "Priority"
other: "Priorities"
label:
one: "Label"
other: "Labels"
ldap_auth_source:
one: "LDAP connection"
other: "LDAP connections"
Expand Down Expand Up @@ -4046,6 +4049,7 @@ en:
label_deleted_custom_field: "(deleted custom field)"
label_deleted_custom_item: "(deleted item)"
label_deleted_custom_option: "(deleted option)"
label_deleted_label: "(deleted label)"
label_deleted_version: "(deleted version)"
label_departments: "Organization"
label_departments_description_html: >
Expand Down
39 changes: 39 additions & 0 deletions db/migrate/20261021120000_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
Comment thread
akabiru marked this conversation as resolved.
end
end
end
2 changes: 1 addition & 1 deletion lib/api/v3/activities/activity_eager_loading_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def predecessor_journals(journals)
SQL
)
.includes(:attachable_journals, :customizable_journals, :storable_journals, :target_version_journals,
:observed_in_version_journals)
:label_journals, :observed_in_version_journals)
end
end
end
Expand Down
Loading
Loading