scrivito_sdk 0.60.0 → 0.65.0.rc1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/app/controllers/scrivito/objs_controller.rb +10 -2
  4. data/app/controllers/scrivito/workspaces_controller.rb +2 -1
  5. data/app/helpers/scrivito_helper.rb +32 -1
  6. data/app/views/scrivito/page_details.html.erb +1 -0
  7. data/app/views/scrivito/{workspaces → webservice}/_workspace.json.jbuilder +0 -0
  8. data/app/views/scrivito/webservice/error.json.jbuilder +1 -1
  9. data/app/views/scrivito/{workspaces/index.json.jbuilder → webservice/workspaces.json.jbuilder} +0 -0
  10. data/config/ca-bundle.crt +1 -1
  11. data/config/precedence_routes.rb +1 -0
  12. data/config/routes.rb +4 -5
  13. data/lib/assets/javascripts/scrivito_ui.js +1875 -1065
  14. data/lib/assets/stylesheets/scrivito_sdk.css +1 -1
  15. data/lib/assets/stylesheets/scrivito_ui.css +1 -1
  16. data/lib/generators/scrivito/install/templates/app/views/page/details.html.erb +3 -1
  17. data/lib/generators/scrivito/page/templates/details.html.erb +3 -1
  18. data/lib/scrivito/attribute_content.rb +159 -17
  19. data/lib/scrivito/attribute_serializer.rb +7 -3
  20. data/lib/scrivito/backend/content_state_node.rb +68 -0
  21. data/lib/scrivito/backend/index.rb +45 -0
  22. data/lib/scrivito/backend/obj_data_cache.rb +68 -0
  23. data/lib/scrivito/backend/obj_data_from_rest.rb +64 -0
  24. data/lib/scrivito/backend/obj_load.rb +45 -0
  25. data/lib/scrivito/backend/obj_query.rb +63 -0
  26. data/lib/scrivito/backend/parent_path_index.rb +21 -0
  27. data/lib/scrivito/backend/path_index.rb +27 -0
  28. data/lib/scrivito/backend/permalink_index.rb +17 -0
  29. data/lib/scrivito/basic_obj.rb +67 -39
  30. data/lib/scrivito/basic_widget.rb +19 -3
  31. data/lib/scrivito/cache_middleware.rb +9 -3
  32. data/lib/scrivito/client_error.rb +3 -3
  33. data/lib/scrivito/cms_backend.rb +64 -18
  34. data/lib/scrivito/cms_data_cache.rb +33 -30
  35. data/lib/scrivito/cms_dispatch_controller.rb +18 -0
  36. data/lib/scrivito/cms_field_tag.rb +3 -2
  37. data/lib/scrivito/cms_rest_api.rb +18 -12
  38. data/lib/scrivito/cms_rest_api/rate_limit.rb +40 -0
  39. data/lib/scrivito/cms_routing.rb +9 -8
  40. data/lib/scrivito/configuration.rb +8 -1
  41. data/lib/scrivito/controller_actions.rb +6 -1
  42. data/lib/scrivito/errors.rb +5 -0
  43. data/lib/scrivito/migrations/cms_backend.rb +2 -0
  44. data/lib/scrivito/named_link.rb +9 -45
  45. data/lib/scrivito/obj_collection.rb +14 -15
  46. data/lib/scrivito/obj_create_params_parser.rb +3 -5
  47. data/lib/scrivito/obj_params_parser.rb +2 -2
  48. data/lib/scrivito/obj_update_params_parser.rb +5 -3
  49. data/lib/scrivito/revision.rb +62 -2
  50. data/lib/scrivito/type_computer.rb +6 -2
  51. data/lib/scrivito/widget_tag.rb +4 -4
  52. data/lib/scrivito/workspace.rb +19 -3
  53. data/lib/scrivito/workspace_data.rb +23 -0
  54. data/lib/scrivito/workspace_data_from_service.rb +11 -28
  55. metadata +31 -7
  56. data/lib/scrivito/workspace_data_from_rest_api.rb +0 -6
@@ -1,11 +1,11 @@
1
1
  module Scrivito
2
2
 
3
3
  class ClientError < StandardError
4
- attr_reader :http_code, :error_code
4
+ attr_reader :http_code, :backend_code
5
5
 
6
- def initialize(message, http_code, error_code=nil)
6
+ def initialize(message, http_code, backend_code = nil)
7
7
  @http_code = http_code
