Commit 1652d3cd authored by George Koltsov's avatar George Koltsov

Import epic award emojis when using Bulk Import

  - When importing groups using Group Migration tool
  (Bulk Import), migrate award emojis over from source
  to destination to allow closer epic feature parity
  compared to file based Group Import/Export
parent bfddef57
---
title: Import epic award emojis when using Bulk Import
merge_request: 53747
author:
type: added
# frozen_string_literal: true
module EE
module BulkImports
module Groups
module Graphql
module GetEpicAwardEmojiQuery
extend self
def to_s
<<-'GRAPHQL'
query($full_path: ID!, $epic_iid: ID!, $cursor: String) {
group(fullPath: $full_path) {
epic(iid: $epic_iid) {
award_emoji: awardEmoji(first: 100, after: $cursor) {
page_info: pageInfo {
end_cursor: endCursor
has_next_page: hasNextPage
}
nodes {
name
user {
public_email: publicEmail
}
}
}
}
}
}
GRAPHQL
end
def variables(context)
iid = context.extra[:epic_iid]
tracker = "epic_#{iid}_award_emoji"
{
full_path: context.entity.source_full_path,
cursor: context.entity.next_page_for(tracker),
epic_iid: iid
}
end
def data_path
base_path << 'nodes'
end
def page_info_path
base_path << 'page_info'
end
private
def base_path
%w[data group epic award_emoji]
end
end
end
end
end
end
......@@ -54,10 +54,10 @@ module EE
GRAPHQL
end
def variables(entity)
def variables(context)
{
full_path: entity.source_full_path,
cursor: entity.next_page_for(:epics)
full_path: context.entity.source_full_path,
cursor: context.entity.next_page_for(:epics)
}
end
......
# frozen_string_literal: true
module EE
module BulkImports
module Groups
module Loaders
class EpicAwardEmojiLoader
NotAllowedError = Class.new(StandardError)
def initialize(options = {})
@options = options
end
# rubocop: disable CodeReuse/ActiveRecord
def load(context, data)
return unless data
epic = context.group.epics.find_by(iid: context.extra[:epic_iid])
return if award_emoji_exists?(epic, data)
raise NotAllowedError unless Ability.allowed?(context.current_user, :award_emoji, epic)
epic.award_emoji.create!(data)
end
private
def award_emoji_exists?(epic, data)
epic.award_emoji.exists?(user_id: data['user_id'], name: data['name'])
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
end
end
# frozen_string_literal: true
module EE
module BulkImports
module Groups
module Pipelines
class EpicAwardEmojiPipeline
include ::BulkImports::Pipeline
extractor ::BulkImports::Common::Extractors::GraphqlExtractor,
query: EE::BulkImports::Groups::Graphql::GetEpicAwardEmojiQuery
transformer ::BulkImports::Common::Transformers::ProhibitedAttributesTransformer
transformer ::BulkImports::Common::Transformers::AwardEmojiTransformer
loader EE::BulkImports::Groups::Loaders::EpicAwardEmojiLoader
# rubocop: disable CodeReuse/ActiveRecord
def initialize(context)
@context = context
@group = context.group
@epic_iids = @group.epics.order(iid: :desc).pluck(:iid)
set_next_epic
end
def after_run(extracted_data)
iid = context.extra[:epic_iid]
tracker = "epic_#{iid}_award_emoji"
context.entity.update_tracker_for(
relation: tracker,
has_next_page: extracted_data.has_next_page?,
next_page: extracted_data.next_page
)
set_next_epic unless extracted_data.has_next_page?
if extracted_data.has_next_page? || context.extra[:epic_iid]
run
end
end
private
def set_next_epic
context.extra[:epic_iid] = @epic_iids.pop
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
end
end
......@@ -10,7 +10,10 @@ module EE
override :pipelines
def pipelines
super << EE::BulkImports::Groups::Pipelines::EpicsPipeline
super + [
EE::BulkImports::Groups::Pipelines::EpicsPipeline,
EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline
]
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::BulkImports::Groups::Graphql::GetEpicAwardEmojiQuery do
it 'has a valid query' do
context = BulkImports::Pipeline::Context.new(create(:bulk_import_entity), epic_iid: 1)
result = GitlabSchema.execute(
described_class.to_s,
variables: described_class.variables(context)
).to_h
expect(result['errors']).to be_blank
end
describe '#data_path' do
it 'returns data path' do
expected = %w[data group epic award_emoji nodes]
expect(described_class.data_path).to eq(expected)
end
end
describe '#page_info_path' do
it 'returns pagination information path' do
expected = %w[data group epic award_emoji page_info]
expect(described_class.page_info_path).to eq(expected)
end
end
end
......@@ -4,12 +4,13 @@ require 'spec_helper'
RSpec.describe EE::BulkImports::Groups::Graphql::GetEpicsQuery do
describe '#variables' do
let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page') }
let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page', bulk_import: nil) }
let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'returns query variables based on entity information' do
expected = { full_path: entity.source_full_path, cursor: entity.next_page_for }
expect(described_class.variables(entity)).to eq(expected)
expect(described_class.variables(context)).to eq(expected)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::BulkImports::Groups::Loaders::EpicAwardEmojiLoader do
describe '#load' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:epic) { create(:epic, group: group, iid: 1) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
let_it_be(:data) do
{
'name' => 'banana',
'user_id' => user.id
}
end
before do
stub_licensed_features(epics: true)
context.extra[:epic_iid] = epic.iid
group.add_developer(user)
end
context 'when emoji does not exist' do
it 'creates new emoji' do
expect { subject.load(context, data) }.to change(::AwardEmoji, :count).by(1)
epic = group.epics.last
emoji = epic.award_emoji.first
expect(emoji.name).to eq(data['name'])
expect(emoji.user).to eq(user)
end
end
context 'when same emoji exists' do
it 'does not create a new emoji' do
epic.award_emoji.create!(data)
expect { subject.load(context, data) }.not_to change(::AwardEmoji, :count)
end
end
context 'when user is not allowed to award emoji' do
before do
allow(Ability).to receive(:allowed?).with(user, :award_emoji, epic).and_return(false)
end
it 'raises NotAllowedError exception' do
expect { subject.load(context, data) }.to raise_error(described_class::NotAllowedError)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline do
let_it_be(:cursor) { 'cursor' }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:epic) { create(:epic, group: group) }
let_it_be(:tracker) { "epic_#{epic.iid}_award_emoji" }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
let_it_be(:entity) do
create(
:bulk_import_entity,
group: group,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: 'My Destination Group',
destination_namespace: group.full_path
)
end
let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
before do
stub_licensed_features(epics: true)
group.add_owner(user)
end
subject { described_class.new(context) }
describe '#initialize' do
it 'update context with next epic iid' do
subject
expect(context.extra[:epic_iid]).to eq(epic.iid)
end
end
describe '#run' do
it 'imports epic award emoji' do
data = extractor_data(has_next_page: false, cursor: cursor)
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
allow(extractor)
.to receive(:extract)
.and_return(data)
end
expect { subject.run }.to change(::AwardEmoji, :count).by(1)
expect(epic.award_emoji.first.name).to eq('thumbsup')
end
end
describe '#after_run' do
context 'when extracted data has next page' do
it 'updates tracker information and runs pipeline again' do
data = extractor_data(has_next_page: true, cursor: cursor)
expect(subject).to receive(:run)
subject.after_run(data)
page_tracker = entity.trackers.find_by(relation: tracker)
expect(page_tracker.has_next_page).to eq(true)
expect(page_tracker.next_page).to eq(cursor)
end
end
context 'when extracted data has no next page' do
it 'updates tracker information and does not run pipeline' do
data = extractor_data(has_next_page: false)
expect(subject).not_to receive(:run)
subject.after_run(data)
page_tracker = entity.trackers.find_by(relation: tracker)
expect(page_tracker.has_next_page).to eq(false)
expect(page_tracker.next_page).to be_nil
end
it 'updates context with next epic iid' do
epic2 = create(:epic, group: group)
data = extractor_data(has_next_page: false)
expect(subject).to receive(:run)
subject.after_run(data)
expect(context.extra[:epic_iid]).to eq(epic2.iid)
end
end
end
describe 'pipeline parts' do
it { expect(described_class).to include_module(BulkImports::Pipeline) }
it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
it 'has extractors' do
expect(described_class.get_extractor)
.to eq(
klass: BulkImports::Common::Extractors::GraphqlExtractor,
options: {
query: EE::BulkImports::Groups::Graphql::GetEpicAwardEmojiQuery
}
)
end
it 'has transformers' do
expect(described_class.transformers)
.to contain_exactly(
{ klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
{ klass: BulkImports::Common::Transformers::AwardEmojiTransformer, options: nil }
)
end
it 'has loaders' do
expect(described_class.get_loader).to eq(klass: EE::BulkImports::Groups::Loaders::EpicAwardEmojiLoader, options: nil)
end
end
def extractor_data(has_next_page:, cursor: nil)
data = [{ 'name' => 'thumbsup' }]
page_info = {
'end_cursor' => cursor,
'has_next_page' => has_next_page
}
BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
end
end
......@@ -20,13 +20,13 @@ RSpec.describe EE::BulkImports::Groups::Pipelines::EpicsPipeline do
let(:context) { BulkImports::Pipeline::Context.new(entity) }
subject { described_class.new(context) }
before do
stub_licensed_features(epics: true)
group.add_owner(user)
end
subject { described_class.new(context) }
describe '#run' do
it 'imports group epics into destination group' do
first_page = extractor_data(has_next_page: true, cursor: cursor)
......
......@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe BulkImports::Importers::GroupImporter do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:bulk_import) { create(:bulk_import, user: user) }
let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import, group: group) }
let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
let(:context) { BulkImports::Pipeline::Context.new(bulk_import_entity) }
......@@ -22,6 +23,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
expect_to_run_pipeline EE::BulkImports::Groups::Pipelines::EpicsPipeline, context: context
expect_to_run_pipeline EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline, context: context
subject.execute
......
......@@ -13,7 +13,7 @@ module BulkImports
response = client.execute(
client.parse(query.to_s),
query.variables(context.entity)
query.variables(context)
).original_hash.deep_dup
BulkImports::Pipeline::ExtractedData.new(
......
# frozen_string_literal: true
module BulkImports
module Common
module Transformers
class AwardEmojiTransformer
def initialize(*args); end
def transform(context, data)
user = find_user(context, data&.dig('user', 'public_email')) || context.current_user
data
.except('user')
.merge('user_id' => user.id)
end
private
def find_user(context, email)
return if email.blank?
context.group.users.find_by_any_email(email, confirmed: true) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
end
end
......@@ -29,8 +29,8 @@ module BulkImports
GRAPHQL
end
def variables(entity)
{ full_path: entity.source_full_path }
def variables(context)
{ full_path: context.entity.source_full_path }
end
def base_path
......
......@@ -26,10 +26,10 @@ module BulkImports
GRAPHQL
end
def variables(entity)
def variables(context)
{
full_path: entity.source_full_path,
cursor: entity.next_page_for(:labels)
full_path: context.entity.source_full_path,
cursor: context.entity.next_page_for(:labels)
}
end
......
......@@ -31,10 +31,10 @@ module BulkImports
GRAPHQL
end
def variables(entity)
def variables(context)
{
full_path: entity.source_full_path,
cursor: entity.next_page_for(:group_members)
full_path: context.entity.source_full_path,
cursor: context.entity.next_page_for(:group_members)
}
end
......
......@@ -4,10 +4,12 @@ module BulkImports
module Pipeline
class Context
attr_reader :entity, :bulk_import
attr_accessor :extra
def initialize(entity)
def initialize(entity, extra = {})
@entity = entity
@bulk_import = entity.bulk_import
@extra = extra
end
def group
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
describe '#transform' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
let(:hash) do
{
'name' => 'thumbs up',
'user' => {
'public_email' => email
}
}
end
before do
group.add_developer(user)
end
shared_examples 'sets user_id and removes user key' do
it 'sets found user_id and removes user key' do
transformed_hash = subject.transform(context, hash)
expect(transformed_hash['user']).to be_nil
expect(transformed_hash['user_id']).to eq(user.id)
end
end
context 'when user can be found by email' do
let(:email) { user.email }
include_examples 'sets user_id and removes user key'
end
context 'when user cannot be found by email' do
let(:user) { bulk_import.user }
let(:email) { nil }
include_examples 'sets user_id and removes user key'
end
end
end
......@@ -4,12 +4,13 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
describe '#variables' do
let(:entity) { double(source_full_path: 'test') }
let(:entity) { double(source_full_path: 'test', bulk_import: nil) }
let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'returns query variables based on entity information' do
expected = { full_path: entity.source_full_path }
expect(described_class.variables(entity)).to eq(expected)
expect(described_class.variables(context)).to eq(expected)
end
end
......
......@@ -4,12 +4,13 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do
describe '#variables' do
let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page') }
let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page', bulk_import: nil) }
let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'returns query variables based on entity information' do
expected = { full_path: entity.source_full_path, cursor: entity.next_page_for }
expect(described_class.variables(entity)).to eq(expected)
expect(described_class.variables(context)).to eq(expected)
end
end
......
......@@ -5,11 +5,12 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
it 'has a valid query' do
entity = create(:bulk_import_entity)
context = BulkImports::Pipeline::Context.new(entity)
query = GraphQL::Query.new(
GitlabSchema,
described_class.to_s,
variables: described_class.variables(entity)
variables: described_class.variables(context)
)
result = GitlabSchema.static_validator.validate(query)
......
......@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe BulkImports::Importers::GroupImporter do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:bulk_import) { create(:bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import, group: group) }
let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
let(:context) { BulkImports::Pipeline::Context.new(bulk_import_entity) }
......@@ -21,7 +22,11 @@ RSpec.describe BulkImports::Importers::GroupImporter do
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee?
if Gitlab.ee?
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context)
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize, context: context)
end
subject.execute
......@@ -29,7 +34,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
end
context 'when failed' do
let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import, group: group) }
it 'does not transition entity to finished state' do
allow(bulk_import_entity).to receive(:start!)
......
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