drupal-exporter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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