scrivito_sdk 0.17.0 → 0.18.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/app/controllers/scrivito/default_cms_controller.rb +15 -3
  4. data/app/controllers/scrivito/objs_controller.rb +61 -25
  5. data/app/controllers/scrivito/users_controller.rb +7 -0
  6. data/app/controllers/scrivito/webservice_controller.rb +17 -7
  7. data/app/controllers/scrivito/workspaces_controller.rb +85 -17
  8. data/app/helpers/scrivito/default_cms_routing_helper.rb +3 -3
  9. data/config/routes.rb +4 -1
  10. data/lib/assets/javascripts/scrivito_editing.js +4182 -508
  11. data/lib/assets/stylesheets/scrivito_editing.css +489 -13
  12. data/lib/generators/cms/migration/templates/migration.erb +4 -4
  13. data/lib/scrivito/attribute_collection.rb +1 -6
  14. data/lib/scrivito/attribute_content.rb +29 -7
  15. data/lib/scrivito/basic_obj.rb +91 -61
  16. data/lib/scrivito/basic_widget.rb +0 -11
  17. data/lib/scrivito/client_config.rb +40 -25
  18. data/lib/scrivito/cms_backend.rb +54 -0
  19. data/lib/scrivito/cms_cache_storage.rb +8 -0
  20. data/lib/scrivito/cms_field_tag.rb +2 -1
  21. data/lib/scrivito/cms_rest_api.rb +9 -0
  22. data/lib/scrivito/configuration.rb +4 -2
  23. data/lib/scrivito/content_state.rb +8 -0
  24. data/lib/scrivito/content_state_caching.rb +20 -0
  25. data/lib/scrivito/editing_context.rb +35 -34
  26. data/lib/scrivito/membership.rb +22 -3
  27. data/lib/scrivito/memberships_collection.rb +8 -4
  28. data/lib/scrivito/obj_class.rb +45 -100
  29. data/lib/scrivito/obj_class_collection.rb +53 -0
  30. data/lib/scrivito/obj_class_data.rb +33 -0
  31. data/lib/scrivito/obj_data.rb +26 -48
  32. data/lib/scrivito/obj_data_from_hash.rb +5 -5
  33. data/lib/scrivito/obj_data_from_service.rb +9 -3
  34. data/lib/scrivito/obj_search_builder.rb +0 -5
  35. data/lib/scrivito/obj_search_enumerator.rb +3 -20
  36. data/lib/scrivito/objs_collection.rb +7 -0
  37. data/lib/scrivito/restriction_set.rb +2 -2
  38. data/lib/scrivito/user.rb +89 -23
  39. data/lib/scrivito/user_definition.rb +73 -70
  40. data/lib/scrivito/workspace.rb +52 -8
  41. data/lib/scrivito/workspace/publish_checker.rb +126 -0
  42. metadata +6 -2
