webflow_sync 1.1.0 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e85c49b258ebbde8c6d5984893be9f59acf389505f0af30951d98fccc4b2f0d
4
- data.tar.gz: '09b216274b17cfd61ff9aa1c2a2cc984b12655302daa4d25e3bb0e37a29b1180'
3
+ metadata.gz: 071c32fa2d8e2e8cd05717a410d44b382b52487f00111113dbbecdb4de1c5280
4
+ data.tar.gz: 15deffb7ed2f04269423ec82498160afb8a928e26b9021a085b6e433a1a52bb7
5
5
  SHA512:
6
- metadata.gz: 3bea1e564a550a72cf7164e3cce4fb7710478e3683c8a1067b1dfc790b87679a59f4a03985fdf610628deeb4334d0d5bd540391afb85d4a41cbb85b67f323525
7
- data.tar.gz: bc6bba426b31b48d20caa8ebbfa88e3ec3237b51faa8ac520e715c73fdedf18c1807eff59932e512200d47ed0fb7e921c0ca10b2cd5531cba5cc0096487f6323
6
+ metadata.gz: b8aeed4c68a939a8a377a5c1e6eee0154fa19778c0b5f1ac725ad36df80d8bcd8f4505bd07c48c6d44dcc3c71b26166a833e512fa7af801da8fc7fcd7f9ff6ad
7
+ data.tar.gz: 307d91073258ab1f08c1d47d47d6126f315ee2be9ed9781dc6ba05e299f846a483843798d2c80cff902767995677e99632e0bb1fb5efb0d38ff63e3883e41975
data/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
 
6
6
  Keep your Ruby on Rails records in sync with WebFlow.*
7
7
 
8
- *Currently only one way Rails => WebFlow synchronization works
8
+ *Currently only one way Rails => WebFlow synchronization.
9
+
10
+ For the latest changes, check the [CHANGELOG.md](CHANGELOG.md).
9
11
 
10
12
  ## Installation
11
13
 
@@ -36,6 +38,35 @@ Run API token Rails generator and follow instructions:
36
38
  ```bash
37
39
  bundle exec rails generate webflow_sync:api_token_flow
38
40
  ```
41
+ ### Configuration options
42
+
43
+ In `config/initializers/webflow_sync.rb` you can specify configuration options:
44
+
45
+ 1. `api_token`
46
+ 2. `webflow_site_id`
47
+ 3. `skip_webflow_sync` - skip synchronization for different environments.
48
+ 4. `publish_on_sync` - republish all domains after create, update and destroy actions.
49
+ 5. `sync_webflow_slug` - save slug generated on WebFlow to the Rails model, `webflow_slug` column.
50
+
51
+ This can be useful if you want to link to WebFlow item directly from your Rails app:
52
+
53
+ ```rb
54
+ link_to('View on site', "https://#{webflow_domain}/articles/#{record.webflow_slug}", target: :blank)
55
+ ```
56
+
57
+ To save slug generated on WebFlow in Rails model, `webflow_slug` column:
58
+
59
+ 1. add `webflow_slug` column on the model table, then
60
+ 2. set the `sync_webflow_slug` option to `true`.
61
+
62
+ Example:
63
+
64
+ ```rb
65
+ WebflowSync.configure do |config|
66
+ config.skip_webflow_sync = ActiveModel::Type::Boolean.new.cast(ENV.fetch('SKIP_WEBFLOW_SYNC'))
67
+ config.sync_webflow_slug = ActiveModel::Type::Boolean.new.cast(ENV.fetch('SYNC_WEBFLOW_SLUG'))
68
+ end
69
+ ```
39
70
 
40
71
  ### Add WebflowSync to models
41
72
 
@@ -51,17 +82,27 @@ Please note that this _does not_ create a WebFlow collection as that's not possi
51
82
 
52
83
  As mentioned above, you need to create the WebFlow collection yourself.
53
84
 
