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
@@ -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