scrivito_sdk 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +5 -0
  3. data/README +6 -0
  4. data/app/controllers/cms_controller.rb +7 -0
  5. data/app/controllers/scrivito/blobs_controller.rb +10 -0
  6. data/app/controllers/scrivito/default_cms_controller.rb +61 -0
  7. data/app/controllers/scrivito/objs_controller.rb +200 -0
  8. data/app/controllers/scrivito/tasks_controller.rb +11 -0
  9. data/app/controllers/scrivito/webservice_controller.rb +36 -0
  10. data/app/controllers/scrivito/workspaces_controller.rb +41 -0
  11. data/app/helpers/cms_helper.rb +7 -0
  12. data/app/helpers/cms_routing_helper.rb +7 -0
  13. data/app/helpers/scrivito/cms_asset_helper.rb +103 -0
  14. data/app/helpers/scrivito/cms_tag_helper.rb +231 -0
  15. data/app/helpers/scrivito/default_cms_helper.rb +21 -0
  16. data/app/helpers/scrivito/default_cms_routing_helper.rb +130 -0
  17. data/app/helpers/scrivito/display_helper.rb +71 -0
  18. data/app/helpers/scrivito/editing_helper.rb +26 -0
  19. data/app/helpers/scrivito/layout_helper.rb +28 -0
  20. data/app/models/named_link.rb +2 -0
  21. data/app/views/cms/_index.html.erb +7 -0
  22. data/app/views/cms/index.html.erb +1 -0
  23. data/app/views/scrivito/_editing_javascript.html.erb +7 -0
  24. data/app/views/scrivito/default_cms/show_widget.html.erb +1 -0
  25. data/app/views/scrivito/objs/copy_widget.html.erb +1 -0
  26. data/app/views/scrivito/objs/create_widget.html.erb +1 -0
  27. data/app/views/scrivito/widget_thumbnail.html.erb +9 -0
  28. data/config/ca-bundle.crt +3509 -0
  29. data/config/cms_routes.rb +17 -0
  30. data/config/locales/de.scrivito.errors.yml +7 -0
  31. data/config/locales/de.scrivito.lib.yml +6 -0
  32. data/config/locales/de.scrivito.models.yml +6 -0
  33. data/config/locales/en.scrivito.errors.yml +7 -0
  34. data/config/locales/en.scrivito.lib.yml +6 -0
  35. data/config/locales/en.scrivito.models.yml +6 -0
  36. data/config/routes.rb +37 -0
  37. data/lib/assets/images/180x120.gif +0 -0
  38. data/lib/assets/images/scrivito/image_placeholder.png +0 -0
  39. data/lib/assets/javascripts/scrivito_editing.js +14642 -0
  40. data/lib/assets/stylesheets/scrivito.css +180 -0
  41. data/lib/assets/stylesheets/scrivito_editing.css +2213 -0
  42. data/lib/generators/cms/migration/USAGE +9 -0
  43. data/lib/generators/cms/migration/migration_generator.rb +21 -0
  44. data/lib/generators/cms/migration/templates/migration.erb +10 -0
  45. data/lib/obj.rb +3 -0
  46. data/lib/scrivito/access_denied.rb +6 -0
  47. data/lib/scrivito/attribute_content.rb +194 -0
  48. data/lib/scrivito/backend_error.rb +4 -0
  49. data/lib/scrivito/basic_obj.rb +840 -0
  50. data/lib/scrivito/basic_widget.rb +238 -0
  51. data/lib/scrivito/blob.rb +48 -0
  52. data/lib/scrivito/cache.rb +41 -0
  53. data/lib/scrivito/cache_garbage_collector.rb +83 -0
  54. data/lib/scrivito/cache_middleware.rb +17 -0
  55. data/lib/scrivito/client_config.rb +62 -0
  56. data/lib/scrivito/client_error.rb +12 -0
  57. data/lib/scrivito/cms_accessible.rb +30 -0
  58. data/lib/scrivito/cms_backend.rb +238 -0
  59. data/lib/scrivito/cms_cache_storage.rb +51 -0
  60. data/lib/scrivito/cms_dispatch_controller.rb +46 -0
  61. data/lib/scrivito/cms_env.rb +63 -0
  62. data/lib/scrivito/cms_field_tag.rb +112 -0
  63. data/lib/scrivito/cms_rest_api.rb +151 -0
  64. data/lib/scrivito/cms_rest_api/attribute_serializer.rb +98 -0
  65. data/lib/scrivito/cms_rest_api/blob_uploader.rb +18 -0
  66. data/lib/scrivito/cms_rest_api/widget_extractor.rb +42 -0
  67. data/lib/scrivito/cms_test_request.rb +23 -0
  68. data/lib/scrivito/communication_error.rb +17 -0
  69. data/lib/scrivito/comparison.rb +67 -0
  70. data/lib/scrivito/configuration.rb +221 -0
  71. data/lib/scrivito/connection_manager.rb +100 -0
  72. data/lib/scrivito/content_conversion.rb +43 -0
  73. data/lib/scrivito/content_service.rb +118 -0
  74. data/lib/scrivito/content_state.rb +109 -0
  75. data/lib/scrivito/content_state_caching.rb +47 -0
  76. data/lib/scrivito/content_state_visitor.rb +19 -0
  77. data/lib/scrivito/controller_runtime.rb +35 -0
  78. data/lib/scrivito/date_attribute.rb +16 -0
  79. data/lib/scrivito/deprecation.rb +21 -0
  80. data/lib/scrivito/diff.rb +110 -0
  81. data/lib/scrivito/editing_context.rb +106 -0
  82. data/lib/scrivito/editing_context_middleware.rb +60 -0
  83. data/lib/scrivito/engine.rb +65 -0
  84. data/lib/scrivito/errors.rb +11 -0
  85. data/lib/scrivito/html_string.rb +18 -0
  86. data/lib/scrivito/link.rb +187 -0
  87. data/lib/scrivito/link_parser.rb +81 -0
  88. data/lib/scrivito/log_subscriber.rb +29 -0
  89. data/lib/scrivito/migration.rb +2 -0
  90. data/lib/scrivito/migrations.rb +12 -0
  91. data/lib/scrivito/migrations/cms_backend.rb +94 -0
  92. data/lib/scrivito/migrations/installer.rb +45 -0
  93. data/lib/scrivito/migrations/migration.rb +93 -0
  94. data/lib/scrivito/migrations/migration_dsl.rb +143 -0
  95. data/lib/scrivito/migrations/migration_store.rb +23 -0
  96. data/lib/scrivito/migrations/migrator.rb +135 -0
  97. data/lib/scrivito/migrations/workspace_lock.rb +39 -0
  98. data/lib/scrivito/model_identity.rb +13 -0
  99. data/lib/scrivito/modification.rb +8 -0
  100. data/lib/scrivito/named_link.rb +75 -0
  101. data/lib/scrivito/network_error.rb +11 -0
  102. data/lib/scrivito/obj_data.rb +140 -0
  103. data/lib/scrivito/obj_data_from_hash.rb +31 -0
  104. data/lib/scrivito/obj_data_from_service.rb +84 -0
  105. data/lib/scrivito/obj_params_parser.rb +61 -0
  106. data/lib/scrivito/obj_search_builder.rb +62 -0
  107. data/lib/scrivito/obj_search_enumerator.rb +374 -0
  108. data/lib/scrivito/rate_limit_exceeded.rb +5 -0
  109. data/lib/scrivito/revision.rb +9 -0
  110. data/lib/scrivito/string_tagging.rb +18 -0
  111. data/lib/scrivito/text_link.rb +52 -0
  112. data/lib/scrivito/text_link_conversion.rb +52 -0
  113. data/lib/scrivito/type_computer.rb +34 -0
  114. data/lib/scrivito/widget_field_params.rb +61 -0
  115. data/lib/scrivito/widget_garbage_collection.rb +97 -0
  116. data/lib/scrivito/workspace.rb +222 -0
  117. data/lib/scrivito/workspace_data_from_service.rb +80 -0
  118. data/lib/scrivito/workspace_selection_middleware.rb +23 -0
  119. data/lib/scrivito_sdk.rb +19 -0
  120. data/lib/tasks/cache.rake +12 -0
  121. data/lib/tasks/migration.rake +35 -0
  122. data/lib/widget.rb +3 -0
  123. metadata +291 -0
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Creates a new CMS migration.
3
+
4
+ Example:
5
+ rails generate cms:migration CreateExample
6
+ rails generate cms:migration create_example
7
+
8
+ This will create:
9
+ cms/migrate/[timestamp]_create_example.rb
@@ -0,0 +1,21 @@
1
+ module Cms
2
+ class MigrationGenerator < ::Rails::Generators::NamedBase
3
+ include ::Rails::Generators::Migration
4
+
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ class_option :path,
8
+ type: :string,
9
+ default: 'cms/migrate',
10
+ desc: 'Relative path to Rails.root where to place the migration file. Defaults to "cms/migrate".',
11
+ banner: 'PATH'
12
+
13
+ def self.next_migration_number(dirname)
14
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
15
+ end
16
+
17
+ def create_migration_file
18
+ migration_template('migration.erb', "#{options[:path]}/#{file_name}.rb")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ class <%= class_name %> < ::Scrivito::Migration
2
+ def up
3
+ # get_obj_class('Test')
4
+ # create_obj_class(name: 'Test', type: :publication, attributes: [])
5
+ # update_obj_class('Test', title: 'Test Title')
6
+ # add_attribute_to('Test', { name: 'test', type: 'string' })
7
+ # update_attribute_for('Test', 'test', { title: 'New Title' })
8
+ # delete_attribute_from('Test', 'test')
9
+ end
10
+ end
data/lib/obj.rb ADDED
@@ -0,0 +1,3 @@
1
+ # empty implementation for Obj
2
+ # only unsed in case the app does not define Obj itself
3
+ ::Obj = Class.new(Scrivito::BasicObj)
@@ -0,0 +1,6 @@
1
+ module Scrivito
2
+
3
+ class AccessDenied < StandardError
4
+ end
5
+
6
+ end
@@ -0,0 +1,194 @@
1
+ module Scrivito
2
+
3
+ module AttributeContent
4
+ extend ActiveSupport::Concern
5
+
6
+ def respond_to?(method_id, include_private=false)
7
+ if has_attribute?(method_id)
8
+ true
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ def method_missing(method_name, *args)
15
+ if has_attribute?(method_name)
16
+ read_attribute(method_name.to_s)
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def referenced_widgets
23
+ data_from_cms.all_custom_attributes.
24
+ select { |attr| type_of_attribute(attr) == "widget" }.
25
+ map { |attr| read_attribute(attr) }.
26
+ flatten
27
+ end
28
+
29
+ def contained_widgets
30
+ referenced = referenced_widgets
31
+ referenced + referenced.map { |w| w.contained_widgets }.flatten
32
+ end
33
+
34
+ def read_attribute(attribute_name)
35
+ @attribute_cache.fetch(attribute_name) do
36
+ (raw_value, attribute_type) = data_from_cms.value_and_type_of(attribute_name)
37
+ @attribute_cache[attribute_name] =
38
+ prepare_attribute_value(raw_value, attribute_type, attribute_name)
39
+ end
40
+ end
41
+
42
+ def has_attribute?(name)
43
+ data_from_cms.has_custom_attribute?(name.to_s)
44
+ end
45
+
46
+ # @return [String]
47
+ def type_of_attribute(field_name)
48
+ data_from_cms.type_of(field_name.to_s)
49
+ end
50
+
51
+ # Returns the value of an internal or external attribute specified by its name.
52
+ # Passing an invalid key will not raise an error, but return +nil+.
53
+ # @api public
54
+ def [](key)
55
+ key = key.to_s
56
+ has_attribute?(key) ? read_attribute(key) : nil
57
+ end
58
+
59
+ # Hook method to control which widget classes should be available for this page.
60
+ # Override it to allow only certain classes or none.
61
+ # Must return either +NilClass+, or +Array+.
62
+ #
63
+ # If +nil+ is returned (default), then all widget classes will be available for this page.
64
+ #
65
+ # If +Array+ is returned, then it should include desired class names.
66
+ # Each class name must be either a +String+ or a +Symbol+.
67
+ # Only this class names will be available for this page.
68
+ # Order of the class names will be preserved.
69
+ #
70
+ # @param [String] field_name Name of the widget field.
71
+ # @return [nil, Array<Symbol, String>]
72
+ # @api public
73
+ def valid_widget_classes_for(field_name)
74
+ end
75
+
76
+ def modification_for_attribute(attribute_name, revision=Workspace.current.base_revision)
77
+ if new?(revision)
78
+ Modification::NEW
79
+ elsif deleted?(revision)
80
+ Modification::DELETED
81
+ else
82
+ cms_data_in_revision = cms_data_for_revision(revision)
83
+
84
+ if cms_data_in_revision
85
+ other_value = cms_data_in_revision.unchecked_value_of(attribute_name.to_s)
86
+ if data_from_cms.unchecked_value_of(attribute_name.to_s) == other_value
87
+ Modification::UNMODIFIED
88
+ else
89
+ Modification::EDITED
90
+ end
91
+ else # I am deleted in both revisions!
92
+ Modification::UNMODIFIED
93
+ end
94
+ end
95
+ end
96
+
97
+ def update_data(data)
98
+ self.data_from_cms = data
99
+ @attribute_cache = {}
100
+ end
101
+
102
+ private
103
+
104
+ attr_writer :data_from_cms
105
+
106
+ def data_from_cms
107
+ if @data_from_cms.respond_to?(:call)
108
+ @data_from_cms = @data_from_cms.call
109
+ else
110
+ @data_from_cms
111
+ end
112
+ end
113
+
114
+ def prepare_attribute_value(attribute_value, attribute_type, attribute_name)
115
+ case attribute_type
116
+ when "html"
117
+ StringTagging.tag_as_html(attribute_value)
118
+ when "date"
119
+ DateAttribute.parse(attribute_value) if attribute_value
120
+ when "linklist"
121
+ build_links(attribute_value)
122
+ when "reference"
123
+ BasicObj.find([attribute_value]).first
124
+ when "referencelist"
125
+ BasicObj.find(attribute_value).compact
126
+ when "widget"
127
+ build_widgets(attribute_value, attribute_name)
128
+ else
129
+ attribute_value
130
+ end
131
+ end
132
+
133
+ def build_links(link_definitions)
134
+ if link_definitions.present?
135
+ link_definitions = link_definitions.map(&:with_indifferent_access)
136
+
137
+ object_ids = link_definitions.map { |link_data| link_data[:destination] }.compact.uniq
138
+ objects = object_ids.empty? ? [] : Obj.find(object_ids)
139
+ link_definitions.each_with_object([]) do |link_data, links|
140
+ obj = objects.detect { |o| o && o.id == link_data[:destination] }
141
+ link = Link.new(link_data.merge(obj: obj))
142
+ links << link if link.resolved?
143
+ end
144
+ else
145
+ []
146
+ end
147
+ end
148
+
149
+ def build_widgets(widget_data, attribute_name)
150
+ widget_data.map do |widget_id|
151
+ widget = widget_from_pool(widget_id)
152
+
153
+ unless widget
154
+ raise ScrivitoError, "Widget with ID #{widget_id} not found!"
155
+ end
156
+
157
+ widget.container = self
158
+ widget.container_field_name = attribute_name
159
+
160
+ widget
161
+ end
162
+ end
163
+
164
+ module ClassMethods
165
+ # Instantiate an Obj or Widget instance from obj_data.
166
+ # If a subclass of Obj or Widget with the same name as the property +_obj_class+ exists,
167
+ # the instantiated Obj or Widget will be an instance of that subclass.
168
+ def instantiate(obj_data)
169
+ obj_class = obj_data.value_of('_obj_class')
170
+
171
+ instance = type_computer.compute_type(obj_class).allocate
172
+ instance.update_data(obj_data)
173
+
174
+ instance
175
+ end
176
+
177
+ def with_default_obj_class(attributes)
178
+ return attributes if attributes[:_obj_class] || attributes["_obj_class"]
179
+ return attributes if type_computer.special_class?(self)
180
+ attributes.merge("_obj_class" => self.to_s)
181
+ end
182
+
183
+ def descendants
184
+ type_computer = TypeComputer.new(self, nil)
185
+ CmsRestApi.get("workspaces/#{Workspace.current.id}/obj_classes")['results']
186
+ .map { |obj_class_spec| obj_class_spec['name'] }
187
+ .sort
188
+ .map { |obj_class_name| type_computer.compute_type(obj_class_name) }
189
+ .compact
190
+ end
191
+ end
192
+ end
193
+
194
+ end
@@ -0,0 +1,4 @@
1
+ module Scrivito
2
+ class BackendError < Scrivito::CommunicationError
3
+ end
4
+ end
@@ -0,0 +1,840 @@
1
+ require 'json'
2
+ require 'ostruct'
3
+ require 'active_model/naming'
4
+
5
+ module Scrivito
6
+ # The CMS file class
7
+ # @api public
8
+ class BasicObj
9
+ extend ActiveModel::Naming
10
+
11
+ include AttributeContent
12
+ include ModelIdentity
13
+
14
+ def self.type_computer
15
+ @_type_computer ||= TypeComputer.new(Scrivito::BasicObj, ::Obj)
16
+ end
17
+
18
+ def self.reset_type_computer!
19
+ @_type_computer = nil
20
+ end
21
+
22
+ # Create a new {BasicObj Obj} in the cms
23
+ #
24
+ # This allows you to set the different attributes types of an obj by
25
+ # providing a hash with the attributes names as key and the values you want
26
+ # to set as values
27
+ #
28
+ # @example Reference lists have to be provided as an Array of {BasicObj Objs}
29
+ # Obj.create(:reference_list => [other_obj])
30
+ #
31
+ # @example Passing an {BasicObj Obj} allows you to set a reference
32
+ # Obj.create(:reference => other_obj)
33
+ #
34
+ # @example you can upload files by passing a ruby File object
35
+ # Obj.create(:blob => File.new("image.png"))
36
+ #
37
+ # @example Link list can be set as an Array of {Link Links}
38
+ # Obj.create(:link_list => [
39
+ # # external link
40
+ # Link.new(:url => "http://www.example.com", :title => "Example"),
41
+ # # internal link
42
+ # Link.new(:obj => other_obj, :title => "Other Obj")
43
+ # ])
44
+ #
45
+ # @example Dates attributes accept Time, Date and their subclasses (DateTime for example)
46
+ # Obj.create(:date => Time.new)
47
+ # Obj.create(:date => Date.now)
48
+ #
49
+ # @example String, text, html and enum can be set by passing a {String} value
50
+ # Obj.create(:title => "My Title")
51
+ #
52
+ # @example Arrays of {String Strings} allow you to set multi enum fields
53
+ # Obj.create(:tags => ["ruby", "rails"])
54
+ #
55
+ # @example Simply pass an Array of {BasicWidget Widgets} to change a widget field
56
+ # # Add new widgets
57
+ # Obj.create(:widgets => [Widget.new(_obj_class: 'TitleWidget', tite: 'My Title')])
58
+ #
59
+ # # Changing a widget field
60
+ # obj.update(:widgets => [obj.widgets.first])
61
+ #
62
+ # # Clear a widget field
63
+ # obj.update(:widgets => [])
64
+ #
65
+ # @api public
66
+ # @param [Hash] attributes
67
+ # @return [Obj] the newly created {BasicObj Obj}
68
+ def self.create(attributes)
69
+ attributes = with_default_obj_class(attributes)
70
+
71
+ widget_hash = CmsRestApi::WidgetExtractor.call(attributes)
72
+ converted_attributes = CmsRestApi::AttributeSerializer.convert(attributes)
73
+
74
+ converted_attributes['_widget_pool'] =
75
+ CmsRestApi::AttributeSerializer.generate_widget_pool_changes(widget_hash)
76
+
77
+ json = CmsRestApi.post(cms_rest_api_path, obj: converted_attributes)
78
+
79
+ obj = find(json['id'])
80
+
81
+ CmsRestApi::WidgetExtractor.notify_persisted_widgets(obj, widget_hash)
82
+
83
+ obj
84
+ end
85
+
86
+ # Create a new {BasicObj Obj} instance with the given values and attributes.
87
+ # Normally this method should not be used.
88
+ # Instead Objs should be loaded from the cms database.
89
+ def initialize(attributes = {})
90
+ update_data(ObjDataFromHash.new(attributes))
91
+ end
92
+
93
+ # @api public
94
+ def id
95
+ read_attribute('_id')
96
+ end
97
+
98
+ def revision=(revision)
99
+ raise "cannot change revision once set!" if @revision
100
+ @revision = revision
101
+ end
102
+
103
+ def revision
104
+ @revision or raise "revision not set!"
105
+ end
106
+
107
+ ### FINDERS ####################
108
+
109
+ # Find a {BasicObj Obj} by its id.
110
+ # If the paremeter is an Array containing ids, return a list of corresponding Objs.
111
+ # @param [String, Integer, Array<String, Integer>]id_or_list
112
+ # @return [Obj, Array<Obj>]
113
+ # @api public
114
+ def self.find(id_or_list)
115
+ find_filtering_deleted(id_or_list, false)
116
+ end
117
+
118
+ def self.find_by_id(id)
119
+ find_objs_by(:id, [id]).first.first
120
+ end
121
+
122
+ # Find a {BasicObj Obj} by its id.
123
+ # If the paremeter is an Array containing ids, return a list of corresponding Objs.
124
+ # The results include deleted objects as well.
125
+ # @param [String, Integer, Array<String, Integer>]id_or_list
126
+ # @return [Obj, Array<Obj>]
127
+ # @api public
128
+ def self.find_including_deleted(id_or_list)
129
+ find_filtering_deleted(id_or_list, true)
130
+ end
131
+
132
+ # Returns a {ObjSearchEnumerator} with the given initial subquery consisting of the four arguments.
133
+ #
134
+ # Note that +field+ and +value+ can also be arrays for searching several fields or searching for several values.
135
+ #
136
+ # {ObjSearchEnumerator}s can be chained using one of the chainable methods (e.g. {ObjSearchEnumerator#and} and {ObjSearchEnumerator#and_not}).
137
+ #
138
+ # @example Look for the first 10 Objs whose ObjClass is "Pressrelease" and whose title contains "quarterly":
139
+ # Obj.where(:_obj_class, :equals, 'Pressrelease').and(:title, :contains, 'quarterly').take(10)
140
+ # @param [Symbol, String, Array<Symbol, String>] field See {ObjSearchEnumerator#and} for details
141
+ # @param [Symbol, String] operator See {ObjSearchEnumerator#and} for details
142
+ # @param [String, Array<String>] value See {ObjSearchEnumerator#and} for details
143
+ # @param [Hash] boost See {ObjSearchEnumerator#and} for details
144
+ # @return [ObjSearchEnumerator]
145
+ # @api public
146
+ def self.where(field, operator, value, boost = nil)
147
+ ObjSearchEnumerator.new(nil).and(field, operator, value, boost)
148
+ end
149
+
150
+ # Returns a {ObjSearchEnumerator} of all {BasicObj Obj}s.
151
+ # If invoked on a subclass of Obj, the result will be restricted to instances of that subclass.
152
+ # @return [ObjSearchEnumerator]
153
+ # @api public
154
+ def self.all
155
+ if superclass == Scrivito::BasicObj
156
+ search_for_all
157
+ else
158
+ find_all_by_obj_class(name)
159
+ end
160
+ end
161
+
162
+ # Returns a {ObjSearchEnumerator} of all Objs with the given +obj_class+.
163
+ # @param [String] obj_class Name of the ObjClass.
164
+ # @return [ObjSearchEnumerator]
165
+ # @api public
166
+ def self.find_all_by_obj_class(obj_class)
167
+ search_for_all.and(:_obj_class, :equals, obj_class)
168
+ end
169
+
170
+ # Find the {BasicObj Obj} with the given path.
171
+ # Returns +nil+ if no matching Obj exists.
172
+ # @param [String] path Path of the {BasicObj Obj}.
173
+ # @return [Obj]
174
+ # @api public
175
+ def self.find_by_path(path)
176
+ find_objs_by(:path, [path]).first.first
177
+ end
178
+
179
+ def self.find_many_by_paths(pathes)
180
+ find_objs_by(:path, pathes).map(&:first)
181
+ end
182
+
183
+ # Find an {BasicObj Obj} with the given name.
184
+ # If several Objs with the given name exist, an arbitrary one of these Objs is chosen and returned.
185
+ # If no Obj with the name exits, +nil+ is returned.
186
+ # @param [String] name Name of the {BasicObj Obj}.
187
+ # @return [Obj]
188
+ # @api public
189
+ def self.find_by_name(name)
190
+ where(:_name, :equals, name).batch_size(1).first
191
+ end
192
+
193
+ # Returns a {ObjSearchEnumerator} of all Objs with the given name.
194
+ # @param [String] name Name of the {BasicObj Obj}.
195
+ # @return [ObjSearchEnumerator]
196
+ # @api public
197
+ def self.find_all_by_name(name)
198
+ where(:_name, :equals, name)
199
+ end
200
+
201
+ # Returns the {BasicObj Obj} with the given permalink, or +nil+ if no matching Obj exists.
202
+ # @param [String] permalink The permalink of the {BasicObj Obj}.
203
+ # @return [Obj]
204
+ # @api public
205
+ def self.find_by_permalink(permalink)
206
+ find_objs_by(:permalink, [permalink]).first.first
207
+ end
208
+
209
+ # Returns the {BasicObj Obj} with the given permalink, or raise ResourceNotFound if no matching Obj exists.
210
+ # @param [String] permalink The permalink of the {BasicObj Obj}.
211
+ # @return [Obj]
212
+ # @api public
213
+ def self.find_by_permalink!(permalink)
214
+ find_by_permalink(permalink) or
215
+ raise ResourceNotFound, "Could not find Obj with permalink '#{permalink}'"
216
+ end
217
+
218
+ # accepts the name of an "obj_by" - view, a list of keys
219
+ # and an "include_deleted" flag
220
+ # returns a list of lists of Objs: a list of Objs for each given keys.
221
+ def self.find_objs_by(view, keys, include_deleted = false)
222
+ if include_deleted
223
+ finder_method_name = :find_obj_data_including_deleted_by
224
+ else
225
+ finder_method_name = :find_obj_data_by
226
+ end
227
+
228
+ revision = Workspace.current.revision
229
+ result = CmsBackend.instance.public_send(finder_method_name, revision, view, keys)
230
+
231
+ result.map do |list|
232
+ list.map do |obj_data|
233
+ obj = BasicObj.instantiate(obj_data)
234
+ obj.revision = revision
235
+
236
+ obj
237
+ end
238
+ end
239
+ end
240
+
241
+ # Hook method to control which page classes should be available for a page with given path.
242
+ # Override it to allow only certain classes or none.
243
+ # Must return either +NilClass+, or +Array+.
244
+ #
245
+ # Be aware that the given argument is a parent path.
246
+ # E.g. when creating a page with path +/products/shoes+ then the argument will be +/products+.
247
+ #
248
+ # If +NilClass+ is returned, then all possible classes will be available.
249
+ # By default +NilClass+ is returned.
250
+ #
251
+ # If +Array+ is returned, then it should include desired class names.
252
+ # Each class name must be either a +String+ or a +Symbol+.
253
+ # Only this class names will be available. Order of the class names will be preserved.
254
+ #
255
+ # @param [String] parent_path Path of the parent obj
256
+ # @return [NilClass, Array<Symbol, String>]
257
+ # @api public
258
+ def self.valid_page_classes_beneath(parent_path)
259
+ end
260
+
261
+ # Update the {BasicObj Obj} with the attributes provided.
262
+ #
263
+ # For an overview of which values you can set via this method see the
264
+ # documentation of {BasicObj.create Obj.create}.
265
+ #
266
+ # @api public
267
+ # @param [Hash] attributes
268
+ def update(attributes)
269
+ widget_hash = CmsRestApi::WidgetExtractor.call(attributes, self)
270
+ converted_attributes = CmsRestApi::AttributeSerializer.convert(attributes)
271
+
272
+ converted_attributes['_widget_pool'] =
273
+ CmsRestApi::AttributeSerializer.generate_widget_pool_changes(widget_hash)
274
+
275
+ widget_pool = converted_attributes['_widget_pool']
276
+ widget_gc = WidgetGarbageCollection.new(self, {self => attributes}.merge(widget_hash))
277
+ widget_gc.widgets_to_delete.each { |widget| widget_pool[widget.id] = nil }
278
+
279
+ CmsRestApi.put(cms_rest_api_path, obj: converted_attributes)
280
+
281
+ Workspace.reload
282
+
283
+ reload
284
+
285
+ CmsRestApi::WidgetExtractor.notify_persisted_widgets(self, widget_hash)
286
+
287
+ self
288
+ end
289
+
290
+ # Destroys the {BasicObj Obj} in the current {Workspace}
291
+ # @api public
292
+ def destroy
293
+ if children.any?
294
+ raise ClientError.new(I18n.t('scrivito.errors.models.basic_obj.has_children'), 412)
295
+ end
296
+
297
+ CmsRestApi.delete(cms_rest_api_path)
298
+
299
+ Workspace.reload
300
+ end
301
+
302
+ def to_param
303
+ id
304
+ end
305
+
306
+ # return the {BasicObj Obj} that is the parent of this Obj.
307
+ # returns +nil+ for the root Obj.
308
+ # @api public
309
+ def parent
310
+ if child_path?
311
+ BasicObj.find_by_path(parent_path)
312
+ end
313
+ end
314
+
315
+ # Returns an Array of all the ancestor objects, starting at the root and ending at this object's parent.
316
+ # @return [Array<Obj>]
317
+ # @api public
318
+ def ancestors
319
+ return [] unless child_path?
320
+
321
+ ancestor_paths = parent_path.scan(/\/[^\/]+/).inject([""]) do |list, component|
322
+ list << list.last + component
323
+ end
324
+ ancestor_paths[0] = "/"
325
+ BasicObj.find_many_by_paths(ancestor_paths)
326
+ end
327
+
328
+ # return a list of all child {BasicObj Obj}s.
329
+ # @return [Array<Obj>]
330
+ # @api public
331
+ def children
332
+ return [] unless path
333
+
334
+ self.class.find_objs_by(:ppath, [path]).first
335
+ end
336
+
337
+ ### ATTRIBUTES #################
338
+
339
+ # returns the {BasicObj Obj}'s path as a String.
340
+ # @api public
341
+ def path
342
+ read_attribute('_path')
343
+ end
344
+
345
+ # returns the {BasicObj Obj}'s name, i.e. the last component of the path.
346
+ # @api public
347
+ def name
348
+ if child_path?
349
+ path.match(/[^\/]+$/)[0]
350
+ else
351
+ ""
352
+ end
353
+ end
354
+
355
+ # Returns the root {BasicObj Obj}, i.e. the Obj with the path "/"
356
+ # @return [Obj]
357
+ # @api public
358
+ def self.root
359
+ BasicObj.find_by_path("/") or raise ResourceNotFound,
360
+ "Obj.root not found: There is no Obj with path '/'."
361
+ end
362
+
363
+ # Returns the homepage obj. This can be overwritten in your application's +Obj+.
364
+ # Use {#homepage?} to check if an obj is the homepage.
365
+ # @return [Obj]
366
+ # @api public
367
+ def self.homepage
368
+ root
369
+ end
370
+
371
+ # @api private
372
+ def self.generate_widget_pool_id
373
+ SecureRandom.hex(4)
374
+ end
375
+
376
+ # returns the obj's permalink.
377
+ # @api public
378
+ def permalink
379
+ read_attribute('_permalink')
380
+ end
381
+
382
+ # This method determines the controller that should be invoked when the +Obj+ is requested.
383
+ # By default a controller matching the Obj's obj_class will be used.
384
+ # If the controller does not exist, the CmsController will be used as a fallback.
385
+ # Overwrite this method to force a different controller to be used.
386
+ # @return [String]
387
+ # @api public
388
+ def controller_name
389
+ obj_class_name
390
+ end
391
+
392
+ # This method determines the action that should be invoked when the +Obj+ is requested.
393
+ # The default action is 'index'.
394
+ # Overwrite this method to force a different action to be used.
395
+ # @return [String]
396
+ # @api public
397
+ def controller_action_name
398
+ "index"
399
+ end
400
+
401
+ # Returns true if the current obj is the {.homepage} obj.
402
+ # @api public
403
+ def homepage?
404
+ self == self.class.homepage
405
+ end
406
+
407
+ # This method is used to calculate a part of a URL of this Obj.
408
+ #
409
+ # The routing schema: <code><em><obj.id></em>/<em><obj.slug></em></code>
410
+ #
411
+ # The default is {http://apidock.com/rails/ActiveSupport/Inflector/parameterize parameterize}
412
+ # on +obj.title+.
413
+ #
414
+ # You can customize this part by overwriting {#slug}.
415
+ # @return [String]
416
+ # @api public
417
+ def slug
418
+ (title || '').parameterize
419
+ end
420
+
421
+ # This method determines the description that is shown in the changes list.
422
+ # It can be overriden by a custom value.
423
+ # @api public
424
+ def description_for_editor
425
+ slug.presence || path
426
+ end
427
+
428
+ # Returns the title of the content or the name.
429
+ # @return [String]
430
+ # @api public
431
+ def display_title
432
+ self.title || name
433
+ end
434
+
435
+ # @api public
436
+ def title
437
+ read_attribute('title')
438
+ end
439
+
440
+ # Returns true if image? or generic?
441
+ def binary?
442
+ [:image, :generic].include?(read_attribute('_obj_type').to_sym)
443
+ end
444
+
445
+ # Returns true if this object is the root object.
446
+ # @api public
447
+ def root?
448
+ path == "/"
449
+ end
450
+
451
+ # Returns a list of children excluding the binary? ones unless :all is specfied.
452
+ # This is mainly used for navigations.
453
+ # @return [Array<Obj>]
454
+ # @api public
455
+ def toclist(*args)
456
+ return [] if binary?
457
+ toclist = children
458
+ toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all)
459
+ toclist
460
+ end
461
+
462
+ # @param objs_to_be_sorted [Array<BasicObj>] unsorted list of Objs
463
+ # @param list [Array<BasicObj>] list of Objs that defines the order
464
+ # @return [Array<BasicObj>] a sorted list of Objs. Any objs present in +objs_to_be_sorted+ but not in +list+ are appended at the end, sorted by +Obj#id+
465
+ def self.sort_by_list(objs_to_be_sorted, list)
466
+ (list & objs_to_be_sorted) + (objs_to_be_sorted - list).sort_by(&:id)
467
+ end
468
+
469
+ # This should be a SET, because it's faster in this particular case.
470
+ OLD_INTERNAL_KEYS = Set.new(%w[
471
+ body
472
+ id
473
+ last_changed
474
+ name
475
+ obj_class_name
476
+ path
477
+ permalink
478
+ text_links
479
+ title
480
+ ])
481
+
482
+ # Returns the value of an internal or external attribute specified by its name.
483
+ # Passing an invalid key will not raise an error, but return +nil+.
484
+ # @api public
485
+ def [](key)
486
+ key = key.to_s
487
+ if OLD_INTERNAL_KEYS.include?(key)
488
+ send(key)
489
+ elsif key.start_with?('_') && OLD_INTERNAL_KEYS.include?(internal_key = key[1..-1])
490
+ # For backwards compatibility reasons
491
+ send(internal_key)
492
+ else
493
+ super
494
+ end
495
+ end
496
+
497
+ # Reloads the attributes of this object from the database.
498
+ # Notice that the ruby class of this Obj instance will NOT change,
499
+ # even if the obj_class in the database has changed.
500
+ # @api public
501
+ def reload
502
+ id = self.id.to_s
503
+
504
+ reload_data = Proc.new do
505
+ CmsBackend.instance.find_obj_data_by(Workspace.current.revision, :id, [id]).first.first
506
+ end
507
+
508
+ update_data(reload_data)
509
+ end
510
+
511
+ # @return [String]
512
+ # @api public
513
+ def obj_class_name
514
+ read_attribute('_obj_class')
515
+ end
516
+
517
+ def obj_class
518
+ raise ScrivitoError, "BasicObj#obj_class is no longer available"+
519
+ ", please use BasicObj#obj_class_name instead."
520
+ end
521
+
522
+ # @api public
523
+ def last_changed
524
+ read_attribute('_last_changed')
525
+ end
526
+
527
+ def new?(revision=Workspace.current.base_revision)
528
+ if read_attribute('_modification') != 'deleted'
529
+ cms_data_for_revision(revision).nil?
530
+ else
531
+ false
532
+ end
533
+ end
534
+
535
+ def deleted?(revision=Workspace.current.base_revision)
536
+ if read_attribute('_modification') == 'deleted'
537
+ cms_data_for_revision(revision).present?
538
+ end
539
+ end
540
+
541
+ def modification(revision=Workspace.current.base_revision)
542
+ obj_data_from_revision = cms_data_for_revision(revision)
543
+
544
+ if deleted?(revision)
545
+ Modification::DELETED
546
+ elsif new?(revision)
547
+ Modification::NEW
548
+ else # Edited
549
+ if obj_data_from_revision.present?
550
+ if data_from_cms == obj_data_from_revision
551
+ Modification::UNMODIFIED
552
+ else
553
+ Modification::EDITED
554
+ end
555
+ else
556
+ Modification::UNMODIFIED
557
+ end
558
+ end
559
+ end
560
+
561
+ def widget_data_for_revision(id, revision)
562
+ if revision_obj_data = cms_data_for_revision(revision)
563
+ revision_obj_data.value_of('_widget_pool')[id]
564
+ end
565
+ end
566
+
567
+ def in_revision(revision)
568
+ if obj_data = cms_data_for_revision(revision)
569
+ obj = Obj.instantiate(obj_data)
570
+ obj.revision = revision
571
+
572
+ obj
573
+ end
574
+ end
575
+
576
+ # For a binary Obj, the content_type is equal to the content_type of its body (i.e. its data).
577
+ # For non-binary Objs, a the default content_type is "text/html".
578
+ # Override this method in subclasses to define a different content_type.
579
+ # Note that only Objs with content_type "text/html"
580
+ # will be rendered with layout and templates by the DefaultCmsController.
581
+ # @return [String]
582
+ # @api public
583
+ def content_type
584
+ if binary?
585
+ body_content_type
586
+ else
587
+ "text/html"
588
+ end
589
+ end
590
+ alias mime_type content_type
591
+
592
+ # returns the extension (the part after the last dot) from the Obj's name.
593
+ # returns an empty string if no extension is present in the Obj's name.
594
+ # @return [String]
595
+ # @api public
596
+ def file_extension
597
+ File.extname(name)[1..-1] || ""
598
+ end
599
+
600
+ # Returns the body (main content) of the Obj for non-binary Objs.
601
+ # Returns +nil+ for binary Objs.
602
+ # @return [String]
603
+ # @api public
604
+ def body
605
+ if binary?
606
+ nil
607
+ else
608
+ StringTagging.tag_as_html(read_attribute('body'))
609
+ end
610
+ end
611
+
612
+ # for binary Objs body_length equals the file size
613
+ # for non-binary Objs body_length equals the number of characters in the body (main content)
614
+ # @api public
615
+ def body_length
616
+ if binary?
617
+ blob = find_blob
618
+ blob ? blob.length : 0
619
+ else
620
+ (body || "").length
621
+ end
622
+ end
623
+
624
+ # returns an URL to retrieve the Obj's body for binary Objs.
625
+ # returns +nil+ for non-binary Objs.
626
+ # @return [String]
627
+ # @api public
628
+ def body_data_url
629
+ if binary?
630
+ blob = find_blob
631
+ blob.url if blob
632
+ end
633
+ end
634
+
635
+ # returns the content type of the Obj's body for binary Objs.
636
+ # returns +nil+ for non-binary Objs.
637
+ # @return [String]
638
+ # @api public
639
+ def body_content_type
640
+ if binary?
641
+ blob = find_blob
642
+ if blob
643
+ blob.content_type
644
+ else
645
+ "application/octet-stream"
646
+ end
647
+ end
648
+ end
649
+
650
+ def inspect
651
+ "<#{self.class} id=\"#{id}\" path=\"#{path}\">"
652
+ end
653
+
654
+ def details_view_path
655
+ "#{obj_class_name.underscore}/details"
656
+ end
657
+
658
+ def widget_from_pool(widget_id)
659
+ widget_data = widget_data_from_pool(widget_id)
660
+ instantiate_widget(widget_id, widget_data) if widget_data
661
+ end
662
+
663
+ def copy_widget_from(src_obj_id, src_widget_id)
664
+ raise "cannot copy widget, since workspace is not modifiable" if Workspace.current.published?
665
+
666
+ src_obj_content = CmsRestApi.get(cms_rest_api_path(src_obj_id))
667
+ widget_content = src_obj_content["_widget_pool"]["#{src_widget_id}"]
668
+
669
+ raise "cannot copy widget, since widget does not exist" unless widget_content
670
+
671
+ src_widget = BasicObj.find(src_obj_id).widget_from_pool(src_widget_id)
672
+ widget_content.delete_if do |attribute_name, _|
673
+ src_widget.type_of_attribute(attribute_name) == "widget"
674
+ end
675
+ widget_pool_id = BasicObj.generate_widget_pool_id
676
+
677
+ CmsRestApi.put(cms_rest_api_path, obj: {_widget_pool: {widget_pool_id => widget_content}})
678
+
679
+ widget_pool_id
680
+ end
681
+
682
+ # for internal testing purposes only
683
+ def blob_id
684
+ find_blob.try(:id)
685
+ end
686
+
687
+ # Reverts changes of this object.
688
+ # After calling this method it's as if this object has been never modified in the current working copy.
689
+ # This method does not work with +new+ or +deleted+ objects.
690
+ # This method also does also not work for the +published+ workspace or the +rtc+ working copy.
691
+ def revert
692
+ Workspace.current.assert_revertable
693
+
694
+ if binary?
695
+ raise "revert not supported for binary objs"
696
+ else
697
+ case modification
698
+ when Modification::UNMODIFIED
699
+ # don't do anything
700
+ when Modification::EDITED
701
+ previous_content = CmsRestApi.get(
702
+ "revisions/#{Workspace.current.base_revision_id}/objs/#{id}")
703
+ updated_content = previous_content.except('id', '_id')
704
+
705
+ added_widget_ids = read_widget_pool.keys - previous_content['_widget_pool'].keys
706
+ added_widget_ids.each do |added_widget_id|
707
+ updated_content['_widget_pool'][added_widget_id] = nil
708
+ end
709
+
710
+ CmsRestApi.put(cms_rest_api_path, obj: updated_content)
711
+
712
+ reload
713
+ else
714
+ raise ScrivitoError, "cannot revert changes, since obj is #{modification}."
715
+ end
716
+ end
717
+ end
718
+
719
+ def mark_resolved
720
+ CmsRestApi.put(cms_rest_api_path, obj: {_conflicts: nil})
721
+ reload
722
+ end
723
+
724
+ def container_and_field_name_for_widget(widget_id)
725
+ if field_name = field_name_in_data_for_widget(data_from_cms, widget_id)
726
+ return [self, field_name]
727
+ else
728
+ read_widget_pool.each do |parent_widget_id, widget_data|
729
+ if field_name = field_name_in_data_for_widget(widget_data, widget_id)
730
+ return [widget_from_pool(parent_widget_id), field_name]
731
+ end
732
+ end
733
+ end
734
+ [nil, nil]
735
+ end
736
+
737
+ def widget_data_from_pool(widget_id)
738
+ read_widget_pool[widget_id]
739
+ end
740
+
741
+ def has_conflict?
742
+ read_attribute('_conflicts') != nil
743
+ end
744
+
745
+ def all_widgets_from_pool
746
+ read_widget_pool.keys.map do |widget_id|
747
+ widget_from_pool(widget_id)
748
+ end
749
+ end
750
+
751
+ def generate_widget_pool_id
752
+ 10.times do
753
+ id = self.class.generate_widget_pool_id
754
+
755
+ return id if widget_data_from_pool(id).nil?
756
+ end
757
+
758
+ raise ScrivitoError.new('Could not generate a new unused widget id')
759
+ end
760
+
761
+ private
762
+
763
+ def cms_data_for_revision(revision)
764
+ if revision
765
+ CmsBackend.instance.find_obj_data_by(revision, "id", [id]).first.first
766
+ end
767
+ end
768
+
769
+ def field_name_in_data_for_widget(data, widget_id)
770
+ data.all_custom_attributes.find do |attribute_name|
771
+ (value, type) = data.value_and_type_of(attribute_name)
772
+ type == "widget" && value.include?(widget_id)
773
+ end
774
+ end
775
+
776
+ def read_widget_pool
777
+ read_attribute('_widget_pool')
778
+ end
779
+
780
+ def instantiate_widget(widget_id, widget_data)
781
+ BasicWidget.instantiate(widget_data).tap do |widget|
782
+ widget.id = widget_id
783
+ widget.obj = self
784
+ end
785
+ end
786
+
787
+ def parent_path
788
+ raise "parent_path called for root" if root?
789
+ path.gsub(/\/[^\/]+$/, "").presence || "/"
790
+ end
791
+
792
+ def as_date(value)
793
+ DateAttribute.parse(value) unless value.nil?
794
+ end
795
+
796
+ def find_blob
797
+ blob_spec = read_attribute('blob')
798
+ Blob.find(blob_spec["id"]) if blob_spec
799
+ end
800
+
801
+ def cms_rest_api_path(obj_id = id)
802
+ "#{self.class.cms_rest_api_path}/#{obj_id}"
803
+ end
804
+
805
+ def child_path?
806
+ !path.nil? && !root?
807
+ end
808
+
809
+ class << self
810
+ def restore(obj_id)
811
+ Workspace.current.assert_revertable
812
+
813
+ base_revision_path = "revisions/#{Workspace.current.base_revision_id}/objs/#{obj_id}"
814
+ obj_attributes = CmsRestApi.get(base_revision_path).except('id').merge('_id' => obj_id)
815
+ CmsRestApi.post(cms_rest_api_path, obj: obj_attributes)
816
+ end
817
+
818
+ def cms_rest_api_path
819
+ "workspaces/#{Workspace.current.id}/objs"
820
+ end
821
+
822
+ private
823
+
824
+ def find_filtering_deleted(id_or_list, include_deleted)
825
+ case id_or_list
826
+ when Array
827
+ find_objs_by(:id, id_or_list, include_deleted).map(&:first).compact
828
+ else
829
+ obj = find_objs_by(:id, [id_or_list.to_s], include_deleted).first.first
830
+ obj or raise ResourceNotFound, "Could not find Obj with id #{id_or_list}"
831
+ end
832
+ end
833
+
834
+ def search_for_all
835
+ ObjSearchEnumerator.new(nil).batch_size(1000)
836
+ end
837
+ end
838
+ end
839
+
840
+ end