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
@@ -14,16 +14,16 @@ class <%= class_name %> < ::Scrivito::Migration
14
14
  #
15
15
  # To get you started, here is a list of the most common SDK methods to alter the CMS content.
16
16
  #
17
- # @example Create a new obj class named "Homepage" of type "publication" that has a "string" attribute named "locale".
18
- # Scrivito::ObjClass.create(name: 'Homepage', type: :publication, attributes: [
17
+ # @example Create a new non-binary obj class named "Homepage" that has a "string" attribute named "locale".
18
+ # Scrivito::ObjClass.create(name: 'Homepage', is_binary: false, attributes: [
19
19
  # {name: 'locale', type: :string}
20
20
  # ])
21
21
  #
22
22
  # @example Find the obj class named "Homepage".
23
23
  # Scrivito::ObjClass.find('Homepage')
24
24
  #
25
- # @example Update the "title" of an obj class named "Homepage".
26
- # Scrivito::ObjClass.find('Homepage').update(title: 'Home')
25
+ # @example Update the "is_active" attribute of the obj class named "Homepage".
26
+ # Scrivito::ObjClass.find('Homepage').update(is_active: false)
27
27
  #
28
28
  # @example Add an "enum" attribute named "category" to the obj class named "Homepage".
29
29
  # Scrivito::ObjClass.find('Homepage').attributes.add(name: 'category', type: :enum, values: %w(tech, social))
@@ -54,14 +54,9 @@ module Scrivito
54
54
  # instance or an attribute property hash.
55
55
  # @return [Scrivito::AttributeCollection]
56
56
  def add(attribute)
57
- unless attribute.respond_to?(:to_cms_rest_api)
58
- attribute = Attribute.new(attribute)
59
- end
60
-
57
+ attribute = Attribute.new(attribute) unless attribute.respond_to?(:to_cms_rest_api)
61
58
  @attributes << attribute
62
-
63
59
  @obj_class.update(attributes: @attributes)
64
-
65
60
  self
66
61
  end
67
62
  alias_method :<<, :add
@@ -40,7 +40,8 @@ module AttributeContent
40
40
  end
41
41
 
42
42
  def has_custom_attribute?(name)
43
- data_from_cms.has_custom_attribute?(name.to_s)
43
+ name = name.to_s
44
+ name != 'blob' && data_from_cms.has_custom_attribute?(name)
44
45
  end
45
46
  alias_method :has_attribute?, :has_custom_attribute?
46
47
 
@@ -54,7 +55,12 @@ module AttributeContent
54
55
  # @api public
55
56
  def [](key)
56
57
  key = key.to_s
57
- has_attribute?(key) ? read_attribute(key) : nil
58
+
59
+ if key == '_obj_class'
60
+ obj_class
61
+ else
62
+ has_attribute?(key) ? read_attribute(key) : nil
63
+ end
58
64
  end
59
65
 
60
66
  # Hook method to control which widget classes should be available for this page.
@@ -85,8 +91,8 @@ module AttributeContent
85
91
  cms_data_in_revision = cms_data_for_revision(revision)
86
92
 
87
93
  if cms_data_in_revision
88
- other_value = cms_data_in_revision.unchecked_value_of(attribute_name.to_s)
89
- if data_from_cms.unchecked_value_of(attribute_name.to_s) == other_value
94
+ other_value = cms_data_in_revision.value_without_default_of(attribute_name.to_s)
95
+ if data_from_cms.value_without_default_of(attribute_name.to_s) == other_value
90
96
  Modification::UNMODIFIED
91
97
  else
92
98
  Modification::EDITED
@@ -102,6 +108,22 @@ module AttributeContent
102
108
  @attribute_cache = {}
103
109
  end
104
110
 
111
+ # Returns the obj class name of this object.
112
+ # @api public
113
+ # @return [String]
114
+ def obj_class_name
115
+ data_from_cms.value_of('_obj_class')
116
+ end
117
+
118
+ # Returns the obj class of this object.
119
+ # @api public
120
+ # @return [ObjClass]
121
+ def obj_class
122
+ if obj_class_data = CmsBackend.instance.find_obj_class_data_by_name(revision, obj_class_name)
123
+ ObjClass.new(obj_class_data, revision.workspace)
124
+ end
125
+ end
126
+
105
127
  private
106
128
 
107
129
  attr_writer :data_from_cms
@@ -140,7 +162,7 @@ module AttributeContent
140
162
  link_definitions = link_definitions.map(&:with_indifferent_access)
141
163
 
142
164
  object_ids = link_definitions.map { |link_data| link_data[:destination] }.compact.uniq
143
- objects = object_ids.empty? ? [] : Obj.find(object_ids)
165
+ objects = object_ids.empty? ? [] : BasicObj.find(object_ids)
144
166
  link_definitions.each_with_object([]) do |link_data, links|
145
167
  obj = objects.detect { |o| o && o.id == link_data[:destination] }
146
168
  link = Link.new(link_data.merge(obj: obj))
@@ -153,7 +175,7 @@ module AttributeContent
153
175
 
154
176
  def build_link(attribute_value)
155
177
  return unless attribute_value
156
-
178
+
157
179
  if attribute_value['destination']
158
180
  build_internal_link(attribute_value)
159
181
  else
@@ -163,7 +185,7 @@ module AttributeContent
163
185
 
164
186
  def build_internal_link(attribute_value)
165
187
  properties = {
166
- obj: Obj.find(attribute_value['destination']),
188
+ obj: BasicObj.find(attribute_value['destination']),
167
189
  title: attribute_value['title'],
168
190
  query: attribute_value['query'],
169
191
  fragment: attribute_value['fragment'],
@@ -6,6 +6,20 @@ module Scrivito
6
6
  # The CMS file class
7
7
  # @api public
8
8
  class BasicObj
9
+ UNIQ_ATTRIBUTES = %w[
10
+ _id
11
+ _path
12
+ _permalink
13
+ ].freeze
14
+
15
+ GENERATED_ATTRIBUTES = %w[
16
+ _conflicts
17
+ _last_changed
18
+ _modification
19
+ _obj_type
20
+ _text_links
21
+ ].freeze
22
+
9
23
  extend ActiveModel::Naming
10
24
 
11
25
  include AttributeContent
@@ -77,10 +91,9 @@ module Scrivito
77
91
  def self.create(attributes)
78
92
  attributes = with_default_obj_class(attributes)
79
93
  api_attributes, widget_properties = prepare_attributes_for_rest_api(attributes, nil)
80
- json = CmsRestApi.post(cms_rest_api_path, obj: api_attributes)
81
- obj = find(json['id'] || json['_id'])
94
+ json = Workspace.current.api_request(:post, '/objs', obj: api_attributes)
95
+ obj = find(json['_id'])
82
96
  CmsRestApi::WidgetExtractor.notify_persisted_widgets(obj, widget_properties)
83
- Workspace.current.reload
84
97
  obj
85
98
  end
86
99
 
@@ -263,23 +276,36 @@ module Scrivito
263
276
  # )
264
277
  def update(attributes)
265
278
  api_attributes, widget_properties = prepare_attributes_for_rest_api(attributes)
266
- CmsRestApi.put(cms_rest_api_path, obj: api_attributes)
267
- workspace.reload
268
- reload
279
+ workspace.api_request(:put, "/objs/#{id}", obj: api_attributes)
280
+ reload_data
269
281
  CmsRestApi::WidgetExtractor.notify_persisted_widgets(self, widget_properties)
270
282
  self
271
283
  end
272
284
 
285
+ # Copies itself to a given path.
286
+ # @api public
287
+ # @param [Hash] options
288
+ # @option options [String,Symbol] :_path (nil) the path of the copy.
289
+ # @option options [String,Symbol] :_id (nil) the id of the copy.
290
+ # @option options [String,Symbol] :_permalink (nil) the permalink of the copy.
291
+ # @raise [ArgumentError] if +options+ includes invalid keys.
292
+ # @return [Obj] the created copy
293
+ # @example Copy a blog post.
294
+ # blog_post = Obj.find_by_path('/blog/first_post')
295
+ # blog_post.copy(_path: '/blog/second_post')
296
+ def copy(options)
297
+ options = options.stringify_keys.assert_valid_keys('_path', '_id', '_permalink')
298
+ json = workspace.api_request(:post, '/objs', obj: copyable_attributes.merge(options))
299
+ self.class.find(json['_id'])
300
+ end
301
+
273
302
  # Destroys the {BasicObj Obj} in the current {Workspace}
274
303
  # @api public
275
304
  def destroy
276
305
  if children.any?
277
306
  raise ClientError.new(I18n.t('scrivito.errors.models.basic_obj.has_children'), 412)
278
307
  end
279
-
280
- CmsRestApi.delete(cms_rest_api_path)
281
-
282
- workspace.reload
308
+ workspace.api_request(:delete, "/objs/#{id}")
283
309
  end
284
310
 
285
311
  def to_param
@@ -486,26 +512,15 @@ module Scrivito
486
512
  # even if the obj_class in the database has changed.
487
513
  # @api public
488
514
  def reload
489
- id = self.id.to_s
490
-
491
- reload_data = Proc.new do
492
- CmsBackend.instance.find_obj_data_by(workspace.revision, :id, [id]).first.first
493
- end
494
-
495
- update_data(reload_data)
496
- end
497
-
498
- # @return [String]
499
- # @api public
500
- def obj_class_name
501
- read_attribute('_obj_class')
515
+ workspace.reload
516
+ reload_data
502
517
  end
503
518
 
504
- # Returns the {Scrivito::ObjClass} of this object.
505
- # @return [Scrivito::ObjClass]
506
- # @api public
507
- def obj_class
508
- ObjClass.find(obj_class_name)
519
+ def reload_data
520
+ id = self.id.to_s
521
+ update_data -> {
522
+ CmsBackend.instance.find_obj_data_by(workspace.revision, :id, [id]).first.first
523
+ }
509
524
  end
510
525
 
511
526
  # @api public
@@ -665,35 +680,23 @@ module Scrivito
665
680
  # This method does not work with +new+ or +deleted+ objects.
666
681
  # This method also does also not work for the +published+ workspace or the +rtc+ working copy.
667
682
  def revert
668
- workspace.assert_revertable
683
+ assert_revertable
669
684
 
670
- if binary?
671
- raise "revert not supported for binary objs"
672
- else
673
- case modification
674
- when Modification::UNMODIFIED
675
- # don't do anything
676
- when Modification::EDITED
677
- previous_content = CmsRestApi.get(
678
- "revisions/#{workspace.base_revision_id}/objs/#{id}")
679
- updated_content = previous_content.except('id', '_id')
680
-
681
- added_widget_ids = read_widget_pool.keys - previous_content['_widget_pool'].keys
682
- added_widget_ids.each do |added_widget_id|
683
- updated_content['_widget_pool'][added_widget_id] = nil
684
- end
685
+ if modification == Modification::EDITED
686
+ base_revision_path = "revisions/#{workspace.base_revision_id}/objs/#{id}"
687
+ previous_attributes = CmsRestApi.get(base_revision_path).except('_id')
688
+ previous_widget_pool = previous_attributes['_widget_pool']
685
689
 
686
- CmsRestApi.put(cms_rest_api_path, obj: updated_content)
690
+ ids_of_new_widgets = read_widget_pool.keys - previous_widget_pool.keys
691
+ ids_of_new_widgets.each { |widget_id| previous_widget_pool[widget_id] = nil }
687
692
 
688
- reload
689
- else
690
- raise ScrivitoError, "cannot revert changes, since obj is #{modification}."
691
- end
693
+ workspace.api_request(:put, "/objs/#{id}", obj: previous_attributes)
694
+ reload
692
695
  end
693
696
  end
694
697
 
695
698
  def mark_resolved
696
- CmsRestApi.put(cms_rest_api_path, obj: {_conflicts: nil})
699
+ workspace.api_request(:put, "/objs/#{id}", obj: {_conflicts: nil})
697
700
  reload
698
701
  end
699
702
 
@@ -718,6 +721,10 @@ module Scrivito
718
721
  read_attribute('_conflicts') != nil
719
722
  end
720
723
 
724
+ def publishable?
725
+ !has_conflict?
726
+ end
727
+
721
728
  def all_widgets_from_pool
722
729
  read_widget_pool.keys.map do |widget_id|
723
730
  widget_from_pool(widget_id)
@@ -734,6 +741,24 @@ module Scrivito
734
741
  raise ScrivitoError.new('Could not generate a new unused widget id')
735
742
  end
736
743
 
744
+ # Generates a +Hash+ containing all public attributes. The hash will _not_ include attributes,
745
+ # which are read-only or used for internal purpose.
746
+ # @api public
747
+ # @return [Hash] a hash containing all public attributes.
748
+ # @example
749
+ # old_obj = Obj.homepage
750
+ # attrs = old_obj.to_h
751
+ # puts attrs
752
+ # #=> {"_obj_class"=>"Publication", "_path"=>"/", "_id"=>"f64a68258ca15faf", "_widget_pool"=>{}}
753
+ # old_obj.destroy
754
+ #
755
+ # new_obj = Obj.create(attrs)
756
+ # puts new_obj == old_obj
757
+ # #=> true
758
+ def to_h
759
+ data_from_cms.to_h.except(*GENERATED_ATTRIBUTES)
760
+ end
761
+
737
762
  private
738
763
 
739
764
  def cms_data_for_revision(revision)
@@ -774,10 +799,6 @@ module Scrivito
774
799
  Blob.find(blob_spec["id"]) if blob_spec
775
800
  end
776
801
 
777
- def cms_rest_api_path(obj_id = id)
778
- "#{self.class.cms_rest_api_path(workspace)}/#{obj_id}"
779
- end
780
-
781
802
  def workspace
782
803
  if revision.workspace
783
804
  revision.workspace
@@ -794,17 +815,26 @@ module Scrivito
794
815
  self.class.prepare_attributes_for_rest_api(attributes, self)
795
816
  end
796
817
 
818
+ def copyable_attributes
819
+ workspace.api_request(:get, "/objs/#{id}")
820
+ .except(*GENERATED_ATTRIBUTES)
821
+ .except(*UNIQ_ATTRIBUTES)
822
+ end
823
+
824
+ def assert_revertable
825
+ workspace.assert_revertable
826
+ raise "revert not supported for binary objs" if binary?
827
+ if modification == Modification::NEW || modification == Modification::DELETED
828
+ raise ScrivitoError, "cannot revert changes, since obj is #{modification}."
829
+ end
830
+ end
831
+
797
832
  class << self
798
833
  def restore(obj_id)
799
834
  Workspace.current.assert_revertable
800
-
801
835
  base_revision_path = "revisions/#{Workspace.current.base_revision_id}/objs/#{obj_id}"
802
- obj_attributes = CmsRestApi.get(base_revision_path).except('id').merge('_id' => obj_id)
803
- CmsRestApi.post(cms_rest_api_path, obj: obj_attributes)
804
- end
805
-
806
- def cms_rest_api_path(workspace = Workspace.current)
807
- "workspaces/#{workspace.id}/objs"
836
+ obj_attributes = CmsRestApi.get(base_revision_path).merge('_id' => obj_id)
837
+ Workspace.current.api_request(:post, '/objs', obj: obj_attributes)
808
838
  end
809
839
 
810
840
  def prepare_attributes_for_rest_api(attributes, obj)
@@ -99,17 +99,6 @@ class BasicWidget
99
99
  @attributes_to_be_saved.nil?
100
100
  end
101
101
 
102
- def obj_class_name
103
- data_from_cms.value_of('_obj_class')
104
- end
105
-
106
- # Returns the {Scrivito::ObjClass} of this widget.
107
- # @return [Scrivito::ObjClass]
108
- # @api public
109
- def obj_class
110
- ObjClass.find(obj_class_name)
111
- end
112
-
113
102
  def ==(other)
114
103
  other.respond_to?(:obj) && obj == other.obj && other.respond_to?(:id) && id == other.id
115
104
  end
@@ -7,46 +7,33 @@ class ClientConfig < Struct.new(:obj, :editing_context, :lookup_context, :resour
7
7
 
8
8
  def to_json
9
9
  config = {}
10
- config[:editing_context] = editing_context_config
10
+ config[:editing_context] = editing_context_config
11
+ config[:i18n] = i18n_config
12
+ config[:obj] = obj_config
13
+ config[:resource_dialog] = resource_dialog_config
14
+ config[:user] = user_config
11
15
  config[:user_permissions] = user_permissions_config
12
- config[:i18n] = i18n_config
13
- config[:obj] = obj_config
14
- config[:resource_dialog] = resource_dialog_config
15
16
  config.to_json
16
17
  end
17
18
 
18
19
  private
19
20
 
20
- def user_permissions_config
21
- {
22
- publish_workspace: can_publish_workspace
23
- }
24
- end
25
-
26
21
  def editing_context_config
27
22
  {
28
23
  display_mode: editing_context.display_mode,
29
24
 
30
25
  selected_workspace: {
31
- id: editing_context.selected_workspace.id,
32
- title: editing_context.selected_workspace.title
26
+ id: selected_workspace.id,
27
+ title: selected_workspace.title
33
28
  },
34
29
 
35
30
  visible_workspace: {
36
- id: editing_context.visible_workspace.id,
37
- title: editing_context.visible_workspace.title
31
+ id: visible_workspace.id,
32
+ title: visible_workspace.title
38
33
  }
39
34
  }
40
35
  end
41
36
 
42
- def can_publish_workspace
43
- if editing_context.editor
44
- editing_context.editor.able_to?(:publish_workspace)
45
- else
46
- false
47
- end
48
- end
49
-
50
37
  def i18n_config
51
38
  {locale: I18n.locale}
52
39
  end
@@ -61,6 +48,7 @@ class ClientConfig < Struct.new(:obj, :editing_context, :lookup_context, :resour
61
48
  has_conflict: obj.has_conflict?,
62
49
  has_details_view: obj_has_details_view?,
63
50
  modification: modification(obj),
51
+ restriction_messages: editor.restriction_messages_for(obj),
64
52
  }
65
53
  }
66
54
  else
@@ -77,7 +65,7 @@ class ClientConfig < Struct.new(:obj, :editing_context, :lookup_context, :resour
77
65
  def resource_dialog_config
78
66
  if resource
79
67
  {
80
- obj: Configuration.obj_formats.fetch('_default').call(resource),
68
+ obj: Configuration.obj_formats.fetch('_default').call(resource, editor),
81
69
  return_to: return_to,
82
70
  }
83
71
  else
@@ -85,13 +73,40 @@ class ClientConfig < Struct.new(:obj, :editing_context, :lookup_context, :resour
85
73
  end
86
74
  end
87
75
 
76
+ def user_permissions_config
77
+ {
78
+ publish_workspace: editor.can?(:publish, selected_workspace)
79
+ }
80
+ end
81
+
82
+ def user_config
83
+ {
84
+ current: {
85
+ id: editor.id
86
+ }
87
+ }
88
+ end
89
+
88
90
  def modification(obj_or_resource)
89
- if editing_context.comparison.active?
90
- editing_context.comparison.modification(obj_or_resource)
91
+ comparison = editing_context.comparison
92
+ if comparison.active?
93
+ comparison.modification(obj_or_resource)
91
94
  else
92
95
  obj_or_resource.modification
93
96
  end
94
97
  end
98
+
99
+ def editor
100
+ editing_context.editor
101
+ end
102
+
103
+ def selected_workspace
104
+ editing_context.selected_workspace
105
+ end
106
+
107
+ def visible_workspace
108
+ editing_context.visible_workspace
109
+ end
95
110
  end
96
111
 
97
112
  end