Commit 26fa3733 authored by Alexandru Croitor's avatar Alexandru Croitor Committed by Patrick Derichs

Generate weight change notes as synthetic notes

We are adding weich change tracking events similarly to label events
and in order to keep consistent and not duplicate system notes
we are generating the notes out of tracked weight change events.
parent 682e7701
...@@ -8,4 +8,17 @@ class ResourceWeightEvent < ApplicationRecord ...@@ -8,4 +8,17 @@ class ResourceWeightEvent < ApplicationRecord
belongs_to :issue belongs_to :issue
scope :by_issue, ->(issue) { where(issue_id: issue.id) } scope :by_issue, ->(issue) { where(issue_id: issue.id) }
scope :created_after, ->(time) { where('created_at > ?', time) }
def discussion_id(resource = nil)
strong_memoize(:discussion_id) do
Digest::SHA1.hexdigest(discussion_id_key.join("-"))
end
end
private
def discussion_id_key
[self.class.name, created_at, user_id]
end
end end
# frozen_string_literal: true
class WeightNote < Note
attr_accessor :resource_parent, :event
def self.from_event(event, resource: nil, resource_parent: nil)
resource ||= event.issue
attrs = {
system: true,
author: event.user,
created_at: event.created_at,
noteable: resource,
event: event,
system_note_metadata: SystemNoteMetadata.new(action: 'weight'),
resource_parent: resource_parent
}
if resource_parent.is_a?(Project)
attrs[:project_id] = resource_parent.id
end
WeightNote.new(attrs)
end
def note
@note ||= note_text
end
def note_html
@note_html ||= "<p dir=\"auto\">#{note_text(html: true)}</p>"
end
def project
resource_parent if resource_parent.is_a?(Project)
end
def group
resource_parent if resource_parent.is_a?(Group)
end
private
def note_text(html: false)
weight_text = html ? "<strong>#{event.weight}</strong>" : event.weight
event.weight ? "changed weight to #{weight_text}" : 'removed the weight'
end
end
# frozen_string_literal: true
# We store events about issuable label changes in a separate table (not as
# other system notes), but we still want to display notes about label changes
# as classic system notes in UI. This service generates "synthetic" notes for
# label event changes.
module ResourceEvents
class BaseSyntheticNotesBuilderService
include Gitlab::Utils::StrongMemoize
attr_reader :resource, :current_user, :params
def initialize(resource, current_user, params = {})
@resource = resource
@current_user = current_user
@params = params
end
def execute
synthetic_notes
end
private
def since_fetch_at(events)
return events unless params[:last_fetched_at].present?
last_fetched_at = Time.at(params.fetch(:last_fetched_at).to_i)
events.created_after(last_fetched_at - NotesFinder::FETCH_OVERLAP)
end
def resource_parent
strong_memoize(:resource_parent) do
resource.project || resource.group
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
# We store events about issuable label changes in a separate table (not as # We store events about issuable label changes and weight changes in separate tables (not as
# other system notes), but we still want to display notes about label changes # other system notes), but we still want to display notes about label and weight changes
# as classic system notes in UI. This service generates "synthetic" notes for # as classic system notes in UI. This service merges synthetic label and weight notes
# label event changes and merges them with classic notes and sorts them by # with classic notes and sorts them by creation time.
# creation time.
module ResourceEvents module ResourceEvents
class MergeIntoNotesService class MergeIntoNotesService
...@@ -19,39 +18,15 @@ module ResourceEvents ...@@ -19,39 +18,15 @@ module ResourceEvents
end end
def execute(notes = []) def execute(notes = [])
(notes + label_notes).sort_by { |n| n.created_at } (notes + synthetic_notes).sort_by { |n| n.created_at }
end end
private private
def label_notes def synthetic_notes
label_events_by_discussion_id.map do |discussion_id, events| SyntheticLabelNotesBuilderService.new(resource, current_user, params).execute
LabelNote.from_events(events, resource: resource, resource_parent: resource_parent)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def label_events_by_discussion_id
return [] unless resource.respond_to?(:resource_label_events)
events = resource.resource_label_events.includes(:label, user: :status)
events = since_fetch_at(events)
events.group_by { |event| event.discussion_id }
end
# rubocop: enable CodeReuse/ActiveRecord
def since_fetch_at(events)
return events unless params[:last_fetched_at].present?
last_fetched_at = Time.at(params.fetch(:last_fetched_at).to_i)
events.created_after(last_fetched_at - NotesFinder::FETCH_OVERLAP)
end
def resource_parent
strong_memoize(:resource_parent) do
resource.project || resource.group
end
end end
end end
end end
ResourceEvents::MergeIntoNotesService.prepend_if_ee('EE::ResourceEvents::MergeIntoNotesService')
# frozen_string_literal: true
# We store events about issuable label changes in a separate table (not as
# other system notes), but we still want to display notes about label changes
# as classic system notes in UI. This service generates "synthetic" notes for
# label event changes.
module ResourceEvents
class SyntheticLabelNotesBuilderService < BaseSyntheticNotesBuilderService
private
def synthetic_notes
label_events_by_discussion_id.map do |discussion_id, events|
LabelNote.from_events(events, resource: resource, resource_parent: resource_parent)
end
end
def label_events_by_discussion_id
return [] unless resource.respond_to?(:resource_label_events)
events = resource.resource_label_events.includes(:label, user: :status) # rubocop: disable CodeReuse/ActiveRecord
events = since_fetch_at(events)
events.group_by { |event| event.discussion_id }
end
end
end
...@@ -5,7 +5,7 @@ class CreateResourceWeightEvent < ActiveRecord::Migration[5.2] ...@@ -5,7 +5,7 @@ class CreateResourceWeightEvent < ActiveRecord::Migration[5.2]
def change def change
create_table :resource_weight_events do |t| create_table :resource_weight_events do |t|
t.references :user, null: false, foreign_key: { on_delete: :cascade }, t.references :user, null: false, foreign_key: { on_delete: :nullify },
index: { name: 'index_resource_weight_events_on_user_id' } index: { name: 'index_resource_weight_events_on_user_id' }
t.references :issue, null: false, foreign_key: { on_delete: :cascade }, t.references :issue, null: false, foreign_key: { on_delete: :cascade },
index: false index: false
......
...@@ -11,7 +11,7 @@ module EE ...@@ -11,7 +11,7 @@ module EE
super super
ActiveRecord::Base.no_touching do ActiveRecord::Base.no_touching do
handle_weight_change_note handle_weight_change
handle_date_change_note if is_update handle_date_change_note if is_update
end end
end end
...@@ -28,26 +28,15 @@ module EE ...@@ -28,26 +28,15 @@ module EE
end end
end end
def handle_weight_change_note def handle_weight_change
if !issuable.is_a?(Epic) && issuable.previous_changes.include?('weight')
note = create_weight_change_note
track_weight_change(note)
end
end
def create_weight_change_note
::SystemNoteService.change_weight_note(issuable, issuable.project, current_user)
end
def track_weight_change(note)
return unless weight_changes_tracking_enabled? return unless weight_changes_tracking_enabled?
return unless issuable.previous_changes.include?('weight')
EE::ResourceEvents::ChangeWeightService.new([issuable], current_user, note.created_at).execute EE::ResourceEvents::ChangeWeightService.new([issuable], current_user, Time.now).execute
end end
def weight_changes_tracking_enabled? def weight_changes_tracking_enabled?
::Feature.enabled?(:track_issue_weight_change_events, issuable.project) !issuable.is_a?(Epic) && ::Feature.enabled?(:track_issue_weight_change_events, issuable.project)
end end
end end
end end
......
...@@ -12,7 +12,10 @@ module EE ...@@ -12,7 +12,10 @@ module EE
end end
def execute def execute
::Gitlab::Database.bulk_insert(ResourceWeightEvent.table_name, resource_weight_changes) unless resource_weight_changes.empty? unless resource_weight_changes.empty?
::Gitlab::Database.bulk_insert(ResourceWeightEvent.table_name, resource_weight_changes)
resources.each(&:expire_note_etag_cache)
end
end end
private private
......
# frozen_string_literal: true
module EE
module ResourceEvents
module MergeIntoNotesService
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
private
override :synthetic_notes
def synthetic_notes
super + SyntheticWeightNotesBuilderService.new(resource, current_user, params).execute
end
end
end
end
# frozen_string_literal: true
# We store events about issue weight changes in a separate table,
# but we still want to display notes about weight changes
# as classic system notes in UI. This service generates "synthetic" notes for
# weight event changes.
module EE
module ResourceEvents
class SyntheticWeightNotesBuilderService < ::ResourceEvents::BaseSyntheticNotesBuilderService
private
def synthetic_notes
weight_change_events.map do |event|
WeightNote.from_event(event, resource: resource, resource_parent: resource_parent)
end
end
def weight_change_events
return [] unless resource.respond_to?(:resource_weight_events)
events = resource.resource_weight_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
since_fetch_at(events)
end
end
end
end
...@@ -348,7 +348,7 @@ describe API::Issues, :mailer do ...@@ -348,7 +348,7 @@ describe API::Issues, :mailer do
it 'adds a note when the weight is changed' do it 'adds a note when the weight is changed' do
expect do expect do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { weight: 9 } put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { weight: 9 }
end.to change { Note.count }.by(1) end.to change { ResourceWeightEvent.count }.by(1)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['weight']).to eq(9) expect(json_response['weight']).to eq(9)
......
...@@ -8,7 +8,20 @@ describe Issuable::CommonSystemNotesService do ...@@ -8,7 +8,20 @@ describe Issuable::CommonSystemNotesService do
let(:issuable) { create(:issue) } let(:issuable) { create(:issue) }
context 'on issuable update' do context 'on issuable update' do
it_behaves_like 'system note creation', { weight: 5 }, 'changed weight to **5**' context 'when weight is changed' do
before do
issuable.update!(weight: 5)
end
it 'creates a resource label event' do
described_class.new(project, user).execute(issuable, old_labels: [])
event = issuable.reload.resource_weight_events.last
expect(event).not_to be_nil
expect(event.weight).to eq 5
expect(event.user_id).to eq user.id
end
end
context 'when issuable is an epic' do context 'when issuable is an epic' do
let(:timestamp) { Time.now } let(:timestamp) { Time.now }
...@@ -40,9 +53,8 @@ describe Issuable::CommonSystemNotesService do ...@@ -40,9 +53,8 @@ describe Issuable::CommonSystemNotesService do
issuable.save issuable.save
end end
it 'creates a system note for weight' do it 'does not create a common system note for weight' do
expect { subject }.to change { issuable.notes.count }.from(0).to(1) expect { subject }.not_to change { issuable.notes.count }
expect(issuable.notes.last.note).to match('changed weight')
end end
context 'when resource weight event tracking is enabled' do context 'when resource weight event tracking is enabled' do
...@@ -53,11 +65,10 @@ describe Issuable::CommonSystemNotesService do ...@@ -53,11 +65,10 @@ describe Issuable::CommonSystemNotesService do
it 'creates a resource weight event' do it 'creates a resource weight event' do
subject subject
note = issuable.notes.last
event = issuable.resource_weight_events.last event = issuable.resource_weight_events.last
expect(event.weight).to eq(5) expect(event.weight).to eq(5)
expect(event.created_at).to eq(note.created_at) expect(event.user_id).to eq(user.id)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment