scrivito_sdk 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +5 -0
- data/README +6 -0
- data/app/controllers/cms_controller.rb +7 -0
- data/app/controllers/scrivito/blobs_controller.rb +10 -0
- data/app/controllers/scrivito/default_cms_controller.rb +61 -0
- data/app/controllers/scrivito/objs_controller.rb +200 -0
- data/app/controllers/scrivito/tasks_controller.rb +11 -0
- data/app/controllers/scrivito/webservice_controller.rb +36 -0
- data/app/controllers/scrivito/workspaces_controller.rb +41 -0
- data/app/helpers/cms_helper.rb +7 -0
- data/app/helpers/cms_routing_helper.rb +7 -0
- data/app/helpers/scrivito/cms_asset_helper.rb +103 -0
- data/app/helpers/scrivito/cms_tag_helper.rb +231 -0
- data/app/helpers/scrivito/default_cms_helper.rb +21 -0
- data/app/helpers/scrivito/default_cms_routing_helper.rb +130 -0
- data/app/helpers/scrivito/display_helper.rb +71 -0
- data/app/helpers/scrivito/editing_helper.rb +26 -0
- data/app/helpers/scrivito/layout_helper.rb +28 -0
- data/app/models/named_link.rb +2 -0
- data/app/views/cms/_index.html.erb +7 -0
- data/app/views/cms/index.html.erb +1 -0
- data/app/views/scrivito/_editing_javascript.html.erb +7 -0
- data/app/views/scrivito/default_cms/show_widget.html.erb +1 -0
- data/app/views/scrivito/objs/copy_widget.html.erb +1 -0
- data/app/views/scrivito/objs/create_widget.html.erb +1 -0
- data/app/views/scrivito/widget_thumbnail.html.erb +9 -0
- data/config/ca-bundle.crt +3509 -0
- data/config/cms_routes.rb +17 -0
- data/config/locales/de.scrivito.errors.yml +7 -0
- data/config/locales/de.scrivito.lib.yml +6 -0
- data/config/locales/de.scrivito.models.yml +6 -0
- data/config/locales/en.scrivito.errors.yml +7 -0
- data/config/locales/en.scrivito.lib.yml +6 -0
- data/config/locales/en.scrivito.models.yml +6 -0
- data/config/routes.rb +37 -0
- data/lib/assets/images/180x120.gif +0 -0
- data/lib/assets/images/scrivito/image_placeholder.png +0 -0
- data/lib/assets/javascripts/scrivito_editing.js +14642 -0
- data/lib/assets/stylesheets/scrivito.css +180 -0
- data/lib/assets/stylesheets/scrivito_editing.css +2213 -0
- data/lib/generators/cms/migration/USAGE +9 -0
- data/lib/generators/cms/migration/migration_generator.rb +21 -0
- data/lib/generators/cms/migration/templates/migration.erb +10 -0
- data/lib/obj.rb +3 -0
- data/lib/scrivito/access_denied.rb +6 -0
- data/lib/scrivito/attribute_content.rb +194 -0
- data/lib/scrivito/backend_error.rb +4 -0
- data/lib/scrivito/basic_obj.rb +840 -0
- data/lib/scrivito/basic_widget.rb +238 -0
- data/lib/scrivito/blob.rb +48 -0
- data/lib/scrivito/cache.rb +41 -0
- data/lib/scrivito/cache_garbage_collector.rb +83 -0
- data/lib/scrivito/cache_middleware.rb +17 -0
- data/lib/scrivito/client_config.rb +62 -0
- data/lib/scrivito/client_error.rb +12 -0
- data/lib/scrivito/cms_accessible.rb +30 -0
- data/lib/scrivito/cms_backend.rb +238 -0
- data/lib/scrivito/cms_cache_storage.rb +51 -0
- data/lib/scrivito/cms_dispatch_controller.rb +46 -0
- data/lib/scrivito/cms_env.rb +63 -0
- data/lib/scrivito/cms_field_tag.rb +112 -0
- data/lib/scrivito/cms_rest_api.rb +151 -0
- data/lib/scrivito/cms_rest_api/attribute_serializer.rb +98 -0
- data/lib/scrivito/cms_rest_api/blob_uploader.rb +18 -0
- data/lib/scrivito/cms_rest_api/widget_extractor.rb +42 -0
- data/lib/scrivito/cms_test_request.rb +23 -0
- data/lib/scrivito/communication_error.rb +17 -0
- data/lib/scrivito/comparison.rb +67 -0
- data/lib/scrivito/configuration.rb +221 -0
- data/lib/scrivito/connection_manager.rb +100 -0
- data/lib/scrivito/content_conversion.rb +43 -0
- data/lib/scrivito/content_service.rb +118 -0
- data/lib/scrivito/content_state.rb +109 -0
- data/lib/scrivito/content_state_caching.rb +47 -0
- data/lib/scrivito/content_state_visitor.rb +19 -0
- data/lib/scrivito/controller_runtime.rb +35 -0
- data/lib/scrivito/date_attribute.rb +16 -0
- data/lib/scrivito/deprecation.rb +21 -0
- data/lib/scrivito/diff.rb +110 -0
- data/lib/scrivito/editing_context.rb +106 -0
- data/lib/scrivito/editing_context_middleware.rb +60 -0
- data/lib/scrivito/engine.rb +65 -0
- data/lib/scrivito/errors.rb +11 -0
- data/lib/scrivito/html_string.rb +18 -0
- data/lib/scrivito/link.rb +187 -0
- data/lib/scrivito/link_parser.rb +81 -0
- data/lib/scrivito/log_subscriber.rb +29 -0
- data/lib/scrivito/migration.rb +2 -0
- data/lib/scrivito/migrations.rb +12 -0
- data/lib/scrivito/migrations/cms_backend.rb +94 -0
- data/lib/scrivito/migrations/installer.rb +45 -0
- data/lib/scrivito/migrations/migration.rb +93 -0
- data/lib/scrivito/migrations/migration_dsl.rb +143 -0
- data/lib/scrivito/migrations/migration_store.rb +23 -0
- data/lib/scrivito/migrations/migrator.rb +135 -0
- data/lib/scrivito/migrations/workspace_lock.rb +39 -0
- data/lib/scrivito/model_identity.rb +13 -0
- data/lib/scrivito/modification.rb +8 -0
- data/lib/scrivito/named_link.rb +75 -0
- data/lib/scrivito/network_error.rb +11 -0
- data/lib/scrivito/obj_data.rb +140 -0
- data/lib/scrivito/obj_data_from_hash.rb +31 -0
- data/lib/scrivito/obj_data_from_service.rb +84 -0
- data/lib/scrivito/obj_params_parser.rb +61 -0
- data/lib/scrivito/obj_search_builder.rb +62 -0
- data/lib/scrivito/obj_search_enumerator.rb +374 -0
- data/lib/scrivito/rate_limit_exceeded.rb +5 -0
- data/lib/scrivito/revision.rb +9 -0
- data/lib/scrivito/string_tagging.rb +18 -0
- data/lib/scrivito/text_link.rb +52 -0
- data/lib/scrivito/text_link_conversion.rb +52 -0
- data/lib/scrivito/type_computer.rb +34 -0
- data/lib/scrivito/widget_field_params.rb +61 -0
- data/lib/scrivito/widget_garbage_collection.rb +97 -0
- data/lib/scrivito/workspace.rb +222 -0
- data/lib/scrivito/workspace_data_from_service.rb +80 -0
- data/lib/scrivito/workspace_selection_middleware.rb +23 -0
- data/lib/scrivito_sdk.rb +19 -0
- data/lib/tasks/cache.rake +12 -0
- data/lib/tasks/migration.rake +35 -0
- data/lib/widget.rb +3 -0
- metadata +291 -0
@@ -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,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,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
|