scrivito_sdk 0.60.0 → 0.65.0.rc1

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