compostr 0.1.0

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.
@@ -0,0 +1,100 @@
1
+ module Compostr
2
+ class EntityCache
3
+ attr_accessor :cpt_class, :name_id_map, :uuid_id_map
4
+ attr_accessor :full_data
5
+
6
+ # cpt_class has to be descendant of Compostr::CustomPostType
7
+ def initialize cpt_class
8
+ @cpt_class = cpt_class
9
+ @name_id_map = nil
10
+ @uuid_id_map = nil
11
+ if !(@cpt_class < Compostr::CustomPostType)
12
+ raise "Unsupported Entity for EntityCache: #{@cpt_class}"
13
+ end
14
+ end
15
+
16
+ # Pretty nasty stuff. Stores all posts of cpt in memory (alas,
17
+ # only once) and looks through them until one with uuid found.
18
+ def in_mem_lookup uuid
19
+ # TODO index (hash) on (uu)id, access index only
20
+ if full_data.length == 10_000
21
+ # warn heavily
22
+ elsif full_data.length > 1000
23
+ # warn softly
24
+ end
25
+ uuid_selector = lambda do |x|
26
+ x["custom_fields"].find do |f|
27
+ f["key"] == "uuid" && f["value"] == uuid
28
+ end
29
+ end
30
+ full_data.find &uuid_selector
31
+ #@full_data.find &WPEvent::Lambdas.with_cf_uuid(uuid)
32
+ end
33
+
34
+ def id_of_name name
35
+ return [] if name.nil? || name.empty?
36
+ name_id_map[name]
37
+ end
38
+
39
+ def id_of_names names
40
+ return [] if names.nil? || names.empty?
41
+ names.map{|name| name_id_map[name]}
42
+ end
43
+
44
+ def id_of_uuid uuid
45
+ return [] if uuid.nil? || uuid.empty?
46
+ uuid_id_map[uuid]
47
+ end
48
+
49
+ def id_of_uuids uuids
50
+ return [] if uuids.nil? || uuids.empty?
51
+ uuids.map{|uuid| id_of_uuid uuid}
52
+ end
53
+
54
+ # init and return @name_id_map
55
+ def name_id_map
56
+ @name_id_map ||= full_data.map {|p| [p["post_title"], p["post_id"]]}.to_h
57
+ end
58
+
59
+ def uuid_id_map
60
+ @uuid_id_map ||= uuid_pid_map
61
+ end
62
+
63
+ # Burn-in cache
64
+ def full_data
65
+ @full_data ||= get_all_posts
66
+ end
67
+
68
+ # Select entities for which given selector returns true
69
+ # example: selector = lambda {|e| e.to_s == 'keepme'}
70
+ def select_by selector
71
+ full_data.select &selector
72
+ end
73
+
74
+ def by_post_id id
75
+ if @full_data.nil?
76
+ Compostr::wp.getPost blog_id: 0,
77
+ post_id: id
78
+ else
79
+ @full_data.find do |p|
80
+ p["post_id"] == id
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def get_all_posts
88
+ Compostr::wp.getPosts blog_id: 0,
89
+ filter: { post_type: @cpt_class.post_type, number: 100_000 }
90
+ end
91
+
92
+ def uuid_pid_map
93
+ full_data.map do |post|
94
+ # TODO lambda
95
+ uuid = post["custom_fields"].find {|f| f["key"] == "uuid"}&.fetch("value", nil)
96
+ [uuid, post["post_id"]]
97
+ end.to_h
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,31 @@
1
+ require 'mime-types'
2
+
3
+ module Compostr
4
+ class ImageUpload
5
+ attr_accessor :file_path, :post_id
6
+
7
+ def initialize file_path, post_id=nil
8
+ # TODO decide on getMediaLibrary to find an already uploaded image
9
+ @file_path = file_path
10
+ @post_id = post_id
11
+ end
12
+
13
+ # Push data to Wordpress instance, return attachment_id
14
+ def do_upload!
15
+ data = create_data
16
+ response = Compostr::wp.uploadFile(data: data)
17
+ response["attachment_id"]
18
+ end
19
+
20
+ private
21
+
22
+ def create_data
23
+ {
24
+ name: File.basename(@file_path),
25
+ type: MIME::Types.type_for(file_path).first.to_s,
26
+ post_id: @post_id || '',
27
+ bits: XMLRPC::Base64.new(IO.read file_path)
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ require 'mime-types'
2
+
3
+ module Compostr
4
+ class ImageUploader
5
+ attr_accessor :image_store
6
+ attr_accessor :media_cache
7
+
8
+ def initialize image_store, media_cache
9
+ if !image_store
10
+ Compostr.logger.warn "ImageUploader will not uploading anything (no image store specified)."
11
+ end
12
+ @image_store = image_store
13
+ @media_cache = media_cache
14
+ end
15
+
16
+ # Returns attachment id of an image already uploaded
17
+ # or attachment_id of image after fresh upload
18
+ # (or nil if path empty)
19
+ def process rel_path, wp_event=nil
20
+ return nil if rel_path.to_s.strip.empty?
21
+ return nil if @image_store.nil?
22
+
23
+ # Do we need URI encoding here?
24
+ if attachment_id = @media_cache.id_of_name(rel_path)
25
+ Compostr.logger.debug "Image already uploaded."
26
+ else
27
+ path = File.join(@image_store, rel_path)
28
+ Compostr.logger.info "Uploading file #{path}"
29
+ upload = Compostr::ImageUpload.new(path)
30
+ attachment_id = upload.do_upload!
31
+ Compostr.logger.debug "Uploaded image id: #{attachment_id}"
32
+ end
33
+
34
+ return attachment_id
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ require 'logger'
2
+
3
+ module Compostr
4
+ # Module to extend to get easy access to standard log functions.
5
+ # These are debug, info, warn, error and fatal.
6
+ # All log functions use the Compostr.logger (which can be customized).
7
+ # A typical client will just `extend Compostr::Logging` .
8
+ module Logging
9
+ def debug msg
10
+ Compostr.logger.debug msg
11
+ end
12
+ def info msg
13
+ Compostr.logger.info msg
14
+ end
15
+ def warn msg
16
+ Compostr.logger.warn msg
17
+ end
18
+ def error msg
19
+ Compostr.logger.error msg
20
+ end
21
+ def fatal msg
22
+ Compostr.logger.fatal msg
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ module Compostr
2
+ # Cache name->id for media items
3
+ # pretty wet copy of entitycache. Unclear how to DRY this up.
4
+ # Wordpress apparently lowercases file endings on upload, so this fact
5
+ # is respected in the lookup (only file ending is modified).
6
+ class MediaLibraryCache
7
+ attr_accessor :name_id_map
8
+
9
+ def initialize
10
+ @name_id_map = nil
11
+ end
12
+
13
+ # return id of given name, initializing the cache
14
+ # if necessary
15
+ def id_of_name name
16
+ return [] if name.nil? || name.empty?
17
+ # Downcase file ending
18
+ name_for_wp = File.basename(name, '.*') + File.extname(name).downcase
19
+ name_id_map[name_for_wp]
20
+ end
21
+
22
+ # return array of ids to given names, initializing the cache
23
+ # if necessary
24
+ def id_of_names names
25
+ return [] if names.nil? || names.empty?
26
+ names.map{|name| id_of_name name }
27
+ end
28
+
29
+ # init and return @name_id_map
30
+ def name_id_map
31
+ if @name_id_map.nil?
32
+ @name_id_map = create_name_id_map
33
+ end
34
+ @name_id_map || {}
35
+ end
36
+
37
+ private
38
+ def create_name_id_map
39
+ items = Compostr::wp.getMediaLibrary(blog_id: 0)
40
+
41
+ # warn if size is interesting.
42
+
43
+ items.map do |i|
44
+ uri = URI.parse URI.encode(i['link'])
45
+ filename = File.basename uri.path
46
+
47
+ [filename, i['attachment_id']]
48
+ end.to_h
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,88 @@
1
+ module Compostr
2
+ # Magically syncs custom posts between json, intermediate representation
3
+ # or other forms with data in wordpress. Scary API award.
4
+ class Syncer
5
+ include Compostr::Logging
6
+
7
+ attr_accessor :image_uploader
8
+ attr_accessor :synced_uuids
9
+ attr_accessor :updated_uuids
10
+
11
+ def initialize image_uploader
12
+ @image_uploader = image_uploader
13
+ @synced_uuids = []
14
+ @updated_uuids = []
15
+ end
16
+
17
+ # Updates or creates Custom Post Types Posts.
18
+ #
19
+ # The post will be identified by uuid (or not).
20
+ #
21
+ # new_post_content
22
+ # The data that **should** be in wordpress (without knowing
23
+ # of wordpress post or custom field ids). Usually descendant of CustomPostType.
24
+ #
25
+ # old_post
26
+ # The data currently available in wordpress (including
27
+ # wordpress post id, custom field ids).
28
+ def merge_push new_post, old_post
29
+ if old_post && old_post.in_wordpress?
30
+ info "#{new_post.class.name} with UUID #{new_post.uuid} found, updating"
31
+ debug old_post.fields.inspect
32
+
33
+ new_post.different_from? old_post
34
+
35
+ new_post.post_id = old_post.post_id
36
+ new_post.integrate_field_ids old_post
37
+
38
+ # TODO unclear how to deal with images
39
+ #attachment_id = @image_uploader.process json['image_url']
40
+ #new_entity.featured_image_id = attachment_id
41
+
42
+ #TODO one (cat) is missing when updating single via bundle json
43
+
44
+ content = new_post.to_content_hash
45
+ adjust_content content
46
+ # TODO and image ...
47
+
48
+ debug "Upload Post ##{new_post.post_id} with wp-content: #{content}"
49
+
50
+ post_id = Compostr::wp.editPost(blog_id: 0,
51
+ post_id: new_post.post_id,
52
+ content: content)
53
+ if post_id
54
+ info "#{new_post.class} ##{new_post.post_id} updated"
55
+ else
56
+ info "#{new_post.class} ##{new_post.post_id} not updated!"
57
+ end
58
+ else
59
+ # Easy, create new one
60
+ info "#{new_post.class.name} with UUID #{new_post.uuid} not found, creating"
61
+ content = new_post.to_content_hash
62
+ adjust_content content
63
+
64
+ # Ouch ....
65
+ #attachment_id = @image_uploader.process json['image_url']
66
+ #custom_post.featured_image_id = attachment_id
67
+
68
+ debug "Create Post with wp-content: #{content}"
69
+
70
+ new_post_id = Compostr::wp.newPost(blog_id: 0,
71
+ content: content)
72
+ if new_post_id
73
+ info "#{new_post.class} with WP ID #{new_post_id} created"
74
+ else
75
+ info "#{new_post.class} not created!"
76
+ end
77
+ new_post.post_id = new_post_id
78
+ end
79
+ end
80
+
81
+ # Add language term and post author data to WP content hash.
82
+ def adjust_content content
83
+ content[:terms_names] = { 'language' => [ Compostr::config.language_term || 'Deutsch' ] }
84
+ content[:post_author] = Compostr::config.author_id || 1
85
+ # publish? Date ... ?
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module Compostr
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,14 @@
1
+ module Compostr
2
+ module WPString
3
+ def self.wp_string string
4
+ content = string.to_s.strip
5
+ if !content.match(/img/)
6
+ re = /<("[^"]*"|'[^']*'|[^'">])*>/
7
+ if content.gsub(re, '') == ''
8
+ return ''
9
+ end
10
+ end
11
+ content
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: http://wordpress.mydomain/xmlrpc.php
6
+ body:
7
+ encoding: UTF-8
8
+ string: '<?xml version="1.0" ?><methodCall><methodName>wp.newPost</methodName><params><param><value><array><data><value><i4>0</i4></value><value><string>admin</string></value><value><string>buzzword</string></value><value><struct><member><name>post_type</name><value><string>boardgame</string></value></member><member><name>post_status</name><value><string>publish</string></value></member><member><name>post_data</name><value><dateTime.iso8601>20170518T11:42:23</dateTime.iso8601></value></member><member><name>post_title</name><value><string>Dungeonlord</string></value></member><member><name>post_content</name><value><string></string></value></member><member><name>custom_fields</name><value><array><data/></array></value></member><member><name>terms_names</name><value><struct><member><name>language</name><value><array><data><value><string>Deutsch</string></value></data></array></value></member></struct></value></member><member><name>post_author</name><value><i4>1</i4></value></member></struct></value></data></array></value></param></params></methodCall>
9
+
10
+ '
11
+ headers:
12
+ User-Agent:
13
+ - XMLRPC::Client (Ruby 2.3.1)
14
+ Content-Type:
15
+ - text/xml; charset=utf-8
16
+ Content-Length:
17
+ - '1056'
18
+ Connection:
19
+ - keep-alive
20
+ Accept-Encoding:
21
+ - identity
22
+ Accept:
23
+ - "*/*"
24
+ response:
25
+ status:
26
+ code: 200
27
+ message: OK
28
+ headers:
29
+ Date:
30
+ - Thu, 18 May 2017 09:31:43 GMT
31
+ Server:
32
+ - Apache/2.2.14 (Ubuntu)
33
+ X-Powered-By:
34
+ - PHP/5.3.2-1ubuntu4.30
35
+ Connection:
36
+ - close
37
+ X-Content-Security-Policy:
38
+ - allow *
39
+ Content-Security-Policy:
40
+ - frame-ancestors 'self'
41
+ Vary:
42
+ - Accept-Encoding
43
+ Content-Length:
44
+ - '179'
45
+ Content-Type:
46
+ - text/xml; charset=UTF-8
47
+ body:
48
+ encoding: UTF-8
49
+ string: |
50
+ <?xml version="1.0" encoding="UTF-8"?>
51
+ <methodResponse>
52
+ <params>
53
+ <param>
54
+ <value>
55
+ <string>2766</string>
56
+ </value>
57
+ </param>
58
+ </params>
59
+ </methodResponse>
60
+ http_version:
61
+ recorded_at: Thu, 18 May 2017 09:42:25 GMT
62
+ recorded_with: VCR 3.0.3
@@ -0,0 +1,61 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: http://wordpress.mydomain/xmlrpc.php
6
+ body:
7
+ encoding: UTF-8
8
+ string: '<?xml version="1.0" ?><methodCall><methodName>wp.deletePost</methodName><params><param><value><array><data><value><i4>0</i4></value><value><string>admin</string></value><value><string>buzzword</string></value><value><string>100000</string></value></data></array></value></param></params></methodCall>
9
+
10
+ '
11
+ headers:
12
+ User-Agent:
13
+ - XMLRPC::Client (Ruby 2.3.1)
14
+ Content-Type:
15
+ - text/xml; charset=utf-8
16
+ Content-Length:
17
+ - '298'
18
+ Connection:
19
+ - keep-alive
20
+ Accept-Encoding:
21
+ - identity
22
+ Accept:
23
+ - "*/*"
24
+ response:
25
+ status:
26
+ code: 200
27
+ message: OK
28
+ headers:
29
+ Date:
30
+ - Mon, 22 May 2017 14:04:20 GMT
31
+ Server:
32
+ - Apache/2.2.14 (Ubuntu)
33
+ X-Powered-By:
34
+ - PHP/5.3.2-1ubuntu4.30
35
+ Connection:
36
+ - close
37
+ X-Content-Security-Policy:
38
+ - allow *
39
+ Content-Security-Policy:
40
+ - frame-ancestors 'self'
41
+ Vary:
42
+ - Accept-Encoding
43
+ Content-Length:
44
+ - '395'
45
+ Content-Type:
46
+ - text/xml; charset=UTF-8
47
+ body:
48
+ encoding: ASCII-8BIT
49
+ string: !binary |-
50
+ PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPG1ldGhv
51
+ ZFJlc3BvbnNlPgogIDxmYXVsdD4KICAgIDx2YWx1ZT4KICAgICAgPHN0cnVj
52
+ dD4KICAgICAgICA8bWVtYmVyPgogICAgICAgICAgPG5hbWU+ZmF1bHRDb2Rl
53
+ PC9uYW1lPgogICAgICAgICAgPHZhbHVlPjxpbnQ+NDA0PC9pbnQ+PC92YWx1
54
+ ZT4KICAgICAgICA8L21lbWJlcj4KICAgICAgICA8bWVtYmVyPgogICAgICAg
55
+ ICAgPG5hbWU+ZmF1bHRTdHJpbmc8L25hbWU+CiAgICAgICAgICA8dmFsdWU+
56
+ PHN0cmluZz5VbmfDvGx0aWdlIEJlaXRyYWdzLUlELjwvc3RyaW5nPjwvdmFs
57
+ dWU+CiAgICAgICAgPC9tZW1iZXI+CiAgICAgIDwvc3RydWN0PgogICAgPC92
58
+ YWx1ZT4KICA8L2ZhdWx0Pgo8L21ldGhvZFJlc3BvbnNlPgo=
59
+ http_version:
60
+ recorded_at: Mon, 22 May 2017 14:15:31 GMT
61
+ recorded_with: VCR 3.0.3