Commit 34dbb70e authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'ajk-design-activity-b' into 'master'

Add support for event creation on designs

See merge request gitlab-org/gitlab!33449
parents 2aa4fc2a bbc741c7
...@@ -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)
......
...@@ -599,6 +599,7 @@ design: &design ...@@ -599,6 +599,7 @@ design: &design
- versions - versions
- notes - notes
- user_mentions - user_mentions
- events
designs: *design designs: *design
actions: actions:
- design - design
......
...@@ -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