drupal-exporter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG.md +4 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +72 -0
  8. data/LICENSE +22 -0
  9. data/README.md +251 -0
  10. data/Rakefile +9 -0
  11. data/bin/drupal-exporter +37 -0
  12. data/drupal_exporter.gemspec +34 -0
  13. data/drupal_settings/boolean_columns.yml +2 -0
  14. data/drupal_settings/drupal_content_types.json +47 -0
  15. data/drupal_settings/drupal_settings.yml +23 -0
  16. data/lib/cli.rb +13 -0
  17. data/lib/configuration.rb +46 -0
  18. data/lib/converters/content_types_structure_creator.rb +60 -0
  19. data/lib/converters/contentful_model_to_json.rb +109 -0
  20. data/lib/drupal/content_type.rb +151 -0
  21. data/lib/drupal/export.rb +69 -0
  22. data/lib/drupal/file_managed.rb +42 -0
  23. data/lib/drupal/tag.rb +52 -0
  24. data/lib/drupal/user.rb +46 -0
  25. data/lib/drupal/vocabulary.rb +42 -0
  26. data/lib/migrator.rb +28 -0
  27. data/lib/version.rb +3 -0
  28. data/spec/fixtures/database_rows/content_type_article.json +14 -0
  29. data/spec/fixtures/database_rows/image.json +10 -0
  30. data/spec/fixtures/database_rows/node_content_type_article.json +15 -0
  31. data/spec/fixtures/database_rows/node_content_type_blog.json +15 -0
  32. data/spec/fixtures/database_rows/tag.json +8 -0
  33. data/spec/fixtures/database_rows/user.json +18 -0
  34. data/spec/fixtures/database_rows/vocabulary.json +9 -0
  35. data/spec/fixtures/drupal/assets/file/file_4.json +6 -0
  36. data/spec/fixtures/drupal/entries/article/article_5.json +24 -0
  37. data/spec/fixtures/drupal/entries/tag/tag_1.json +9 -0
  38. data/spec/fixtures/drupal/entries/user/user_1.json +6 -0
  39. data/spec/fixtures/drupal/entries/vocabulary/vocabulary_3.json +6 -0
  40. data/spec/fixtures/json_responses/article.json +24 -0
  41. data/spec/fixtures/json_responses/image.json +6 -0
  42. data/spec/fixtures/json_responses/tag.json +9 -0
  43. data/spec/fixtures/json_responses/vocabulary.json +6 -0
  44. data/spec/fixtures/settings/boolean_columns.yml +2 -0
  45. data/spec/fixtures/settings/drupal_content_types.json +47 -0
  46. data/spec/fixtures/settings/drupal_settings.yml +17 -0
  47. data/spec/lib/configuration_spec.rb +18 -0
  48. data/spec/lib/drupal/content_type_spec.rb +123 -0
  49. data/spec/lib/drupal/export_spec.rb +33 -0
  50. data/spec/lib/drupal/file_managed_spec.rb +52 -0
  51. data/spec/lib/drupal/tag_spec.rb +60 -0
  52. data/spec/lib/drupal/user_spec.rb +49 -0
  53. data/spec/lib/drupal/vocabulary_spec.rb +51 -0
  54. data/spec/spec_helper.rb +11 -0
  55. data/spec/support/db_rows_json.rb +15 -0
  56. data/spec/support/shared_configuration.rb +20 -0
  57. metadata +297 -0
