scrivito_sdk 0.17.0 → 0.18.0

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