@@ -0,0 +1,53 @@
1
+ module Scrivito
2
+ # This class allows you to retrieve obj classes from a specific working copy. It behaves almost
3
+ # exactly as an Array, so methods like +#each+, +#select+ etc. are available. You can get an
4
+ # instance by accessing {Workspace#obj_classes}.
5
+ #
6
+ # @api public
7
+ class ObjClassCollection
8
+ include Enumerable
9
+
10
+ # Initializes an obj class collection for a workspace.
11
+ #
12
+ # @param [Workspace] workspace
13
+ # @return [ObjClassCollection]
14
+ def initialize(workspace)
15
+ @workspace = workspace
16
+ end
17
+
18
+ # Finds an obj class by its name in the working copy of the collection.
19
+ #
20
+ # @api public
21
+ #
22
+ # @example Find the obj class named "Homepage" in the "rtc" {Workspace}.
23
+ # Workspace.find('rtc').obj_classes['Homepage']
24
+ #
25
+ # @param [String] name The name of the obj class.
26
+ # @return [ObjClass, nil] Returns the obj class or nil when no obj class with the given +name+ can be found in the working copy.
27
+ def [](name)
28
+ if obj_class_data = CmsBackend.instance.find_obj_class_data_by_name(workspace.revision, name)
29
+ ObjClass.new(obj_class_data, workspace)
30
+ end
31
+ end
32
+
33
+ # @!method each
34
+ # Yields successive obj classes of the collection. Implements the +Enumerable+ interface.
35
+ # @api public
36
+ # @yield [ObjClass] Successive obj classes of the collection.
37
+ # @example Find all obj classes in the "rtc" {Workspace} and print their name.
38
+ # Workspace.find('rtc').obj_classes.each do |obj_class|
39
+ # puts obj_class.name
40
+ # end
41
+ delegate :each, to: :obj_classes
42
+
43
+ private
44
+
45
+ def obj_classes
46
+ CmsBackend.instance.find_all_obj_class_data(workspace.revision).map do |obj_class_data|
47
+ ObjClass.new(obj_class_data, workspace)
48
+ end
49
+ end
50
+
51
+ attr_reader :workspace
52
+ end
53
+ end
@@ -0,0 +1,33 @@
1
+ module Scrivito
2
+
3
+ class ObjClassData
4
+ def initialize(raw_data)
5
+ raw_data = raw_data.deep_stringify_keys
6
+ if type = raw_data['type']
7
+ raw_data['is_binary'] = %w[image generic].include?(type)
8
+ end
9
+ @raw_data = raw_data
10
+ end
11
+
12
+ def id
13
+ @raw_data['id']
14
+ end
15
+
16
+ def name
17
+ @raw_data['name']
18
+ end
19
+
20
+ def is_active
21
+ !!@raw_data['is_active']
22
+ end
23
+
24
+ def is_binary
25
+ !!@raw_data['is_binary']
26
+ end
27
+
28
+ def attributes
29
+ @raw_data['attributes'] || []
30
+ end
31
+ end
32
+
33
+ end
@@ -1,8 +1,6 @@
1
1
  module Scrivito
2
2
 
3
3
  class ObjData