@@ -0,0 +1,34 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require File.expand_path('../lib/version', __FILE__)
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'drupal-exporter'
8
+ spec.version = Version::VERSION
9
+ spec.authors = ['Contentful GmbH (Andreas Tiefenthaler)']
10
+ spec.email = ['rubygems@contentful.com']
11
+ spec.description = 'Drupal exporter that prepares content to be imported'
12
+ spec.summary = 'Exporter for Drupal 7'
13
+ spec.homepage = 'https://github.com/contentful/drupal-exporter.rb'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables << 'drupal-exporter'
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'http', '~> 0.6'
22
+ spec.add_dependency 'multi_json', '~> 1'
23
+ spec.add_dependency 'sequel','~> 4.15'
24
+ spec.add_dependency 'mysql2','~> 0.3'
25
+ spec.add_dependency 'activesupport','~> 4.1'
26
+ spec.add_dependency 'pg', '~> 0.17.0'
27
+ spec.add_dependency 'escort','~> 0.4.0'
28
+ spec.add_dependency 'i18n', '~> 0.6'
29
+
30
+ spec.add_development_dependency 'rspec', '~> 3'
31
+ spec.add_development_dependency 'rspec-its', '~> 1.1.0'
32
+ spec.add_development_dependency 'bundler', '~> 1.6'
33
+ spec.add_development_dependency 'rake'
34
+ end
@@ -0,0 +1,2 @@
1
+ - field_if_content_type
2
+ - field_boolean
@@ -0,0 +1,47 @@
1
+ // machine_name_of_content_type : {
2
+ // contentful_api_field : column_machine_name
3
+ // }
4
+
5
+ {
6
+ "article": {
7
+ "body": "body",
8
+ "image": "field_image"
9
+ },
10
+ "page": {
11
+ "body": "body"
12
+ },
13
+ "blog": {
14
+ "body": "body"
15
+ },
16
+ "content_type": {
17
+ "body": "body",
18
+ "age": "field_age",
19
+ "if_content_type": "field_if_content_type",
20
+ "decimal": "field_decimal"
21
+ },
22
+ "second_content_type": {
23
+ "body": "body",
24
+ "firma": "field_firma"
25
+ },
26
+ "content_all_types": {
27
+ "body": "body",
28
+ "file": "field_file",
29
+ "image_sec": "field_sec_image",
30
+ "integer": "field_integer",
31
+ "boolean": "field_boolean",
32
+ "decimal": "field_sec_decimal",
33
+ "list_float": "field_list_float",
34
+ "list_integer": "field_list_integer",
35
+ "list_text": "field_list_text",
36
+ "long_text": "field_long_text",
37
+ "text_summary": "field_text_summary",
38
+ "term_tagging": {
39
+ "table": "field_term_tagging"
40
+ },
41
+ "text": "field_text"
42
+ },
43
+ "assety": {
44
+ "body": "body",
45
+ "assets": "field_asset"
46
+ }
47
+ }
@@ -0,0 +1,23 @@
1
+ #PATH to all data
2
+ data_dir: PATH_WHERE_ALL_DATA_WILL_BE_SAVED
3
+
4
+ ########## EXPORT DATA ##########
5
+
6
+ #Connecting to a database
7
+ adapter: mysql2
8
+ host: localhost
9
+ database: drupal_database_name
10
+ user: username
11
+ password: password
12
+
13
+ # DRUPAL
14
+
15
+ drupal_content_types_json: drupal_settings/drupal_content_types.json
16
+ drupal_boolean_columns: drupal_settings/boolean_columns.yml
17
+ drupal_base_url: http://example_hostname.com
18
+
19
+ # CONVERT CONTENTFUL MODEL TO CONTENTFUL IMPORT STRUCTURE
20
+ content_model_json: PATH_TO_CONTENTFUL_MODEL_JSON_FILE
21
+ converted_model_dir: PATH_WHERE_CONVERTED_CONTENT_MODEL_WILL_BE_SAVED
22
+
23
+ contentful_structure_dir: PATH_TO_CONTENTFUL_STRUCUTRE_JSON_FILE
@@ -0,0 +1,13 @@
1
+ require_relative 'migrator'
2
+ require 'yaml'
3
+
4
+ module Command
5
+ class CLI < Escort::ActionCommand::Base
6
+
7
+ def execute
8
+ setting_file = YAML.load_file(global_options[:file])
9
+ Migrator.new(setting_file).run(command_name)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ require 'sequel'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module Contentful
5
+ class Configuration
6
+ attr_reader :space_id,
7
+ :config,
8
+ :data_dir,
9
+ :collections_dir,
10
+ :entries_dir,
11
+ :assets_dir,
12
+ :db,
13
+ :drupal_content_types,
14
+ :drupal_base_url
15
+
16
+ def initialize(settings)
17
+ @config = settings
18
+ validate_required_parameters
19
+ @data_dir = config['data_dir']
20
+ @collections_dir = "#{data_dir}/collections"
21
+ @entries_dir = "#{data_dir}/entries"
22
+ @assets_dir = "#{data_dir}/assets"
23
+ @space_id = config['space_id']
24
+ @drupal_content_types = JSON.parse(File.read(config['drupal_content_types_json']), symbolize_names: true).with_indifferent_access
25
+ @drupal_base_url = config['drupal_base_url']
26
+ @db = adapter_setup
27
+ end
28
+
29
+ def validate_required_parameters
30
+ fail ArgumentError, 'Set PATH to data_dir, the destination for all generated files. Check README' if config['data_dir'].nil?
31
+ fail ArgumentError, 'Set PATH to drupal_content_types_json. File with Drupal database structure. View README' if config['drupal_content_types_json'].nil?
32
+ define_adapter
33
+ end
34
+
35
+ def define_adapter
36
+ %w(adapter user host database).each do |param|
37
+ fail ArgumentError, "Set database connection parameters [adapter, host, database, user, password]. Missing the '#{param}' parameter! Password is optional. Check README!" unless config[param]
38
+ end
39
+ end
40
+
41
+ def adapter_setup
42
+ Sequel.connect(:adapter => config['adapter'], :user => config['user'], :host => config['host'], :database => config['database'], :password => config['password'])
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ module Contentful
2
+ module Converter
3
+ class ContentTypesStructureCreator
4
+
5
+ attr_reader :config, :logger
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ @logger = Logger.new(STDOUT)
10
+ end
11
+
12
+ def create_content_type_json_file(content_type_name, values)
13
+ collection = {
14
+ id: values[:id],
15
+ name: values[:name],
16
+ description: values[:description],
17
+ displayField: values[:displayField],
18
+ fields: create_fields(values[:fields])
19
+ }
20
+ write_json_to_file("#{config.collections_dir}/#{content_type_name}.json", collection)
21
+ logger.info "Saving #{content_type_name}.json to #{config.collections_dir}"
22
+ end
23
+
24
+ def create_fields(fields)
25
+ fields.each_with_object([]) do |(field, value), results|
26
+ results << {
27
+ name: create_field(field, value).capitalize,
28
+ id: create_field(field, value),
29
+ type: create_type_field(value),
30
+ link_type: create_link_type_field(value),
31
+ link: create_link_field(value)
32
+ }.compact
33
+ end
34
+ end
35
+
36
+ def create_field(field, value)
37
+ value.is_a?(Hash) ? value[:id] : field
38
+ end
39
+
40
+ def create_link_type_field(value)
41
+ value.is_a?(Hash) ? value[:link_type] : nil
42
+ end
43
+
44
+ def create_type_field(value)
45
+ value.is_a?(Hash) ? value[:type] : value
46
+ end
47
+
48
+ def create_link_field(value)
49
+ value.is_a?(Hash) ? value[:link] : nil
50
+ end
51
+
52
+ def write_json_to_file(path, data)
53
+ File.open(path, 'w') do |file|
54
+ file.write(JSON.pretty_generate(data))
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,109 @@
1
+ require_relative 'content_types_structure_creator'
2
+
3
+ module Contentful
4
+ module Converter
5
+ class ContentfulModelToJson
6
+ attr_reader :config, :logger, :converted_model_dir, :content_types
7
+
8
+ FIELD_TYPE = %w( Link Array )
9
+
10
+ def initialize(settings)
11
+ @config = settings
12
+ @logger = Logger.new(STDOUT)
13
+ end
14
+
15
+ def create_content_type_json
16
+ contentful_structure = load_contentful_structure_file
17
+ logger.info 'Create JSON files with content types structure...'
18
+ contentful_structure.each do |content_type, values|
19
+ content_type_name = content_type_name(content_type)
20
+ create_directory(config.collections_dir)
21
+ ContentTypesStructureCreator.new(config).create_content_type_json_file(content_type_name, values)
22
+ end
23
+ logger.info 'Done!'
24
+ end
25
+
26
+ def convert_to_import_form
27
+ set_content_model_parameters
28
+ logger.info 'Converting Contentful model to Contentful import structure...'
29
+ File.open(converted_model_dir, 'w') { |file| file.write({}) }
30
+ content_type_file = JSON.parse(File.read(content_types))['items']
31
+ content_type_file.each do |content_type|
32
+ parsed_content_type = {
33
+ id: content_type['sys']['id'],
34
+ name: content_type['name'],
35
+ description: content_type['description'],
36
+ displayField: content_type['displayField'],
37
+ fields: create_content_type_fields(content_type)
38
+ }
39
+ import_form = JSON.parse(File.read(converted_model_dir))
40
+ File.open(converted_model_dir, 'w') do |file|
41
+ file.write(JSON.pretty_generate(import_form.merge!(content_type['name'] => parsed_content_type)))
42
+ end
43
+ end
44
+ logger.info "Done! Contentful import structure file saved in #{converted_model_dir}"
45
+ end
46
+
47
+ def create_content_type_fields(content_type)
48
+ content_type['fields'].each_with_object({}) do |(field, _value), results|
49
+ id = link_id(field)
50
+ results[id] = case field['type']
51
+ when 'Link'
52
+ {id: field['id'], type: field['linkType'], link: 'Link'}
53
+ when 'Array'
54
+ {id: field['id'], type: field['type'], link_type: field['items']['linkType'], link: field['items']['type']}
55
+ else
56
+ field['type']
57
+ end
58
+ end
59
+ end
60
+
61
+ def link_id(field)
62
+ if FIELD_TYPE.include? field['type']
63
+ field['name'].capitalize
64
+ else
65
+ field['id']
66
+ end
67
+ end
68
+
69
+ def content_type_name(content_type)
70
+ I18n.transliterate(content_type).underscore.tr(' ', '_')
71
+ end
72
+
73
+ def create_directory(path)
74
+ FileUtils.mkdir_p(path) unless File.directory?(path)
75
+ end
76
+
77
+ # If contentful_structure JSON file exists, it will load the file. If not, it will automatically create an empty file.
78
+ # This file is required to convert contentful model to contentful import structure.
79
+ def load_contentful_structure_file
80
+ fail ArgumentError, 'Set PATH for contentful structure JSON file. View README' if config.config['contentful_structure_dir'].nil?
81
+ file_exists? ? load_existing_contentful_structure_file : create_empty_contentful_structure_file
82
+ end
83
+
84
+ def file_exists?
85
+ File.exists?(config.config['contentful_structure_dir'])
86
+ end
87
+
88
+ def create_empty_contentful_structure_file
89
+ File.open(config.config['contentful_structure_dir'], 'w') { |file| file.write({}) }
90
+ load_existing_contentful_structure_file
91
+ end
92
+
93
+ def load_existing_contentful_structure_file
94
+ JSON.parse(File.read(config.config['contentful_structure_dir']), symbolize_names: true).with_indifferent_access
95
+ end
96
+
97
+ def set_content_model_parameters
98
+ validate_content_model_files
99
+ @converted_model_dir = config.config['converted_model_dir']
100
+ @content_types = config.config['content_model_json']
101
+ end
102
+
103
+ def validate_content_model_files
104
+ fail ArgumentError, 'Set PATH for content model JSON file. View README' if config.config['content_model_json'].nil?
105
+ fail ArgumentError, 'Set PATH where converted content model file will be saved. View README' if config.config['converted_model_dir'].nil?
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,151 @@
1
+ module Contentful
2
+ module Exporter
3
+ module Drupal
4
+ class ContentType
5
+
6
+ attr_reader :exporter, :config, :type, :schema
7
+
8
+ def initialize(exporter, config, type, schema)
9
+ @exporter = exporter
10
+ @config = config
11
+ @type = type
12
+ @schema = schema
13
+ end
14
+
15
+ def save_content_types_as_json
16
+ exporter.create_directory("#{config.entries_dir}/#{type}")
17
+ config.db[:node].where(type: type).each do |content_row|
18
+ extract_data(content_row)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def extract_data(content_row)
25
+ puts "Saving #{type} - id: #{content_row[:nid]}"
26
+ db_object = map_fields(content_row)
27
+ exporter.write_json_to_file("#{config.entries_dir}/#{type}/#{db_object[:id]}.json", db_object)
28
+ end
29
+
30
+ def map_fields(row, result = {})
31
+ result.merge!(set_default_data(row))
32
+ result.merge!(find_related_data(row))
33
+ result
34
+ end
35
+
36
+ def id(content_id)
37
+ "#{type}_#{content_id}"
38
+ end
39
+
40
+ def author(user_id)
41
+ {type: 'Author', id: "user_#{user_id}"}
42
+ end
43
+
44
+ def tags(entity_row_id)
45
+ entity_tags(entity_row_id).each_with_object([]) do |tag, tags|
46
+ linked_tag = {type: 'EntryTag', id: "tag_#{tag[:field_tags_tid]}"}
47
+ tags << linked_tag
48
+ end
49
+ end
50
+
51
+ def entity_tags(entity_id)
52
+ config.db[:field_data_field_tags].where(entity_id: entity_id)
53
+ end
54
+
55
+ def set_default_data(row, result = {})
56
+ result[:id] = id(row[:nid])
57
+ result[:title] = row[:title]
58
+ result[:author] = author(row[:uid])
59
+ result[:tags] = tags(row[:nid]) unless tags(row[:nid]).empty?
60
+ result[:created_at] = created_at(row[:created])
61
+ result
62
+ end
63
+
64
+ def find_related_data(row, result = {})
65
+ schema.each do |key, column_name|
66
+ result[key] = column_name.is_a?(String) ? fetch_data_from_related_table(row[:nid], column_name) : fetch_custom_tags(row[:nid], column_name)
67
+ end
68
+ result
69
+ end
70
+
71
+ def fetch_data_from_related_table(entity_id, table_name)
72
+ related_row = get_related_row(entity_id, table_name)
73
+ fetch_data_from_related_row(related_row, table_name)
74
+ end
75
+
76
+ def fetch_data_from_related_row(related_row, table_name)
77
+ respond_to_file?(related_row, table_name) ? get_file_id(related_row, table_name) : related_value(related_row, table_name)
78
+ end
79
+
80
+ def respond_to_file?(related_row, table_name)
81
+ file_id = "#{table_name}_fid".to_sym
82
+ (related_row.present? && related_row.first[file_id]) ? true : false
83
+ end
84
+
85
+ def get_file_id(related_row, table_name)
86
+ file_key = "#{table_name}_fid".to_sym
87
+ file_id = related_row.first[file_key]
88
+ file_asset_id = file_id(file_id)
89
+ link_asset_to_content_type(file_asset_id)
90
+ end
91
+
92
+ def file_id(file_id)
93
+ config.db[:file_managed].where(fid: file_id).first[:fid]
94
+ end
95
+
96
+ def get_related_row(entity_id, table_name)
97
+ config.db[related_table_name(table_name)].where(entity_id: entity_id)
98
+ end
99
+
100
+ def link_asset_to_content_type(file_asset_id)
101
+ {type: 'File', id: "file_#{file_asset_id}"}
102
+ end
103
+
104
+ def related_value(related_rows, table_name)
105
+ value = related_rows.empty? ? nil : related_rows.first[field_name(table_name)]
106
+ convert_type_value(value, table_name)
107
+ end
108
+
109
+ def fetch_custom_tags(entity_id, table_name)
110
+ custom_tag_table = "field_data_#{table_name['table']}".to_sym
111
+ tag_id = "#{table_name['table']}_tid".to_sym
112
+ config.db[custom_tag_table].where(entity_id: entity_id).each_with_object([]) do |content_tag, tags|
113
+ lined_tags = {type: 'EntryTag', id: "tag_#{content_tag[tag_id]}"}
114
+ tags << lined_tags
115
+ end
116
+ end
117
+
118
+ def related_table_name(table_name)
119
+ "field_data_#{table_name}".to_sym
120
+ end
121
+
122
+ def field_name(table_name)
123
+ "#{table_name}_value".to_sym
124
+ end
125
+
126
+ def created_at(timestamp)
127
+ Time.at(timestamp).to_datetime
128
+ end
129
+
130
+ def convert_type_value(value, column_name)
131
+ if value.is_a?(BigDecimal)
132
+ value.to_f
133
+ elsif boolean_column?(column_name)
134
+ convert_boolean_value(value)
135
+ else
136
+ value
137
+ end
138
+ end
139
+
140
+ def boolean_column?(column_name)
141
+ exporter.boolean_columns && exporter.boolean_columns.flatten.include?(column_name) ? true : false
142
+ end
143
+
144
+ def convert_boolean_value(value)
145
+ value.nil? ? nil : (value == 1 ? true : false)
146
+ end
147
+
148
+ end
149
+ end
150
+ end
151
+ end