54
- Make sure that the collection `slug` matches the Rails model collection name (the output of `model_class.model_name.collection`).
85
+ Make sure that the collection `slug` matches the Rails model collection name (the output of `model_class.model_name.to_s.underscore.dasherize.pluralize`).
55
86
 
56
87
  For example, for `Article` model:
57
88
 
58
89
  ```ruby
59
- > Article.model_name.collection
90
+ > Article.model_name.to_s.underscore.dasherize.pluralize
60
91
  # => "articles"
61
92
  ```
62
93
 
63
94
  Your WebFlow collection `slug` should be `"articles"`.
64
95
 
96
+ For Rails models named with multiple words, make a collection that have a space between words in the name. WebFlow will set the `slug` by replacing space for "-".
97
+
98
+ For example, for `FeaturedArticle` model:
99
+
100
+ ```ruby
101
+ > FeaturedArticle.model_name.to_s.underscore.dasherize.pluralize
102
+ # => "featured-articles"
103
+ ```
104
+ Your WebFlow collection `slug` in this case should be `"featured-articles"`.
105
+
65
106
  ### Set `webflow_site_id`
66
107
 
67
108
  There are couple of ways how you can set the `webflow_site_id` to be used.
@@ -128,13 +169,43 @@ Or if you already use `#as_json` for some other use-case and cannot modify it, y
128
169
  # app/models/article.rb
129
170
  class Article < ApplicationRecord
130
171
  include WebflowSync::ItemSync
131
-
172
+
132
173
  def as_webflow_json
133
174
  self.as_json
134
175
  end
135
176
  end