4
- MissingAttribute = Class.new
5
-
6
4
  internal_key_list = %w[
7
5
  last_changed
8
6
  modification
@@ -21,8 +19,6 @@ module Scrivito
21
19
 
22
20
  INTERNAL_COMPARISON_KEYS = Set.new(internal_comparison_key_list.map { |name| "_#{name}" })
23
21
 
24
- SPECIAL_KEYS = Set.new(%w[ body title blob ])
25
-
26
22
  ATTRIBUTE_DEFAULT_VALUES = {
27
23
  "string" => "",
28
24
  "text" => "",
@@ -46,27 +42,23 @@ module Scrivito
46
42
  value_and_type_of(attribute_name).second
47
43
  end
48
44
 
49
- def unchecked_value_of(attribute_name)
50
- unchecked_value_and_type_of(attribute_name).first
45
+ def value_without_default_of(attribute_name)
46
+ value_and_type_without_default_of(attribute_name).first
51
47
  end
52
48
 
53
49
  def value_and_type_of(attribute_name)
54
- value_and_type = internal_value_and_type_of(attribute_name)
55
-
56
- if value_and_type.first.is_a?(MissingAttribute)
57
- raise ScrivitoError.new("Illegal attribute name #{attribute_name}")
50
+ if internal_attribute?(attribute_name)
51
+ internal_value_and_type(attribute_name)
58
52
  else
59
- value_and_type
53
+ custom_value_and_type_with_default(attribute_name)
60
54
  end
61
55
  end
62
56
 
63
- def unchecked_value_and_type_of(attribute_name)
64
- value_and_type = internal_value_and_type_of(attribute_name)
65
-
66
- if value_and_type.first.is_a?(MissingAttribute)
67
- [nil, nil]
57
+ def value_and_type_without_default_of(attribute_name)
58
+ if internal_attribute?(attribute_name)
59
+ internal_value_and_type(attribute_name)
68
60
  else
69
- value_and_type
61
+ raw_value_and_type_of(attribute_name)
70
62
  end
71
63
  end
72
64
 
@@ -76,6 +68,10 @@ module Scrivito
76
68
  end
77
69
  end
78
70
 
71
+ def to_h
72
+ raise NotImplementedError, 'implement in subclass'
73
+ end
74
+
79
75
  def raw_value_and_type_of(attribute_name)
80
76
  raise NotImplementedError, "implement in subclass"
81
77
  end
@@ -95,7 +91,7 @@ module Scrivito
95
91
 
96
92
  next if attr.start_with?('_') && !INTERNAL_COMPARISON_KEYS.include?(attr)
97
93
 
98
- if unchecked_value_of(attr) != other.unchecked_value_of(attr)
94
+ if value_without_default_of(attr) != other.value_without_default_of(attr)
99
95
  return false
100
96
  end
101
97
  end
@@ -108,24 +104,20 @@ module Scrivito
108
104
 
109
105
  private
110
106
 
111
- def internal_value_and_type_of(attribute_name)
112
- value_and_type = raw_value_and_type_of(attribute_name)
107
+ def custom_value_and_type_with_default(attribute_name)
108
+ value, type = raw_value_and_type_of(attribute_name)
109
+ value = ATTRIBUTE_DEFAULT_VALUES[type] if value.nil?
110
+ [value, type]
111
+ end
113
112
 
114
- if value_and_type.blank?
115
- if INTERNAL_KEYS.include?(attribute_name) || SPECIAL_KEYS.include?(attribute_name)
116
- type = type_of_internal(attribute_name)
117
- [default_attribute_value(attribute_name), type]
118
- else
119
- [ObjData::MissingAttribute.new, nil]
120
- end
121
- else
122
- value, type = value_and_type
123
- if value.nil? && has_custom_attribute?(attribute_name)
124
- value = ATTRIBUTE_DEFAULT_VALUES[type]
125
- end
113
+ def internal_value_and_type(attribute_name)
114
+ value, type = raw_value_and_type_of(attribute_name)
115
+ type = type_of_internal(attribute_name)
116
+ [value, type]
117
+ end
126
118
 
127
- [value, type]
128
- end
119
+ def internal_attribute?(attribute_name)
120
+ attribute_name.starts_with?('_')
129
121
  end
130
122
 
131
123
  def type_of_internal(key)
@@ -136,24 +128,10 @@ module Scrivito
136
128
  "linklist"
137
129
  when "_last_changed"
138
130
  "date"
139
- when "title", "body"
140
- "html"
141
- when "blob"
142
- "binary"
143
131
  else
144
132
  nil
145
133
  end
146
134
  end
147
-
148
- def default_attribute_value(attribute_name)
149
- case attribute_name
150
- when "_text_links"
151
- []
152
- else
153
- nil
154
- end
155
- end
156
-
157
135
  end
158
136
 
159
137
  end
@@ -7,9 +7,7 @@ module Scrivito
7
7
  end
8
8
 
9
9
  def raw_value_and_type_of(attribute_name)
10
- if has_custom_attribute?(attribute_name)
11
- [@hash[attribute_name], @type_hash[attribute_name]]
12
- end
10
+ [@hash[attribute_name], @type_hash[attribute_name]]
13
11
  end
14
12
 
15
13
  def has_custom_attribute?(name)
@@ -20,12 +18,14 @@ module Scrivito
20
18
  @all_attributes ||= begin
21
19
  @hash.keys |
22
20
  INTERNAL_KEYS.to_a |
23
- SPECIAL_KEYS.to_a |
24
21
  %w[_id]
25
22
  end
26
23
  end
27
24
 
25
+ def to_h
26
+ @hash
27
+ end
28
+
28
29
  end
29
30
 
30
31
  end
31
-
@@ -9,7 +9,7 @@ module Scrivito
9
9
  if attribute_name == '_widget_pool'
10
10
  [value_of_widget_pool, nil]
11
11
  else
12
- type_and_value = @data[attribute_name]
12
+ type_and_value = @data.fetch(attribute_name, [nil, nil])
13
13
 
14
14
  if type_and_value.present?
15
15
  type = if type_and_value.length == 1
@@ -28,7 +28,13 @@ module Scrivito
28
28
  end
29
29
 
30
30
  def all_attributes
31
- @all_attributes ||= (@data.keys | INTERNAL_KEYS.to_a | SPECIAL_KEYS.to_a)
31
+ @all_attributes ||= (@data.keys | INTERNAL_KEYS.to_a)
32
+ end
33
+
34
+ def to_h
35
+ @data.dup.tap do |h|
36
+ h.each_pair { |k, v| h[k] = v.first }
37
+ end
32
38
  end
33
39
 
34
40
  private
@@ -76,7 +82,7 @@ module Scrivito
76
82
  end
77
83
 
78
84
  def is_custom_attribute?(attribute_name)
79
- !attribute_name.starts_with?('_') && !SPECIAL_KEYS.include?(attribute_name)
85
+ !internal_attribute?(attribute_name)
80
86
  end
81
87
 
82
88
  end
@@ -9,7 +9,6 @@ class ObjSearchBuilder < Struct.new(:query)
9
9
  set_offset
10
10
  set_order
11
11
  set_batch_size
12
- set_format
13
12
  set_include_deleted
14
13
 
15
14
  enumerator
@@ -48,10 +47,6 @@ class ObjSearchBuilder < Struct.new(:query)
48
47
  enumerator.batch_size(query[:batch_size]) if query[:batch_size]
49
48
  end
50
49
 
51
- def set_format
52
- enumerator.format(query[:format]) if query[:format]
53
- end
54
-
55
50
  def set_include_deleted
56
51
  enumerator.include_deleted if query[:include_deleted]
57
52
  end
@@ -106,8 +106,6 @@ module Scrivito
106
106
  #
107
107
  # @api public
108
108
  class ObjSearchEnumerator
109
- class UnregisteredObjFormat < StandardError; end
110
-
111
109
  include Enumerable
112
110
 
113
111
  attr_reader :workspace
@@ -271,7 +269,7 @@ module Scrivito
271
269
  }
272
270
  end
273
271
 
274
- @size ||= CmsRestApi.get(resource_path, size_query)['total'].to_i
272
+ @size ||= CmsBackend.instance.search_objs(workspace, size_query)['total'].to_i
275
273
  end
276
274
 
277
275
  # load a single batch of search results from the backend.
@@ -282,18 +280,7 @@ module Scrivito
282
280
  def load_batch
283
281
  next_batch = fetch_next_batch(options[:offset] || 0)
284
282
 
285
- formatter = @formatter || -> obj { obj.id }
286
- next_batch.first.map { |obj| formatter.call(obj) }
287
- end
288
-
289
- def format(name)
290
- @formatter = Configuration.obj_formats[name]
291
-
292
- unless @formatter
293
- raise UnregisteredObjFormat, "The format with name '#{name}' is not registered."
294
- end
295
-
296
- self
283
+ next_batch.first
297
284
  end
298
285
 
299
286
  # @api public
@@ -342,7 +329,7 @@ module Scrivito
342
329
  end
343
330
 
344
331
  def fetch_next_batch(offset)
345
- request_result = CmsRestApi.get(resource_path, search_dsl(offset))
332
+ request_result = CmsBackend.instance.search_objs(workspace, search_dsl(offset))
346
333
 
347
334
  obj_ids = request_result['results'].map { |result| result['id'] || result['_id'] }
348
335
  objs = workspace.objs.find_including_deleted(obj_ids)
@@ -352,10 +339,6 @@ module Scrivito
352
339
  [objs, @size]
353
340
  end
354
341
 
355
- def resource_path
356
- "workspaces/#{workspace.id}/objs/search"
357
- end
358
-
359
342
  def search_dsl(offset)
360
343
  patches = {
361
344
  offset: offset,
@@ -89,6 +89,13 @@ module Scrivito
89
89
  find_by(:path, paths).map(&:first)
90
90
  end
91
91
 
92
+ def changes(offset, batch_size)
93
+ where('_modification', 'equals', ['new', 'edited', 'deleted'])
94
+ .batch_size(batch_size)
95
+ .offset(offset)
96
+ .include_deleted
97
+ end
98
+
92
99
  private
93
100
 
94
101
  def find_filtering_deleted(id_or_list, include_deleted)
@@ -20,8 +20,8 @@ module Scrivito
20
20
 
21
21
  def add(options, &block)
22
22
  options = options.with_indifferent_access
23
- attribute = options.fetch(:uses) do
24
- raise ScrivitoError, 'No "uses" option given when adding a publish restriction'
23
+ attribute = options.fetch(:using) do
24
+ raise ScrivitoError, 'No "using" option given when adding a publish restriction'
25
25
  end
26
26
 
27
27
  unless block_given?
data/lib/scrivito/user.rb CHANGED
@@ -1,40 +1,66 @@
1
1
  module Scrivito
2
- # @api beta
3
- class User < Struct.new(:id, :abilities, :description_proc, :suggest_users_proc, :restriction_set)
2
+ # @api public
3
+ class User
4
+ # Valid action verbs for the explicit rules.
5
+ # @api public
6
+ VERBS = [
7
+ :create,
8
+ :delete,
9
+ :invite_to,
10
+ :publish,
11
+ :read,
12
+ :write,
13
+ ].freeze
14
+
4
15
  class << self
5
16
  # Defines a new user.
6
- # @api beta
17
+ # @api public
7
18
  # @param [String] id the unique, unalterable id of the user.
8
19
  # The user id is used to associate the user with the corresponding CMS resources.
9
20
  # It will be persisted in the CMS.
10
21
  # @raise [Scrivito::ScrivitoError] if id is blank
11
22
  # @raise [Scrivito::ScrivitoError] if id is more than 64 characters long
12
- # @yieldparam [Scrivito::UserDefinition] user object to define abilities on
13
- # @see Scrivito::UserDefinition#can
23
+ # @yieldparam [Scrivito::UserDefinition] user object to define rules on
24
+ # @see Scrivito::UserDefinition#can_always
25
+ # @see Scrivito::UserDefinition#can_never
14
26
  # @see Scrivito::UserDefinition#description
27
+ # @see Scrivito::UserDefinition#restrict_obj_publish
28
+ # @see Scrivito::UserDefinition#suggest_users
15
29
  # @example
16
30
  # Scrivito::User.define('alice') do |user|
17
- # user.can(:publish_workspace) { true }
31
+ # user.description { 'Alice Almighty' }
32
+ # user.can_always(:read, :workspace)
33
+ # user.can_always(:write, :workspace)
34
+ # user.can_always(:publish, :workspace, 'You can always publish workspaces.')
18
35
  # end
19
36
  #
20
37
  # Scrivito::User.define('bob') do |user|
21
38
  # user.description { 'Bob Doe' }
22
- # user.can(:publish_workspace) { true }
39
+ # user.can_never(:create, :workspace, 'You are not allowed to create workspaces.')
40
+ # user.can_always(:read, :workspace)
41
+ # user.restrict_obj_publish(using: :_obj_class) do |obj_class|
42
+ # if obj_class.name == 'BlogPost'
43
+ # false
44
+ # else
45
+ # 'You are not allowed to publish blog posts.'
46
+ # end
47
+ # end
23
48
  # end
24
- def define(id, &block)
49
+ def define(id)
25
50
  assert_valid_id(id)
26
51
  user_definition = UserDefinition.new(id)
27
- yield user_definition
52
+ yield user_definition if block_given?
28
53
  user_definition.user
29
54
  end
30
55
 
31
56
  def anonymous_admin
32
- User.new(
33
- id: nil,
34
- abilities: Hash.new(-> {true}).with_indifferent_access,
35
- description_proc: nil,
36
- suggest_users_proc: nil
37
- )
57
+ explicit_rules = {}
58
+ VERBS.each { |verb| explicit_rules[[:can_always, verb, :workspace]] = nil }
59
+ new(id: nil, explicit_rules: explicit_rules)
60
+ end
61
+
62
+ def unknown_user(id)
63
+ new(id: id, explicit_rules: {})
38
64
  end
39
65
 
40
66
  def find(id)
@@ -59,18 +85,41 @@ module Scrivito
59
85
  end
60
86
  end
61
87
 
88
+ attr_reader :id, :explicit_rules, :description_proc, :suggest_users_proc, :restriction_set
89
+
62
90
  def initialize(options)
63
- super(*options.values_at(:id, :abilities, :description_proc,
64
- :suggest_users_proc, :restriction_set))
91
+ @id = options[:id]
92
+ @explicit_rules = options[:explicit_rules]
93
+ @description_proc = options[:description_proc]
94
+ @suggest_users_proc = options[:suggest_users_proc]
95
+ @restriction_set = options[:restriction_set]
96
+
97
+ @explicit_rules.each_key { |rule| assert_valid_verb(rule.second) }
98
+ end
99
+
100
+ def can?(verb, workspace)
101
+ assert_valid_verb(verb)
102
+ can_always?(verb, :workspace) || owner_of?(workspace) && !can_never?(verb, :workspace)
103
+ end
104
+
105
+ def can_always?(verb, subject)
106
+ assert_valid_verb(verb)
107
+ @explicit_rules.has_key?([:can_always, verb, subject])
65
108
  end
66
109
 
67
- def able_to?(ability_name)
68
- !!abilities[ability_name].call
110
+ def can_never?(verb, subject)
111
+ assert_valid_verb(verb)
112
+ @explicit_rules.has_key?([:can_never, verb, subject])
113
+ end
114
+
115
+ def owner_of?(workspace)
116
+ membership = workspace.memberships[self]
117
+ membership ? membership.role == 'owner' : false
69
118
  end
70
119
 
71
120
  # Verfies if the User is able to publish changes to a certain {BasicObj Obj}
72
121
  #
73
- # @api beta
122
+ # @api public
74
123
  # @param [BasicObj] obj the obj that should be published
75
124
  # @return [Boolean] true if the user is allowed to publish otherwise false
76
125
  def can_publish?(obj)
@@ -81,11 +130,11 @@ module Scrivito
81
130
  # specified in a {UserDefinition#restrict_obj_publish} callback if they are not
82
131
  # If the user can publish the obj an empty array is returned
83
132
  #
84
- # @api beta
133
+ # @api public
85
134
  # @param [BasicObj] obj the obj that should be published
86
135
  # @return [Array<String>] Hints why the user can't publish
87
136
  def restriction_messages_for(obj)
88
- return [] if able_to?(UserDefinition::ADMINISTRATE_CMS_ABILITY)
137
+ return [] if can_always?(:publish, :workspace)
89
138
 
90
139
  if obj.modification == Modification::EDITED
91
140
  base_revision_obj = obj.in_revision(obj.revision.workspace.base_revision)
@@ -102,7 +151,20 @@ module Scrivito
102
151
  end
103
152
 
104
153
  def suggest_users(input)
105
- suggest_users_proc ? suggest_users_proc.call(input) : []
154
+ if suggest_users_proc
155
+ suggested_users = suggest_users_proc.call(input)
156
+ suggested_users.nil? ? [] : suggested_users
157
+ else
158
+ user = self.class.find(input)
159
+ user ? [user] : []
160
+ end
161
+ end
162
+
163
+ def as_json(options = nil)
164
+ {
165
+ id: id,
166
+ description: description,
167
+ }
106
168
  end
107
169
 
108
170
  private
@@ -110,5 +172,9 @@ module Scrivito
110
172
  def calculate_description
111
173
  description_proc ? description_proc.call : id
112
174
  end
175
+
176
+ def assert_valid_verb(verb)
177
+ raise ScrivitoError.new("Invalid verb '#{verb}'") unless VERBS.include?(verb)
178
+ end
113
179
  end
114
180
  end