Commit 324b2772 authored by Luke Duncalfe's avatar Luke Duncalfe

Export Design data in project import/export

https://gitlab.com/gitlab-org/gitlab-ee/issues/11090
parent b6689e03
# frozen_string_literal: true
module EE
module ExportHelper
extend ::Gitlab::Utils::Override
override :project_export_descriptions
def project_export_descriptions
super + [_('Design Management files and data')]
end
end
end
......@@ -2,6 +2,7 @@
module DesignManagement
class Design < ApplicationRecord
include Importable
include Noteable
include Gitlab::FileTypeDetection
include Gitlab::Utils::StrongMemoize
......@@ -16,7 +17,8 @@ module DesignManagement
# data
has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :project, :issue, :filename, presence: true
validates :project, :filename, presence: true
validates :issue, presence: true, unless: :importing?
validates :filename, uniqueness: { scope: :issue_id }
validate :validate_file_is_image
......
......@@ -2,6 +2,7 @@
module DesignManagement
class Version < ApplicationRecord
include Importable
include ShaAttribute
NotSameIssue = Class.new(StandardError)
......@@ -34,13 +35,13 @@ module DesignManagement
source: :design,
inverse_of: :versions
validates :designs, presence: true
validates :designs, presence: true, unless: :importing?
validates :sha, presence: true
validates :sha, uniqueness: { case_sensitive: false, scope: :issue_id }
# We are not validating the issue object as it incurs an extra query to fetch
# the record from the DB. Instead, we rely on the foreign key constraint to
# ensure referential integrity.
validates :issue_id, presence: true
validates :issue_id, presence: true, unless: :importing?
sha_attribute :sha
......
---
title: Allow Design Management files and data to be included in the project exporter/importer
merge_request: 14702
author:
type: added
# frozen_string_literal: true
module EE
module Gitlab
module ImportExport
module GroupProjectObjectBuilder
extend ::Gitlab::Utils::Override
extend ActiveSupport::Concern
private
override :where_clause_for_klass
def where_clause_for_klass
return attrs_to_arel(attributes.slice('filename')).and(table[:issue_id].eq(nil)) if design?
super
end
def design?
klass == DesignManagement::Design
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module ImportExport
module ProjectTreeRestorer
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
private
attr_accessor :project
override :remove_feature_dependent_sub_relations
def remove_feature_dependent_sub_relations(relation_item)
if relation_item.is_a?(Hash) && ::Feature.disabled?(:export_designs, project, default_enabled: true)
relation_item.except!('designs', 'design_versions')
end
end
end
end
end
end
......@@ -7,10 +7,16 @@ module EE
extend ActiveSupport::Concern
EE_OVERRIDES = {
design: 'DesignManagement::Design',
designs: 'DesignManagement::Design',
design_versions: 'DesignManagement::Version',
actions: 'DesignManagement::Action',
deploy_access_levels: 'ProtectedEnvironment::DeployAccessLevel',
unprotect_access_levels: 'ProtectedBranch::UnprotectAccessLevel'
}.freeze
EE_EXISTING_OBJECT_CHECK = %i[DesignManagement::Design].freeze
class_methods do
extend ::Gitlab::Utils::Override
......@@ -18,6 +24,11 @@ module EE
def overrides
super.merge(EE_OVERRIDES)
end
override :existing_object_check
def existing_object_check
super + EE_EXISTING_OBJECT_CHECK
end
end
end
end
......
......@@ -49,7 +49,6 @@ describe Security::PipelineVulnerabilitiesFinder do
let(:params) { { report_type: %w[dast] } }
it 'includes only dast' do
# binding.pry
expect(subject.count).to eq dast_count
end
end
......
{
"description":"",
"visibility_level":0,
"archived":false,
"merge_requests_template":null,
"merge_requests_rebase_enabled":false,
"approvals_before_merge":0,
"reset_approvals_on_push":true,
"merge_requests_ff_only_enabled":false,
"issues_template":null,
"shared_runners_enabled":true,
"build_coverage_regex":null,
"build_allow_git_fetch":true,
"build_timeout":3600,
"pending_delete":false,
"public_builds":true,
"last_repository_check_failed":null,
"container_registry_enabled":true,
"only_allow_merge_if_pipeline_succeeds":false,
"has_external_issue_tracker":false,
"request_access_enabled":false,
"has_external_wiki":false,
"ci_config_path":null,
"only_allow_merge_if_all_discussions_are_resolved":false,
"repository_size_limit":null,
"printing_merge_request_link_enabled":true,
"auto_cancel_pending_pipelines":"enabled",
"service_desk_enabled":null,
"delete_error":null,
"disable_overriding_approvers_per_merge_request":null,
"resolve_outdated_diff_discussions":false,
"jobs_cache_index":null,
"external_authorization_classification_label":null,
"pages_https_only":false,
"external_webhook_token":null,
"merge_requests_author_approval":null,
"merge_requests_require_code_owner_approval":null,
"merge_requests_disable_committers_approval":null,
"require_password_to_approve":null,
"labels":[
],
"milestones":[
],
"issues":[
{
"id":469,
"title":"issue 1",
"author_id":1,
"project_id":30,
"created_at":"2019-08-07T03:57:55.007Z",
"updated_at":"2019-08-07T03:57:55.007Z",
"description":"",
"state":"opened",
"iid":1,
"updated_by_id":null,
"weight":null,
"confidential":false,
"due_date":null,
"moved_to_id":null,
"lock_version":0,
"time_estimate":0,
"relative_position":1073742323,
"service_desk_reply_to":null,
"last_edited_at":null,
"last_edited_by_id":null,
"discussion_locked":null,
"closed_at":null,
"closed_by_id":null,
"state_id":1,
"events":[
{
"id":1775,
"project_id":30,
"author_id":1,
"target_id":469,
"created_at":"2019-08-07T03:57:55.158Z",
"updated_at":"2019-08-07T03:57:55.158Z",
"target_type":"Issue",
"action":1
}
],
"timelogs":[
],
"notes":[
],
"label_links":[
],
"resource_label_events":[
],
"issue_assignees":[
],
"designs":[
{
"id":38,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg",
"notes":[
]
},
{
"id":39,
"project_id":30,
"issue_id":469,
"filename":"jonathan_richman.jpg",
"notes":[
]
},
{
"id":40,
"project_id":30,
"issue_id":469,
"filename":"mariavontrap.jpeg",
"notes":[
]
}
],
"design_versions":[
{
"id":24,
"sha":"9358d1bac8ff300d3d2597adaa2572a20f7f8703",
"issue_id":469,
"actions":[
{
"design_id":38,
"version_id":24,
"event":0,
"design":{
"id":38,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg"
}
}
]
},
{
"id":25,
"sha":"e1a4a501bcb42f291f84e5d04c8f927821542fb6",
"issue_id":469,
"actions":[
{
"design_id":38,
"version_id":25,
"event":0,
"design":{
"id":38,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg"
}
},
{
"design_id":39,
"version_id":25,
"event":0,
"design":{
"id":39,
"project_id":30,
"issue_id":469,
"filename":"jonathan_richman.jpg"
}
}
]
},
{
"id":26,
"sha":"27702d08f5ee021ae938737f84e8fe7c38599e85",
"issue_id":469,
"actions":[
{
"design_id":38,
"version_id":26,
"event":0,
"design":{
"id":38,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg"
}
},
{
"design_id":39,
"version_id":26,
"event":0,
"design":{
"id":39,
"project_id":30,
"issue_id":469,
"filename":"jonathan_richman.jpg"
}
},
{
"design_id":40,
"version_id":26,
"event":0,
"design":{
"id":40,
"project_id":30,
"issue_id":469,
"filename":"mariavontrap.jpeg"
}
}
]
}
]
},
{
"id":470,
"title":"issue 2",
"author_id":1,
"project_id":30,
"created_at":"2019-08-07T04:15:57.607Z",
"updated_at":"2019-08-07T04:15:57.607Z",
"description":"",
"state":"opened",
"iid":2,
"updated_by_id":null,
"weight":null,
"confidential":false,
"due_date":null,
"moved_to_id":null,
"lock_version":0,
"time_estimate":0,
"relative_position":1073742823,
"service_desk_reply_to":null,
"last_edited_at":null,
"last_edited_by_id":null,
"discussion_locked":null,
"closed_at":null,
"closed_by_id":null,
"state_id":1,
"events":[
{
"id":1776,
"project_id":30,
"author_id":1,
"target_id":470,
"created_at":"2019-08-07T04:15:57.789Z",
"updated_at":"2019-08-07T04:15:57.789Z",
"target_type":"Issue",
"action":1
}
],
"timelogs":[
],
"notes":[
],
"label_links":[
],
"resource_label_events":[
],
"issue_assignees":[
],
"designs":[
{
"id":42,
"project_id":30,
"issue_id":470,
"filename":"1 (1).jpeg",
"notes":[
]
},
{
"id":43,
"project_id":30,
"issue_id":470,
"filename":"2099743.jpg",
"notes":[
]
},
{
"id":44,
"project_id":30,
"issue_id":470,
"filename":"a screenshot (1).jpg",
"notes":[
]
},
{
"id":41,
"project_id":30,
"issue_id":470,
"filename":"chirrido3.jpg",
"notes":[
]
}
],
"design_versions":[
{
"id":27,
"sha":"8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8",
"issue_id":470,
"actions":[
{
"design_id":41,
"version_id":27,
"event":0,
"design":{
"id":41,
"project_id":30,
"issue_id":470,
"filename":"chirrido3.jpg"
}
}
]
},
{
"id":28,
"sha":"73f871b4c8c1d65c62c460635e023179fb53abc4",
"issue_id":470,
"actions":[
{
"design_id":42,
"version_id":28,
"event":0,
"design":{
"id":42,
"project_id":30,
"issue_id":470,
"filename":"1 (1).jpeg"
}
},
{
"design_id":43,
"version_id":28,
"event":0,
"design":{
"id":43,
"project_id":30,
"issue_id":470,
"filename":"2099743.jpg"
}
}
]
},
{
"id":29,
"sha":"c9b5f067f3e892122a4b12b0a25a8089192f3ac8",
"issue_id":470,
"actions":[
{
"design_id":42,
"version_id":29,
"event":0,
"design":{
"id":42,
"project_id":30,
"issue_id":470,
"filename":"1 (1).jpeg"
}
},
{
"design_id":44,
"version_id":29,
"event":0,
"design":{
"id":44,
"project_id":30,
"issue_id":470,
"filename":"a screenshot (1).jpg"
}
}
]
}
]
}
],
"snippets":[
],
"releases":[
],
"project_members":[
{
"id":95,
"access_level":40,
"source_id":30,
"source_type":"Project",
"user_id":1,
"notification_level":3,
"created_at":"2019-08-07T03:57:32.825Z",
"updated_at":"2019-08-07T03:57:32.825Z",
"created_by_id":1,
"invite_email":null,
"invite_token":null,
"invite_accepted_at":null,
"requested_at":null,
"expires_at":null,
"ldap":false,
"override":false,
"user":{
"id":1,
"email":"admin@example.com",
"username":"root"
}
}
],
"merge_requests":[
],
"ci_pipelines":[
],
"triggers":[
],
"pipeline_schedules":[
],
"services":[
],
"protected_branches":[
],
"protected_environments":[
],
"protected_tags":[
],
"project_feature":{
"id":30,
"project_id":30,
"merge_requests_access_level":20,
"issues_access_level":20,
"wiki_access_level":20,
"snippets_access_level":20,
"builds_access_level":20,
"created_at":"2019-08-07T03:57:32.485Z",
"updated_at":"2019-08-07T03:57:32.485Z",
"repository_access_level":20,
"pages_access_level":10
},
"custom_attributes":[
],
"prometheus_metrics":[
],
"project_badges":[
],
"ci_cd_settings":{
"group_runners_enabled":true
},
"boards":[
],
"pipelines":[
]
}
......@@ -84,9 +84,9 @@ describe Mutations::DesignManagement::Delete do
end
end
it 'runs no more than 27 queries' do
it 'runs no more than 28 queries' do
filenames.each(&:present?) # ignore setup
# Queries: as of 2019-08-08
# Queries: as of 2019-08-28
# -------------
# 01. routing query
# 02. find project by id
......@@ -112,10 +112,11 @@ describe Mutations::DesignManagement::Delete do
# 23. create version with sha and issue
# 24. create design-version links
# 25. validate version.actions.present?
# 26. validate version.sha is unique
# 27. leave transaction 1
# 26. validate version.issue.present?
# 27. validate version.sha is unique
# 28. leave transaction 1
#
expect { run_mutation }.not_to exceed_query_limit(27)
expect { run_mutation }.not_to exceed_query_limit(28)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe ExportHelper do
describe '#project_export_descriptions' do
it 'includes design management' do
expect(project_export_descriptions).to include('Design Management files and data')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeRestorer do
include ImportExport::CommonUtil
using RSpec::Parameterized::TableSyntax
set(:user) { create(:user) }
set(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:shared) { project.import_export_shared }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
before do
allow(shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/')
project_tree_restorer.instance_variable_set(:@path, 'ee/spec/fixtures/lib/gitlab/import_export/project.designs.json')
end
describe 'restoring design management data' do
context 'when the `export_designs` feature is enabled' do
before do
restored_project_json
end
it_behaves_like 'restores project correctly', issues: 2
it 'restores project associations correctly' do
expect(project.designs.size).to eq(7)
end
describe 'restores issue associations correctly' do
let(:issue) { project.issues.offset(index).first }
where(:index, :design_filenames, :version_shas) do
0 | %w[chirrido3.jpg jonathan_richman.jpg mariavontrap.jpeg] | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
1 | ['1 (1).jpeg', '2099743.jpg', 'a screenshot (1).jpg', 'chirrido3.jpg'] | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8 c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
end
with_them do
it do
expect(issue.designs.pluck(:filename)).to contain_exactly(*design_filenames)
expect(issue.design_versions.pluck(:sha)).to contain_exactly(*version_shas)
end
end
end
describe 'restores design version associations correctly' do
let(:project_designs) { project.designs.reorder(:filename, :issue_id) }
let(:design) { project_designs.offset(index).first }
where(:index, :version_shas) do
0 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
1 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4]
2 | %w[c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
3 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
4 | %w[8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8]
5 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
6 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85]
end
with_them do
it do
expect(design.versions.pluck(:sha)).to contain_exactly(*version_shas)
end
end
end
end
context 'when the `export_designs` feature is disabled' do
before do
stub_feature_flags(export_designs: false)
restored_project_json
end
it_behaves_like 'restores project correctly', issues: 2
it 'does not restore any Designs' do
expect(DesignManagement::Design).not_to exist
end
it 'does not restore any Versions' do
expect(DesignManagement::Version.exists?).to be false
end
it 'does not restore any DesignVersions' do
expect(DesignManagement::Action.exists?).to be false
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeSaver do
describe 'saves the project tree into a json object' do
set(:user) { create(:user) }
set(:project) { create(:project) }
set(:issue) { create(:issue, project: project) }
set(:design) { create(:design, :with_file, versions_count: 2, issue: issue) }
set(:note) { create(:diff_note_on_design, noteable: design, project: project, author: user) }
set(:note2) { create(:note, noteable: issue, project: project, author: user) }
let(:shared) { project.import_export_shared }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec_ee" }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:saved_project_json) do
project_tree_saver.save
project_json(project_tree_saver.full_path)
end
before do
project.add_maintainer(user)
end
after do
FileUtils.rm_rf(export_path)
end
it 'saves successfully' do
expect(project_tree_saver.save).to be true
end
describe 'the designs json' do
let(:issue_json) { saved_project_json['issues'].first }
it 'saves issue.designs correctly' do
expect(issue_json['designs'].size).to eq(1)
end
it 'saves issue.design_versions correctly' do
actions = issue_json['design_versions'].map do |v|
v['actions']
end.flatten
expect(issue_json['design_versions'].size).to eq(2)
expect(actions.size).to eq(2)
actions.each do |action|
expect(action['design']).to be_present
end
end
end
end
def project_json(filename)
JSON.parse(IO.read(filename))
end
end
......@@ -129,3 +129,5 @@ module Gitlab
end
end
end
Gitlab::ImportExport::GroupProjectObjectBuilder.prepend_if_ee('EE::Gitlab::ImportExport::GroupProjectObjectBuilder')
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