Commit ca9ef551 authored by Alex Kalderimis's avatar Alex Kalderimis

Add support for event creation on designs

Add methods for adding design events to the EventCreateService, and
uses these methods in the design management services.

A new spec support class is added (MultiChangeValue) to help testing
several change values at once, without having to perform the action
multiple times.

Deletion in particular is made efficient by ensuring that we perform
just a single insert when creating events.
parent 59d52d9a
...@@ -20,6 +20,8 @@ module DesignManagement ...@@ -20,6 +20,8 @@ module DesignManagement
has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: 'DesignUserMention', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :user_mentions, class_name: 'DesignUserMention', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :project, :filename, presence: true validates :project, :filename, presence: true
validates :issue, presence: true, unless: :importing? validates :issue, presence: true, unless: :importing?
validates :filename, uniqueness: { scope: :issue_id }, length: { maximum: 255 } validates :filename, uniqueness: { scope: :issue_id }, length: { maximum: 255 }
...@@ -189,6 +191,11 @@ module DesignManagement ...@@ -189,6 +191,11 @@ module DesignManagement
alias_method :after_note_created, :after_note_changed alias_method :after_note_created, :after_note_changed
alias_method :after_note_destroyed, :after_note_changed alias_method :after_note_destroyed, :after_note_changed
# Part of the interface of objects we can create events about
def resource_parent
project
end
private private
def head_version def head_version
......
...@@ -15,6 +15,7 @@ module DesignManagement ...@@ -15,6 +15,7 @@ module DesignManagement
return error('Forbidden!') unless can_delete_designs? return error('Forbidden!') unless can_delete_designs?
version = delete_designs! version = delete_designs!
EventCreateService.new.destroy_designs(designs, current_user)
success(version: version) success(version: version)
end end
...@@ -48,7 +49,9 @@ module DesignManagement ...@@ -48,7 +49,9 @@ module DesignManagement
end end
def design_action(design) def design_action(design)
on_success { counter.count(:delete) } on_success do
counter.count(:delete)
end
DesignManagement::DesignAction.new(design, :delete) DesignManagement::DesignAction.new(design, :delete)
end end
......
...@@ -20,6 +20,7 @@ module DesignManagement ...@@ -20,6 +20,7 @@ module DesignManagement
uploaded_designs, version = upload_designs! uploaded_designs, version = upload_designs!
skipped_designs = designs - uploaded_designs skipped_designs = designs - uploaded_designs
create_events
success({ designs: uploaded_designs, version: version, skipped_designs: skipped_designs }) success({ designs: uploaded_designs, version: version, skipped_designs: skipped_designs })
rescue ::ActiveRecord::RecordInvalid => e rescue ::ActiveRecord::RecordInvalid => e
error(e.message) error(e.message)
...@@ -47,7 +48,7 @@ module DesignManagement ...@@ -47,7 +48,7 @@ module DesignManagement
end end
def build_actions def build_actions
files.zip(designs).flat_map do |(file, design)| @actions ||= files.zip(designs).flat_map do |(file, design)|
Array.wrap(build_design_action(file, design)) Array.wrap(build_design_action(file, design))
end end
end end
...@@ -57,7 +58,9 @@ module DesignManagement ...@@ -57,7 +58,9 @@ module DesignManagement
return if design_unchanged?(design, content) return if design_unchanged?(design, content)
action = new_file?(design) ? :create : :update action = new_file?(design) ? :create : :update
on_success { ::Gitlab::UsageDataCounters::DesignsCounter.count(action) } on_success do
::Gitlab::UsageDataCounters::DesignsCounter.count(action)
end
DesignManagement::DesignAction.new(design, action, content) DesignManagement::DesignAction.new(design, action, content)
end end
...@@ -67,6 +70,16 @@ module DesignManagement ...@@ -67,6 +70,16 @@ module DesignManagement
content == existing_blobs[design]&.data content == existing_blobs[design]&.data
end end
def create_events
by_action = @actions.group_by(&:action).transform_values { |grp| grp.map(&:design) }
event_create_service.save_designs(current_user, **by_action)
end
def event_create_service
@event_create_service ||= EventCreateService.new
end
def commit_message def commit_message
<<~MSG <<~MSG
Updated #{files.size} #{'designs'.pluralize(files.size)} Updated #{files.size} #{'designs'.pluralize(files.size)}
......
...@@ -96,6 +96,29 @@ class EventCreateService ...@@ -96,6 +96,29 @@ class EventCreateService
create_push_event(BulkPushEventPayloadService, project, current_user, push_data) create_push_event(BulkPushEventPayloadService, project, current_user, push_data)
end end
def save_designs(current_user, create: [], update: [])
created = create.group_by(&:project).flat_map do |project, designs|
Feature.enabled?(:design_activity_events, project) ? designs : []
end.to_set
updated = update.group_by(&:project).flat_map do |project, designs|
Feature.enabled?(:design_activity_events, project) ? designs : []
end.to_set
return [] if created.empty? && updated.empty?
records = created.zip([:created].cycle) + updated.zip([:updated].cycle)
create_record_events(records, current_user)
end
def destroy_designs(designs, current_user)
designs = designs.select do |design|
Feature.enabled?(:design_activity_events, design.project)
end
return [] unless designs.present?
create_record_events(designs.zip([:destroyed].cycle), current_user)
end
# Create a new wiki page event # Create a new wiki page event
# #
# @param [WikiPage::Meta] wiki_page_meta The event target # @param [WikiPage::Meta] wiki_page_meta The event target
...@@ -134,7 +157,32 @@ class EventCreateService ...@@ -134,7 +157,32 @@ class EventCreateService
end end
def create_record_event(record, current_user, status) def create_record_event(record, current_user, status)
create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name) create_event(record.resource_parent, current_user, status,
target_id: record.id, target_type: record.class.name)
end
# If creating several events, this method will insert them all in a single
# statement
#
# @param [[Eventable, Symbol]] a list of pairs of records and a valid status
# @param [User] the author of the event
def create_record_events(pairs, current_user)
base_attrs = {
created_at: Time.now.utc,
updated_at: Time.now.utc,
author_id: current_user.id
}
attribute_sets = pairs.map do |record, status|
action = Event.actions[status]
raise IllegalActionError, "#{status} is not a valid status" if action.nil?
parent_attrs(record.resource_parent)
.merge(base_attrs)
.merge(action: action, target_id: record.id, target_type: record.class.name)
end
Event.insert_all(attribute_sets, returning: %w[id])
end end
def create_push_event(service_class, project, current_user, push_data) def create_push_event(service_class, project, current_user, push_data)
...@@ -160,16 +208,22 @@ class EventCreateService ...@@ -160,16 +208,22 @@ class EventCreateService
action: status, action: status,
author_id: current_user.id author_id: current_user.id
) )
attributes.merge!(parent_attrs(resource_parent))
Event.create!(attributes)
end
def parent_attrs(resource_parent)
resource_parent_attr = case resource_parent resource_parent_attr = case resource_parent
when Project when Project
:project :project_id
when Group when Group
:group :group_id
end end
attributes[resource_parent_attr] = resource_parent if resource_parent_attr
Event.create!(attributes) return {} unless resource_parent_attr
{ resource_parent_attr => resource_parent.id }
end end
def create_resource_event(issuable, current_user, status) def create_resource_event(issuable, current_user, status)
......
...@@ -56,6 +56,10 @@ describe DesignManagement::DeleteDesignsService do ...@@ -56,6 +56,10 @@ describe DesignManagement::DeleteDesignsService do
let(:enabled) { false } let(:enabled) { false }
it_behaves_like "a service error" it_behaves_like "a service error"
it 'does not create any events in the activity stream' do
expect { run_service rescue nil }.not_to change { Event.count }
end
end end
context "when the feature is available" do context "when the feature is available" do
...@@ -72,7 +76,9 @@ describe DesignManagement::DeleteDesignsService do ...@@ -72,7 +76,9 @@ describe DesignManagement::DeleteDesignsService do
it 'does not log any events' do it 'does not log any events' do
counter = ::Gitlab::UsageDataCounters::DesignsCounter counter = ::Gitlab::UsageDataCounters::DesignsCounter
expect { run_service rescue nil }.not_to change { counter.totals }
expect { run_service rescue nil }
.not_to change { [counter.totals, Event.count] }
end end
end end
...@@ -92,6 +98,12 @@ describe DesignManagement::DeleteDesignsService do ...@@ -92,6 +98,12 @@ describe DesignManagement::DeleteDesignsService do
expect { run_service }.to change { counter.read(:delete) }.by(1) expect { run_service }.to change { counter.read(:delete) }.by(1)
end end
it 'creates an event in the activity stream' do
expect { run_service }
.to change { Event.count }.by(1)
.and change { Event.destroyed_action.for_design.count }.by(1)
end
it 'informs the new-version-worker' do it 'informs the new-version-worker' do
expect(::DesignManagement::NewVersionWorker).to receive(:perform_async).with(Integer) expect(::DesignManagement::NewVersionWorker).to receive(:perform_async).with(Integer)
...@@ -129,14 +141,14 @@ describe DesignManagement::DeleteDesignsService do ...@@ -129,14 +141,14 @@ describe DesignManagement::DeleteDesignsService do
let!(:designs) { create_designs(2) } let!(:designs) { create_designs(2) }
it 'removes those designs' do it 'makes the correct changes' do
counter = ::Gitlab::UsageDataCounters::DesignsCounter
expect { run_service } expect { run_service }
.to change { issue.designs.current.count }.from(3).to(1) .to change { issue.designs.current.count }.from(3).to(1)
end .and change { counter.read(:delete) }.by(2)
.and change { Event.count }.by(2)
it 'logs the correct number of deletion events' do .and change { Event.destroyed_action.for_design.count }.by(2)
counter = ::Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }.to change { counter.read(:delete) }.by(2)
end end
it_behaves_like "a success" it_behaves_like "a success"
......
...@@ -65,6 +65,10 @@ describe DesignManagement::SaveDesignsService do ...@@ -65,6 +65,10 @@ describe DesignManagement::SaveDesignsService do
end end
it_behaves_like 'a service error' it_behaves_like 'a service error'
it 'does not create an event in the activity stream' do
expect { run_service }.not_to change { Event.count }
end
end end
context 'when the feature is available' do context 'when the feature is available' do
...@@ -89,6 +93,12 @@ describe DesignManagement::SaveDesignsService do ...@@ -89,6 +93,12 @@ describe DesignManagement::SaveDesignsService do
expect { run_service }.to change { counter.read(:create) }.by(1) expect { run_service }.to change { counter.read(:create) }.by(1)
end end
it 'creates an event in the activity stream' do
expect { run_service }
.to change { Event.count }.by(1)
.and change { Event.for_design.created_action.count }.by(1)
end
it 'creates a commit in the repository' do it 'creates a commit in the repository' do
run_service run_service
...@@ -166,9 +176,12 @@ describe DesignManagement::SaveDesignsService do ...@@ -166,9 +176,12 @@ describe DesignManagement::SaveDesignsService do
expect(updated_designs.first.versions.size).to eq(2) expect(updated_designs.first.versions.size).to eq(2)
end end
it 'increments the update counter' do it 'records the correct events' do
counter = Gitlab::UsageDataCounters::DesignsCounter counter = Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }.to change { counter.read(:update) }.by 1 expect { run_service }
.to change { counter.read(:update) }.by(1)
.and change { Event.count }.by(1)
.and change { Event.for_design.updated_action.count }.by(1)
end end
context 'when uploading a new design' do context 'when uploading a new design' do
...@@ -217,6 +230,14 @@ describe DesignManagement::SaveDesignsService do ...@@ -217,6 +230,14 @@ describe DesignManagement::SaveDesignsService do
.and change { counter.read(:update) }.by(1) .and change { counter.read(:update) }.by(1)
end end
it 'creates the correct activity stream events' do
expect { run_service }
.to change { Event.count }.by(2)
.and change { Event.for_design.count }.by(2)
.and change { Event.created_action.count }.by(1)
.and change { Event.updated_action.count }.by(1)
end
it 'creates a single commit' do it 'creates a single commit' do
commit_count = -> do commit_count = -> do
design_repository.expire_all_method_caches design_repository.expire_all_method_caches
......
...@@ -5,6 +5,9 @@ require 'spec_helper' ...@@ -5,6 +5,9 @@ require 'spec_helper'
describe EventCreateService do describe EventCreateService do
let(:service) { described_class.new } let(:service) { described_class.new }
let_it_be(:user, reload: true) { create :user }
let_it_be(:project) { create(:project) }
describe 'Issues' do describe 'Issues' do
describe '#open_issue' do describe '#open_issue' do
let(:issue) { create(:issue) } let(:issue) { create(:issue) }
...@@ -87,8 +90,6 @@ describe EventCreateService do ...@@ -87,8 +90,6 @@ describe EventCreateService do
end end
describe 'Milestone' do describe 'Milestone' do
let(:user) { create :user }
describe '#open_milestone' do describe '#open_milestone' do
let(:milestone) { create(:milestone) } let(:milestone) { create(:milestone) }
...@@ -210,9 +211,6 @@ describe EventCreateService do ...@@ -210,9 +211,6 @@ describe EventCreateService do
end end
describe '#push', :clean_gitlab_redis_shared_state do describe '#push', :clean_gitlab_redis_shared_state do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:push_data) do let(:push_data) do
{ {
commits: [ commits: [
...@@ -234,9 +232,6 @@ describe EventCreateService do ...@@ -234,9 +232,6 @@ describe EventCreateService do
end end
describe '#bulk_push', :clean_gitlab_redis_shared_state do describe '#bulk_push', :clean_gitlab_redis_shared_state do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:push_data) do let(:push_data) do
{ {
action: :created, action: :created,
...@@ -251,9 +246,6 @@ describe EventCreateService do ...@@ -251,9 +246,6 @@ describe EventCreateService do
end end
describe 'Project' do describe 'Project' do
let(:user) { create :user }
let(:project) { create(:project) }
describe '#join_project' do describe '#join_project' do
subject { service.join_project(project, user) } subject { service.join_project(project, user) }
...@@ -268,4 +260,81 @@ describe EventCreateService do ...@@ -268,4 +260,81 @@ describe EventCreateService do
it { expect { subject }.to change { Event.count }.from(0).to(1) } it { expect { subject }.to change { Event.count }.from(0).to(1) }
end end
end end
describe 'design events' do
let_it_be(:design) { create(:design, project: project) }
let_it_be(:author) { user }
shared_examples 'feature flag gated multiple event creation' do
context 'the feature flag is off' do
before do
stub_feature_flags(design_activity_events: false)
end
specify { expect(result).to be_empty }
specify { expect { result }.not_to change { Event.count } }
specify { expect { result }.not_to exceed_query_limit(0) }
end
context 'the feature flag is enabled for a single project' do
before do
stub_feature_flags(design_activity_events: project)
end
specify { expect(result).not_to be_empty }
specify { expect { result }.to change { Event.count }.by(1) }
end
end
describe '#save_designs' do
let_it_be(:updated) { create_list(:design, 5) }
let_it_be(:created) { create_list(:design, 3) }
let(:result) { service.save_designs(author, create: created, update: updated) }
specify { expect { result }.to change { Event.count }.by(8) }
specify { expect { result }.not_to exceed_query_limit(1) }
it 'creates 3 created design events' do
ids = result.pluck('id')
events = Event.created_action.where(id: ids)
expect(events.map(&:design)).to match_array(created)
end
it 'creates 5 created design events' do
ids = result.pluck('id')
events = Event.updated_action.where(id: ids)
expect(events.map(&:design)).to match_array(updated)
end
it_behaves_like 'feature flag gated multiple event creation' do
let(:project) { created.first.project }
end
end
describe '#destroy_designs' do
let_it_be(:designs) { create_list(:design, 5) }
let_it_be(:author) { create(:user) }
let(:result) { service.destroy_designs(designs, author) }
specify { expect { result }.to change { Event.count }.by(5) }
specify { expect { result }.not_to exceed_query_limit(1) }
it 'creates 5 destroyed design events' do
ids = result.pluck('id')
events = Event.destroyed_action.where(id: ids)
expect(events.map(&:design)).to match_array(designs)
end
it_behaves_like 'feature flag gated multiple event creation' do
let(:project) { designs.first.project }
end
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