8
- @error_code = error_code
8
+ @backend_code = backend_code
9
9
  super(message)
10
10
  end
11
11
  end
@@ -55,8 +55,11 @@ module Scrivito
55
55
  end
56
56
  end
57
57
 
58
+ attr_accessor :die_content_service
59
+
58
60
  def initialize
59
61
  @query_counter = 0
62
+ @caching = true
60
63
  end
61
64
 
62
65
  def begin_caching
@@ -68,6 +71,10 @@ module Scrivito
68
71
  @caching = false
69
72
  end
70
73
 
74
+ def clear_cache
75
+ CmsDataCache.cache.clear
76
+ end
77
+
71
78
  def caching?
72
79
  !!@caching
73
80
  end
@@ -82,6 +89,33 @@ module Scrivito
82
89
  end
83
90
 
84
91
  def find_workspace_data_by_id(id)
92
+ if die_content_service
93
+ from_csid = CmsDataCache.read_workspace_csid(id)
94
+
95
+ changes = CmsRestApi.get("/workspaces/#{id}/changes", from: from_csid)
96
+
97
+ objs = changes["objs"]
98
+ if objs.present?
99
+ if objs != "*"
100
+ last_state = Backend::ContentStateNode.find(from_csid)
101
+
102
+ successor_state = changes["to"]
103
+ current_state = last_state.create_successor(successor_state, objs)
104
+ end
105
+
106
+ # TODO what if workspace data is missing?
107
+ # have the backend team include a new key `current`
108
+ current_csid = changes["workspace"]["content_state_id"]
109
+ CmsDataCache.write_workspace_csid(id, current_csid)
110
+ end
111
+
112
+ # TODO what if workspace data is missing?
113
+ # implement workspace data caching
114
+ workspace_data = changes["workspace"]
115
+
116
+ return WorkspaceData.new(workspace_data)
117
+ end
118
+
85
119
  workspace_data_from_cache = WorkspaceDataFromService.find_from_cache(id)
86
120
  from_content_state_id = workspace_data_from_cache.try(:content_state_id)
87
121
 
@@ -101,17 +135,37 @@ module Scrivito
101
135
 
102
136
  if raw_workspace_data = raw_data['workspace']
103
137
  workspace_data = WorkspaceDataFromService.new(raw_workspace_data)
104
- workspace_data.store_in_cache if from_content_state_id != workspace_data.content_state_id
138
+ if from_content_state_id != workspace_data.content_state_id
139
+ workspace_data.store_in_cache_and_create_content_state
140
+ end
105
141
  workspace_data
106
142
  end
107
143
  end
108
144
 
109
145
  def find_obj_data_by(revision, index, keys)
110
- find_obj_data_filtering_deleted_by(revision, index, keys, false)
111
- end
146
+ index = index.to_s
147
+
148
+ if die_content_service
149
+ obj_datas =
150
+ if index == "id"
151
+ obj_datas = Backend::ObjLoad.load(revision, keys)
152
+ obj_datas.map { |obj_data| obj_data ? [obj_data] : [] }
153
+ else
154
+ index_implementation = Backend::Index.by_name(index)
155
+ Backend::ObjQuery.query(revision, index_implementation, keys)
156
+ end
157
+
158
+ return obj_datas
159
+ end
112
160
 
113
- def find_obj_data_including_deleted_by(revision, index, keys)
114
- find_obj_data_filtering_deleted_by(revision, index, keys, true)
161
+ assert_valid_index_name(index)
162
+ raw_data = find_raw_data_from_cache_or_database_by(revision, index, keys)
163
+
164
+ raw_data.map do |raw_result|
165
+ raw_result.each_with_object([]) do |raw_data, result|
166
+ result << ObjDataFromService.new(raw_data)
167
+ end
168
+ end
115
169
  end
116
170
 
117
171
  def find_blob_data(id, access, verb)
@@ -136,6 +190,11 @@ module Scrivito
136
190
  end
137
191
 
138
192
  def search_objs(workspace, params)
193
+ if die_content_service
194
+ # TODO caching
195
+ return request_search_result_from_backend(workspace, params)
196
+ end
197
+
139
198
  content_state = workspace.revision.content_state
140
199
  cache_index = 'search'
141
200
  cache_key = params.to_param