136
177
  ```
137
178
 
179
+ ### Sync a Rails model with the custom collection
180
+
181
+ If collection `slug` does not match the Rails model collection name, you can still sync with WebFlow collection, but need to specify collection slug for each `CreateItemJob` and `UpdateItemJob` call. If not specified, model name is used as a default slug name.
182
+ You would also need to replace included `WebflowSync::ItemSync` with custom callbacks that will call appropriate WebflowSync jobs.
183
+
184
+ For example:
185
+
186
+ ```ruby
187
+ WebflowSync::CreateItemJob.perform_later(model_name, id, collection_slug)
188
+ WebflowSync::UpdateItemJob.perform_later(model_name, id, collection_slug)
189
+ WebflowSync::DestroyItemJob.perform_later(collection_slug:, webflow_site_id:, webflow_item_id:)
190
+ ```
191
+
192
+ Where:
193
+
194
+ 1. `model_name` - Rails model that has `webflow_site_id` and `webflow_item_id` defined
195
+ 2. `collection_slug` - slug of the WebFlow collection (defaults to: `model_name.underscore.dasherize.pluralize `)
196
+
197
+
198
+ For example:
199
+
200
+ ```ruby
201
+ WebflowSync::CreateItemJob.perform_now('articles', 1, 'stories')
202
+ ```
203
+ Or, if you want to use the default 'articles' collection_slug:
204
+
205
+ ```ruby
206
+ WebflowSync::CreateItemJob.perform_now('articles', 1)
207
+ ```
208
+
138
209
  ### Run the initial sync
139
210
 
140
211
  After setting up which models you want to sync to WebFlow, you can run the initial sync for each of the models:
@@ -2,8 +2,16 @@
2
2
 
3
3
  module WebflowSync
4
4
  class CreateItemJob < ApplicationJob
5
- def perform(collection_slug, id)
6
- model_class = collection_slug.classify.constantize
5
+ # WebFlow collection slug should be in plural form.
6
+ # For collections that have spaces in their name, WebFlow sets slug by replacing space for "-".
7
+ # 'JobListing'.underscore.dasherize.pluralize => "job-listings"
8
+ # 'job_listing'.underscore.dasherize.pluralize => "job-listings"
9
+ # We can sync Rails model that has different class name than its WebFlow collection
10
+ # model_name => Rails model that has webflow_site_id and webflow_item_id columns
11
+ # collection_slug => slug of the WebFlow collection
12
+ # model_name = 'articles'; id = article.id, collection_slug = 'stories'
13
+ def perform(model_name, id, collection_slug = model_name.underscore.dasherize.pluralize)
14
+ model_class = model_name.underscore.classify.constantize
7
15
  record = model_class.find_by(id: id)
8
16
  return if record.blank?
9
17
  return if record.webflow_site_id.blank?
@@ -3,7 +3,7 @@
3
3
  module WebflowSync
4
4
  class InitialSyncJob < ApplicationJob
5
5
  def perform(collection_slug)
6
- model_class = collection_slug.classify.constantize
6
+ model_class = collection_slug.underscore.classify.constantize
7
7
  model_class.where(webflow_item_id: nil).find_each do |record|
8
8
  next if record.webflow_site_id.blank?
9
9
 
@@ -2,12 +2,16 @@
2
2
 
3
3
  module WebflowSync
4
4
  class UpdateItemJob < ApplicationJob
5
- def perform(collection_slug, id)
6
- model_class = collection_slug.classify.constantize
5
+ # WebFlow collection slug should be in plural form.
6
+ # For collections that have spaces in their name, WebFlow sets slug by replacing space for "-".
7
+ # 'JobListing'.underscore.dasherize.pluralize => 'job-listings'
8
+ # 'job_listing'.underscore.dasherize.pluralize => 'job-listings'
9
+ def perform(model_name, id, collection_slug = model_name.underscore.dasherize.pluralize)
10
+ model_class = model_name.underscore.classify.constantize
7
11
  record = model_class.find_by(id: id)
8
12
  return if record.blank?
9
13
  return if record.webflow_site_id.blank?
10
- return WebflowSync::CreateItemJob.perform_now(collection_slug, id) if record.webflow_item_id.blank?
14
+ return WebflowSync::CreateItemJob.perform_now(model_name, id, collection_slug) if record.webflow_item_id.blank?
11
15
 
12
16
  WebflowSync::Api.new(record.webflow_site_id).update_item(record, collection_slug)
13
17
  end
@@ -26,8 +26,10 @@ module WebflowSync
26
26
 
27
27
  def destroy_webflow_item
28
28
  return if self.skip_webflow_sync || WebflowSync.configuration.skip_webflow_sync
29
+ # Make sure slug is in the pulural form, and multiple words slug separated by dashes
30
+ collection_slug = self.model_name.collection.underscore.dasherize
29
31
 
30
- WebflowSync::DestroyItemJob.perform_later(collection_slug: self.model_name.collection,
32
+ WebflowSync::DestroyItemJob.perform_later(collection_slug: collection_slug,
31
33
  webflow_site_id: self.webflow_site_id,
32
34
  webflow_item_id: self.webflow_item_id)
33
35
  end
@@ -8,20 +8,45 @@ module WebflowSync
8
8
  @site_id = site_id
9
9
  end
10
10
 
11
+ def get_all_items(collection_slug:, page_limit: 100) # rubocop:disable Metrics/MethodLength
12
+ collection_id = find_webflow_collection(collection_slug)['_id']
13
+ max_items_per_page = page_limit # Webflow::Error: 'limit' must be less than or equal to 100
14
+ first_page_number = 1
15
+
16
+ result = make_request(:paginate_items, collection_id, page: first_page_number, per_page: max_items_per_page)
17
+ puts "Get all items from WebFlow for #{collection_slug} page: #{first_page_number}"
18
+
19
+ total_items = result['total']
20
+ total_pages = (total_items.to_f / max_items_per_page).ceil
21
+ items = result['items']
22
+
23
+ (2..total_pages).each do |page_number|
24
+ next_page_items = make_request(:paginate_items, collection_id,
25
+ page: page_number, per_page: max_items_per_page)['items']
26
+ puts "Get all items from WebFlow for #{collection_slug} page: #{page_number}"
27
+
28
+ items.concat next_page_items
29
+ end
30
+
31
+ items
32
+ end
33
+
11
34
  def get_item(collection_slug, webflow_item_id)
12
35
  collection = find_webflow_collection(collection_slug)
13
- client.item(collection['_id'], webflow_item_id)
36
+
37
+ make_request(:item, collection['_id'], webflow_item_id)
14
38
  end
15
39
 
16
40
  def create_item(record, collection_slug) # rubocop:disable Metrics/MethodLength
17
41
  collection = find_webflow_collection(collection_slug)
18
- response = client.create_item(
19
- collection['_id'],
20
- record.as_webflow_json.reverse_merge(_archived: false, _draft: false), live: true
21
- )
42
+ response = make_request(:create_item, collection['_id'],
43
+ record.as_webflow_json.reverse_merge(_archived: false, _draft: false), live: true)
44
+
45
+ if update_record_colums(record, response)
46
+ # When the item is created, sometimes changes are not visible throughout the WebFlow site immediately (probably due to some caching).
47
+ # To make this change immediately visible from the WebFlow site, we need to publish the site.
48
+ publish
22
49
 
23
- # use update_column to skip callbacks to prevent WebflowSync::ItemSync to kick off
24
- if record.update_column(:webflow_item_id, response['_id']) # rubocop:disable Rails/SkipsModelValidations
25
50
  puts "Created #{record.inspect} in #{collection_slug}"
26
51
  response
27
52
  else
@@ -32,21 +57,37 @@ module WebflowSync
32
57
 
33
58
  def update_item(record, collection_slug)
34
59
  collection = find_webflow_collection(collection_slug)
35
- response = client.update_item(
36
- { '_cid' => collection['_id'], '_id' => record.webflow_item_id },
37
- record.as_webflow_json.reverse_merge(_archived: false, _draft: false), live: true
38
- )
60
+ response = make_request(:update_item, { '_cid' => collection['_id'], '_id' => record.webflow_item_id },
61
+ record.as_webflow_json.reverse_merge(_archived: false, _draft: false), live: true)
62
+
63
+ # When the item is updated, sometimes changes are not visible throughout the WebFlow site immediately (probably due to some caching).
64
+ # To make this change immediately visible from the WebFlow site, we need to publish the site.
65
+ publish
66
+
39
67
  puts "Updated #{record.inspect} in #{collection_slug}"
40
68
  response
41
69
  end
42
70
 
43
71
  def delete_item(collection_slug, webflow_item_id)
44
72
  collection = find_webflow_collection(collection_slug)
45
- response = client.delete_item({ '_cid' => collection['_id'], '_id' => webflow_item_id })
73
+ response = make_request(:delete_item, { '_cid' => collection['_id'], '_id' => webflow_item_id })
74
+ # When the item is removed from WebFlow, it's still visible throughout the WebFlow site (probably due to some caching).
75
+ # To remove the item immediately from the WebFlow site, we need to publish the site.
76
+ publish
77
+
46
78
  puts "Deleted #{webflow_item_id} from #{collection_slug}"
47
79
  response
48
80
  end
49
81
 
82
+ def publish
83
+ return unless WebflowSync.configuration.publish_on_sync
84
+
85
+ response = make_request(:publish, site_id, domain_names: domain_names)
86
+
87
+ puts "Publish all domains for Webflow site with id: #{site_id}"
88
+ response
89
+ end
90
+
50
91
  private
51
92
 
52
93
  def client
@@ -57,11 +98,50 @@ module WebflowSync
57
98
  @collections ||= client.collections(site_id)
58
99
  end
59
100
 
101
+ def domain_names
102
+ @domain_names ||= find_domain_names
103
+ end
104
+
105
+ def find_domain_names
106
+ # client.domains request does not return the default domain
107
+ # We need to get default domain name if there are no custom domains set to avoid error:
108
+ # Webflow::Error: Domain not found for site
109
+ site = client.site(site_id)
110
+ default_domain_name = "#{site['shortName']}.webflow.io"
111
+ names = [default_domain_name]
112
+ client.domains(site_id).each { |domain| names << domain['name'] }
113
+
114
+ names
115
+ end
116
+
60
117
  def find_webflow_collection(collection_slug)
61
118
  response = collections.find { |collection| collection['slug'] == collection_slug }
62
119
  raise "Cannot find collection #{collection_slug} for Webflow site #{site_id}" unless response
63
120
 
64
121
  response
65
122
  end
123
+
124
+ def update_record_colums(record, response)
125
+ # use update_column to skip callbacks to prevent WebflowSync::ItemSync to kick off
126
+ if WebflowSync.configuration.sync_webflow_slug
127
+ record.update_columns(webflow_item_id: response['_id'], webflow_slug: response['slug']) # rubocop:disable Rails/SkipsModelValidations
128
+ else
129
+ record.update_column(:webflow_item_id, response['_id']) # rubocop:disable Rails/SkipsModelValidations
130
+ end
131
+ end
132
+
133
+ def make_request(method_name, *args, retries: 0, **kwargs)
134
+ if kwargs.present?
135
+ client.public_send(method_name, *args, **kwargs)
136
+ else
137
+ client.public_send(method_name, *args)
138
+ end
139
+ rescue Webflow::Error => e
140
+ raise if retries >= 8 || e.message.strip != 'Rate limit hit'
141
+
142
+ puts "Sleeping #{2**retries} seconds"
143
+ sleep 2**retries
144
+ make_request(method_name, *args, retries: retries + 1, **kwargs)
145
+ end
66
146
  end
67
147
  end
@@ -4,4 +4,6 @@ WebflowSync.configure do |config|
4
4
  # config.api_token = ENV.fetch('WEBFLOW_API_TOKEN')
5
5
  # config.skip_webflow_sync = Rails.env.test? # default
6
6
  config.webflow_site_id = ENV.fetch('WEBFLOW_SITE_ID')
7
+ config.sync_webflow_slug = ENV.fetch('SYNC_WEBFLOW_SLUG')
8
+ config.publish_on_sync = true
7
9
  end
@@ -6,10 +6,13 @@ module WebflowSync
6
6
 
7
7
  def configure
8
8
  self.configuration ||= Configuration.new
9
+
10
+ self.configuration.publish_on_sync = true
11
+ self.configuration.skip_webflow_sync = !Rails.env.production?
12
+
9
13
  yield(self.configuration)
10
14
 
11
15
  self.configuration.api_token ||= ENV.fetch('WEBFLOW_API_TOKEN')
12
- defined?(self.configuration.skip_webflow_sync) or self.configuration.skip_webflow_sync = !Rails.env.production?
13
16
  end
14
17
 
15
18
  private
@@ -18,7 +21,7 @@ module WebflowSync
18
21
  end
19
22
 
20
23
  class Configuration
21
- attr_accessor :skip_webflow_sync, :webflow_site_id
24
+ attr_accessor :skip_webflow_sync, :webflow_site_id, :sync_webflow_slug, :publish_on_sync
22
25
  attr_reader :api_token
23
26
 
24
27
  def api_token=(value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebflowSync
4
- VERSION = '1.1.0'
4
+ VERSION = '3.1.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webflow_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Viktor
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-25 00:00:00.000000000 Z
11
+ date: 2021-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dotenv-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: factory_bot_rails
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -173,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
187
  - !ruby/object:Gem::Version
174
188
  version: '0'
175
189
  requirements: []
176
- rubygems_version: 3.2.11
190
+ rubygems_version: 3.0.9
177
191
  signing_key:
178
192
  specification_version: 4
179
193
  summary: Keep Rails models in sync with WebFlow.