@@ -249,19 +308,6 @@ module Scrivito
249
308
  "blob_metadata/#{id}"
250
309
  end
251
310
 
252
- def find_obj_data_filtering_deleted_by(revision, index, keys, include_deleted)
253
- index = index.to_s
254
- assert_valid_index_name(index)
255
- raw_data = find_raw_data_from_cache_or_database_by(revision, index, keys)
256
-
257
- raw_data.map do |raw_result|
258
- raw_result.each_with_object([]) do |raw_data, result|
259
- next if raw_data['_modification'] == ['deleted'] && !include_deleted
260
- result << ObjDataFromService.new(raw_data)
261
- end
262
- end
263
- end
264
-
265
311
  def find_raw_data_from_cache_or_database_by(revision, index, keys)
266
312
  keys_from_database = []
267
313
  # load results from cache
@@ -26,36 +26,39 @@ module CmsDataCache
26
26
  @second_level_cache = cache_store
27
27
  end
28
28
 
29
- def read_workspace_data(workspace_id)
30
- cache.read("workspace/#{workspace_id}")
31
- end
32
-
33
- def write_workspace_data(workspace_id, workspace_data)
34
- cache.write("workspace/#{workspace_id}", workspace_data)
35
- end
36
-
37
- def read_content_state(content_state_id)
38
- cache.read("content/#{content_state_id}")
39
- end
40
-
41
- def write_content_state(content_state_id, content_state)
42
- cache.write("content/#{content_state_id}", content_state)
43
- end
44
-
45
- def read_obj_data(content_state_id, index, key)
46
- cache.read("content/#{content_state_id}/obj/#{index}/#{key}")
47
- end
48
-
49
- def write_obj_data(content_state_id, index, key, data)
50
- cache.write("content/#{content_state_id}/obj/#{index}/#{key}", data)
51
- end
52
-
53
- def read_obj_classes_data(content_state_id)
54
- cache.read("content/#{content_state_id}/obj_classes")
55
- end
56
-
57
- def write_obj_classes_data(content_state_id, data)
58
- cache.write("content/#{content_state_id}/obj_classes", data)
29
+ SCHEMA = {
30
+ workspace_data: 'workspace/#{workspace_id}',
31
+
32
+ # CONTENT SERVICE
33
+ content_state: 'content/#{content_state_id}',
34
+ obj_data: 'content/#{content_state_id}/obj/#{index}/#{key}',
35
+ obj_classes_data: 'content/#{content_state_id}/obj_classes',
36
+
37
+ # REST API
38
+ obj_data_rest: 'obj/#{cache_id}/#{index}/#{key}',
39
+ content_state_node: 'csn/#{content_state_id}',
40
+ workspace_csid: 'wscsid/#{workspace_id}'
41
+ }
42
+
43
+ SCHEMA.each do |name, schema|
44
+ params = schema.scan(/\#{([^}]+)}/).map(&:first)
45
+ params_code = params.map(&:to_s).join(",")
46
+
47
+ # using eval instead of define_method for performance reasons
48
+ class_eval(<<-END, __FILE__, __LINE__ + 1)
49
+ def read_#{name}(#{params_code})
50
+ cache.read("#{schema}")
51
+ end
52
+ END
53
+
54
+ class_eval(<<-END, __FILE__, __LINE__ + 1)
55
+ def write_#{name}(#{params_code}, data)
56
+ if data == nil
57
+ raise InternalError, "tried to write nil into #{schema}"
58
+ end
59
+ cache.write("#{schema}", data)
60
+ end
61
+ END
59
62
  end
60
63
 
61
64
  private
@@ -1,8 +1,22 @@
1
1
  module Scrivito
2
2
 
3
3
  class CmsDispatchController < ActionController::Metal
4
+ include ActionController::Redirecting
5
+ include Rails.application.routes.url_helpers
6
+ include Scrivito::RoutingHelper
7
+
4
8
  def process(action)
5
9
  CmsEnv.new(env).load
10
+
11
+ if !obj_not_found? && action == 'legacy'
12
+ if Scrivito::Configuration.legacy_routing
13
+ action = 'index'
14
+ else
15
+ redirect_to scrivito_path(loaded_obj)
16
+ return self.response
17
+ end
18
+ end
19
+
6
20
  controller = target_controller(env)
7
21
  env["action_dispatch.request.path_parameters"]["controller"] = controller.controller_path
8
22
 
@@ -17,6 +31,10 @@ module Scrivito
17
31
 
18
32
  private
19
33
 
34
+ def main_app
35
+ Rails.application.routes.url_helpers
36
+ end
37
+
20
38
  def target_controller(env)
21
39
  return default_controller if obj_not_found?
22
40
  controller = "#{loaded_obj.controller_name}Controller".constantize
@@ -1,7 +1,7 @@
1
1
  module Scrivito
2
2
 
3
3
  # this class is the server-side equivalent of the JavaScript class `cms_field_element`
4
- class CmsFieldTag < Struct.new(:view, :tag_name, :obj_or_widget, :field_name)
4
+ class CmsFieldTag < Struct.new(:view, :tag_name, :obj_or_widget, :field_name, :widget_template_name)
5
5
  FIELD_TYPES_WITH_ORIGINAL_CONTENT = %w[
6
6
  binary
7
7
  date
@@ -21,7 +21,7 @@ class CmsFieldTag < Struct.new(:view, :tag_name, :obj_or_widget, :field_name)
21
21
  raise ArgumentError, 'No block allowed for widgetlist fields' if block_given?
22
22
  modifications = modification_info || []
23
23
  rendered_widgets = default_content.each_with_index.map do |widget, index|
24
- WidgetTag.new(view, widget, modifications[index]).render
24
+ WidgetTag.new(view, widget, modifications[index], widget_template_name).render
25
25
  end
26
26
  view.safe_join(rendered_widgets)
27
27
  else
@@ -60,6 +60,7 @@ class CmsFieldTag < Struct.new(:view, :tag_name, :obj_or_widget, :field_name)
60
60
 
61
61
  if field_type == 'widgetlist'
62
62
  options['private-field-widget-allowed-classes'] = build_valid_widget_classes.to_json
63
+ options['private-field-widget-template'] = widget_template_name
63
64
  end
64
65
 
65
66
  options
@@ -32,6 +32,8 @@ module Scrivito
32
32
  :delete => Net::HTTP::Delete,
33
33
  }.freeze
34
34
 
35
+ MAX_RATE_LIMIT_RETRY_DURATION = 20.seconds.freeze
36
+
35
37
  def self.get(resource_path, payload = nil, options = nil)
36
38
  request_cms_api(:get, resource_path, payload, options)
37
39
  end
@@ -100,9 +102,12 @@ module Scrivito
100
102
 
101
103
  response = nil
102
104
  retried = false
105
+ wait_until = Time.now + MAX_RATE_LIMIT_RETRY_DURATION
103
106
  begin
104
- # lower timeout back to DEFAULT_TIMEOUT once the backend has been fixed
105
- response = connection_manager.request(request, 25)
107
+ response = CmsRestApi::RateLimit.retry_on_rate_limit(wait_until) do
108
+ # lower timeout back to DEFAULT_TIMEOUT once the backend has been fixed
109
+ connection_manager.request(request, 25)
110
+ end
106
111
  rescue NetworkError => e
107
112
  if method == :post || retried
108
113
  raise e
@@ -112,11 +117,11 @@ module Scrivito
112
117
  end
113
118
  end
114
119
 
115
- handle_response(response)
120
+ handle_response(resource_path, response)
116
121
  end
117
122
 
118
- def handle_response(response)
119
- code = response.code.to_i
123
+ def handle_response(resource_path, response)
124
+ http_code = response.code.to_i
120
125
  if response.code.start_with?('2')
121
126
  MultiJson.load(response.body)
122
127
  elsif response.code == '403'
@@ -127,15 +132,16 @@ module Scrivito
127
132
  specific_output = error_body['error']
128
133
 
129
134
  if response.code.start_with?('4')
130
- error_code = error_body['code']
131
- raise ClientError.new(specific_output, code, error_code)
135
+ backend_code = error_body['code']
136
+ message = "'#{specific_output}' on '#{resource_path}'"
137
+ raise ClientError.new(specific_output, http_code, backend_code)
132
138
  elsif response.code == '500' && specific_output
133
- raise BackendError.new(specific_output, code)
139
+ raise BackendError.new(specific_output, http_code)
134
140
  else # 3xx and >500 are treated as NetworkErrors
135
- raise NetworkError.new(response.body, code)
141
+ raise NetworkError.new(response.body, http_code)
136
142
  end
137
143
  rescue MultiJson::DecodeError
138
- raise NetworkError.new(response.body, code)
144
+ raise NetworkError.new(response.body, http_code)
139
145
  end
140
146
  end
141
147
  end
@@ -151,8 +157,8 @@ module Scrivito
151
157
  end
152
158
  return task_data["result"] if task_data["status"] == "success"
153
159
  message = task_data["message"] || "Missing error message in task response #{task_data}"
154
- error_code = task_data['code']
155
- raise ClientError.new(message, 400, error_code)
160
+ backend_code = task_data['code']
161
+ raise ClientError.new(message, 400, backend_code)
156
162
  end
157
163
 
158
164
  def set_headers(request)
@@ -0,0 +1,40 @@
1
+ module Scrivito
2
+ class CmsRestApi
3
+ module RateLimit
4
+ class << self
5
+ def retry_on_rate_limit(try_until, &block)
6
+ internal_retry(block, try_until, 0)
7
+ end
8
+
9
+ private
10
+
11
+ def internal_retry(request_proc, try_until, retry_count)
12
+ response = request_proc.call
13
+
14
+ if failed_because_of_rate_limit?(response)
15
+ time_to_sleep = calculate_time_to_sleep(response['Retry-After'].to_f, retry_count)
16
+
17
+ if (Time.now + time_to_sleep.seconds) <= try_until
18
+ sleep time_to_sleep
19
+ internal_retry(request_proc, try_until, retry_count + 1)
20
+ else
21
+ raise Scrivito::RateLimitExceeded.new('rate limit exceeded', 429)
22
+ end
23
+ else
24
+ response
25
+ end
26
+ end
27
+
28
+
29
+ def calculate_time_to_sleep(retry_after, retry_count)
30
+ backoff_wait_time = 2 ** retry_count * 0.5
31
+ [backoff_wait_time, retry_after].max
32
+ end
33
+
34
+ def failed_because_of_rate_limit?(response)
35
+ response.code == '429'
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -89,18 +89,19 @@ class CmsRouting < Struct.new(:request, :main_app)
89
89
  LINK_TO_EMPTY_BLOB
90
90
  end
91
91
  else
92
- id_path_or_url_for_objs(obj, path_or_url, options)
92
+ slug = obj.slug.present? ? obj.slug.sub(/^\//, '') : nil
93
+ id_path_or_url_for_objs(obj, path_or_url, options.merge(slug: slug))
93
94
  end
94
95
  end
95
96
 
96
97
  def id_path_or_url_for_objs(obj, path_or_url, options)
97
- main_app.public_send(
98
- "cms_id_#{path_or_url}",
99
- options.merge(
100
- id: obj.id,
101
- slug: obj.slug.presence
102
- )
103
- )
98
+ method_name = if Scrivito::Configuration.legacy_routing
99
+ "cms_legacy_id_#{path_or_url}"
100
+ else
101
+ "cms_id_#{path_or_url}"
102
+ end
103
+
104
+ main_app.public_send(method_name, options.merge(id: obj.id))
104
105
  end
105
106
 
106
107
  def editor_authenticated?
@@ -176,7 +176,6 @@ module Scrivito
176
176
 
177
177
  def to_prepare
178
178
  unless Rails.configuration.cache_classes
179
- NamedLink.reset_cache
180
179
  BasicObj.reset_type_computer!
181
180
  BasicWidget.reset_type_computer!
182
181
  end
@@ -207,6 +206,7 @@ module Scrivito
207
206
  self.ca_file = DEFAULT_CA_FILE
208
207
  self.endpoint = 'api.scrivito.com'
209
208
  self.check_batch_size = 100
209
+ self.legacy_routing = false
210
210
  end
211
211
 
212
212
  #
@@ -225,6 +225,13 @@ module Scrivito
225
225
  #
226
226
  attr_accessor :ui_locale
227
227
 
228
+
229
+ # @api public
230
+ #
231
+ # Scrivito changed its routing to a slug first url scheme. This configuration option
232
+ # allows you to switch back to the old id first url scheme.
233
+ attr_accessor :legacy_routing
234
+
228
235
  attr_accessor :choose_homepage_callback, :check_batch_size
229
236
 
230
237
  # Configure a callback to be invoked when the Scrivito SDK delivers the homepage.