lib/scrivito/attribute_content.rb in scrivito_sdk-0.50.1 vs lib/scrivito/attribute_content.rb in scrivito_sdk-0.60.0.rc1
- old
+ new
@@ -1,82 +1,125 @@
module Scrivito
+#
+# @api public
+#
module AttributeContent
+ ATTRIBUTE_TYPES = %w[
+ binary
+ date
+ enum
+ html
+ link
+ linklist
+ multienum
+ reference
+ referencelist
+ string
+ stringlist
+ widget
+ widgetlist
+ ]
+
+ COMPATIBLE_ATTRIBUTE_TYPES = [
+ %w[enum string],
+ %w[html string],
+ %w[multienum stringlist],
+ %w[string html],
+ %w[widgetlist widget],
+ ]
+
+ DEFAULT_ATTRIBUTE_VALUES = {
+ 'binary' => nil,
+ 'date' => nil,
+ 'enum' => nil,
+ 'html' => '',
+ 'link' => nil,
+ 'linklist' => [],
+ 'multienum' => [],
+ 'reference' => nil,
+ 'referencelist' => [],
+ 'string' => '',
+ 'text' => '',
+ 'widgetlist' => [],
+ }
+
extend ActiveSupport::Concern
- def respond_to?(method_id, include_private=false)
- if has_custom_attribute?(method_id)
- true
- else
- super
- end
+ delegate :attribute_definitions, to: :class
+
+ def has_attribute?(attribute_name)
+ has_public_system_attribute?(attribute_name) || has_custom_attribute?(attribute_name)
end
- def method_missing(method_name, *args)
- if has_custom_attribute?(method_name)
- read_attribute(method_name.to_s)
- else
- super
+ def has_custom_attribute?(attribute_name)
+ if revision.workspace.uses_obj_classes && attribute_definitions.blank?
+ Scrivito.raise_obj_class_deprecated_error
end
+ !!attribute_definitions[attribute_name]
end
- def referenced_widgets
- data_from_cms.all_custom_attributes.
- select { |attr| type_of_attribute(attr) == "widget" }.
- map { |attr| read_attribute(attr) }.
- flatten
- end
-
- def contained_widgets
- referenced = referenced_widgets
- referenced + referenced.map { |w| w.contained_widgets }.flatten
- end
-
def read_attribute(attribute_name)
@attribute_cache.fetch(attribute_name) do
- (raw_value, attribute_type) = data_from_cms.value_and_type_of(attribute_name)
- @attribute_cache[attribute_name] =
- prepare_attribute_value(raw_value, attribute_type, attribute_name)
+ @attribute_cache[attribute_name] = value_of_attribute(attribute_name)
end
end
- def has_custom_attribute?(name)
- name = name.to_s
- data_from_cms.has_custom_attribute?(name)
+ def type_of_attribute(attribute_name)
+ type_of_system_attribute(attribute_name) || attribute_definitions[attribute_name].try(:type)
end
- alias_method :has_attribute?, :has_custom_attribute?
- # @return [String]
- def type_of_attribute(field_name)
- data_from_cms.type_of(field_name.to_s)
+ def respond_to?(method_id, include_private = false)
+ has_custom_attribute?(method_id) || super
end
- # Returns the value of an internal or external attribute specified by its name.
+ def method_missing(method_name, *args)
+ attribute_name = method_name.to_s
+ has_custom_attribute?(attribute_name) ? read_attribute(attribute_name) : super
+ end
+
+ #
+ # Returns the value of an attribute specified by its name.
# Passing an invalid key will not raise an error, but return +nil+.
+ #
# @api public
- def [](key)
- key = key.to_s
-
- has_attribute?(key) ? read_attribute(key) : nil
+ # @param [Symbol, String] attribute_name the name of the attribute.
+ # @return the value of the attribute if it defined or +nil+ otherwise.
+ #
+ def [](attribute_name)
+ attribute_name = attribute_name.to_s
+ read_attribute(attribute_name) if has_attribute?(attribute_name)
end
+ #
# Hook method to control which widget classes should be available for this page
# or widget. Override it to allow only certain classes or none.
# Must return either +NilClass+, or +Array+.
#
# If +nil+ is returned (default), then all widget classes will be available for this page or widget.
#
- # If +Array+ is returned, then it should include desired class names.
- # Each class name must be either a +String+ or a +Symbol+.
- # Only these class names will be available and their order will be preserved.
+ # If an +Array+ is returned, then it should include the desired classes.
+ # Each class must be either a +String+, a +Symbol+ or the +Class+ itself.
+ # Only these classes will be available and their order will be preserved.
#
- # @param [String] field_name Name of the widget field.
- # @return [nil, Array<Symbol, String>]
# @api public
+ # @param [String] field_name Name of the widget field.
+ # @return [nil, Array<Symbol, String, Class>]
+ #
def valid_widget_classes_for(field_name)
end
+ def valid_widget_class_names_for(field)
+ class_names = []
+ (valid_widget_classes_for(field) || Scrivito.models.widgets.to_a).select do |object|
+ widget_class = object.is_a?(Class) ? object : object.to_s.constantize
+ class_names << widget_class.to_s if widget_class.valid_inside_container?(self.class)
+ end
+ class_names
+ end
+
+
def modification_for_attribute(attribute_name, revision=Workspace.current.base_revision)
return Modification::UNMODIFIED unless revision
if new?(revision)
Modification::NEW
@@ -84,195 +127,179 @@
Modification::DELETED
else
cms_data_in_revision = cms_data_for_revision(revision)
if cms_data_in_revision
- other_value = cms_data_in_revision.value_without_default_of(attribute_name.to_s)
- if data_from_cms.value_without_default_of(attribute_name.to_s) == other_value
+ other_value = cms_data_in_revision.value_of(attribute_name.to_s)
+ if data_from_cms.value_of(attribute_name.to_s) == other_value
Modification::UNMODIFIED
else
Modification::EDITED
end
else # I am deleted in both revisions!
Modification::UNMODIFIED
end
end
end
- def update_data(data)
- self.data_from_cms = data
- @attribute_cache = {}
- end
-
+ #
# Returns the obj class name of this object.
# @api public
# @return [String]
+ #
def obj_class_name
data_from_cms.value_of('_obj_class')
end
+ #
# Returns the obj class of this object.
+ #
# @api public
- # @return [ObjClass]
+ # @deprecated
+ # @see Scrivito::ObjClass.remove
+ #
+ # @return [Scrivito::ObjClass] if the corresponding workspace still uses {Scrivito::ObjClass}.
+ # @return [nil] if the corresponding workspace does not use {Scrivito::ObjClass} anymore.
+ #
def obj_class
- if obj_class_data = CmsBackend.instance.find_obj_class_data_by_name(revision, obj_class_name)
- ObjClass.new(obj_class_data, revision.workspace)
+ if revision.workspace.uses_obj_classes
+ if obj_class_data = CmsBackend.instance.find_obj_class_data_by_name(revision, obj_class_name)
+ ObjClass.new(obj_class_data, revision.workspace)
+ end
+ else
+ Scrivito.print_obj_class_deprecated_warning
+ nil
end
end
+ def find_attribute_containing_widget(widget_id)
+ attribute_definitions.each do |attribute_definition|
+ if attribute_definition.widgetlist?
+ attribute_name = attribute_definition.name
+ return attribute_name if data_from_cms.value_of(attribute_name).include?(widget_id)
+ end
+ end
+ nil
+ end
+
+ def referenced_widgets
+ widgets = []
+ attribute_definitions.each do |attribute_definition|
+ if attribute_definition.widgetlist?
+ widgets += read_attribute(attribute_definition.name)
+ end
+ end
+ widgets
+ end
+
+ def contained_widgets
+ referenced = referenced_widgets
+ referenced + referenced.map { |widget| widget.contained_widgets }.flatten
+ end
+
+ def update_data(data)
+ self.data_from_cms = data
+ @attribute_cache = {}
+ end
+
def to_show_view_path
to_view_path('show')
end
def to_details_view_path
to_view_path('details')
end
- private
-
- attr_writer :data_from_cms
-
def data_from_cms
if @data_from_cms.respond_to?(:call)
@data_from_cms = @data_from_cms.call
else
@data_from_cms
end
end
- def prepare_attribute_value(attribute_value, attribute_type, attribute_name)
- if attribute_name == '_obj_class'
- return obj_class
- end
+ private
- case attribute_type
- when "html"
- StringTagging.tag_as_html(attribute_value)
- when "date"
- DateAttribute.parse(attribute_value) if attribute_value
- when "linklist"
- build_links(attribute_value)
- when "link"
- build_link(attribute_value)
- when "reference"
- workspace.objs.find([attribute_value]).first
- when "referencelist"
- workspace.objs.find(attribute_value).compact
- when "widget"
- build_widgets(attribute_value, attribute_name)
- when "binary"
- build_binary(attribute_value)
- else
- attribute_value
- end
- end
+ attr_writer :data_from_cms
- def build_binary(binary_definition)
- if binary_definition && binary_definition['id']
- Binary.new(binary_definition['id'], workspace.published?)
- end
- end
+ def value_of_attribute(attribute_name)
+ return obj_class if attribute_name == '_obj_class'
+ return value_of_system_attribute(attribute_name) if has_system_attribute?(attribute_name)
- def build_links(link_definitions)
- if link_definitions.present?
- link_definitions = link_definitions.map(&:with_indifferent_access)
-
- object_ids = link_definitions.map { |link_data| link_data[:destination] }.compact.uniq
- objects = object_ids.empty? ? [] : workspace.objs.find(object_ids)
- link_definitions.each_with_object([]) do |link_data, links|
- obj = objects.detect { |o| o && o.id == link_data[:destination] }
- link = Link.new(link_data.merge(obj: obj))
- links << link if link.resolved?
+ if attribute_definition = attribute_definitions[attribute_name]
+ if has_compatible_type_in_backend?(attribute_definition)
+ deserialize_attribute_value(attribute_definition)
+ else
+ default_attribute_value(attribute_definition)
end
- else
- []
end
end
- def build_link(attribute_value)
- return unless attribute_value
-
- if attribute_value['destination']
- build_internal_link(attribute_value)
- else
- build_external_link(attribute_value)
- end
+ def has_compatible_type_in_backend?(attribute_definition)
+ attribute_name, attribute_type = attribute_definition.name, attribute_definition.type
+ attribute_type_from_backend = data_from_cms.type_of(attribute_name)
+ attribute_type == attribute_type_from_backend ||
+ COMPATIBLE_ATTRIBUTE_TYPES.include?([attribute_type, attribute_type_from_backend])
end
- def build_internal_link(attribute_value)
- properties = {
- obj: workspace.objs.find(attribute_value['destination']),
- title: attribute_value['title'],
- query: attribute_value['query'],
- fragment: attribute_value['fragment'],
- target: attribute_value['target'],
- }
-
- Link.new(properties)
- rescue ResourceNotFound
+ def deserialize_attribute_value(attribute_definition)
+ serialized_attribute_value = data_from_cms.value_of(attribute_definition.name)
+ attribute_deserializer.deserialize(serialized_attribute_value, attribute_definition)
end
- def build_external_link(attribute_value)
- properties = {
- url: attribute_value['url'],
- title: attribute_value['title'],
- target: attribute_value['target'],
- }
-
- Link.new(properties)
+ def attribute_deserializer
+ @attribute_deserializer ||= AttributeDeserializer.new(self, workspace)
end
- def build_widgets(widget_data, attribute_name)
- widget_data.map do |widget_id|
- widget = widget_from_pool(widget_id)
-
- unless widget
- raise ScrivitoError, "Widget with ID #{widget_id} not found!"
- end
-
- widget.container = self
- widget.container_field_name = attribute_name
-
- widget
+ def default_attribute_value(attribute_definition)
+ if attribute_value = DEFAULT_ATTRIBUTE_VALUES[attribute_definition.type]
+ attribute_value.dup
end
end
def to_view_path(view_name)
"#{obj_class_name.underscore}/#{view_name}"
end
+ #
+ # @api public
+ #
module ClassMethods
+ #
# Instantiate an Obj or Widget instance from obj_data.
# If a subclass of Obj or Widget with the same name as the property +_obj_class+ exists,
# the instantiated Obj or Widget will be an instance of that subclass.
+ #
def instantiate(obj_data)
obj_class = obj_data.value_of('_obj_class')
instance = type_computer.compute_type(obj_class).allocate
instance.update_data(obj_data)
instance
end
- def with_default_obj_class(attributes)
- return attributes if attributes[:_obj_class] || attributes["_obj_class"]
- return attributes if type_computer.special_class?(self)
- attributes.merge("_obj_class" => self.to_s)
+ def prepare_attributes_for_instantiation(attributes)
+ attributes.with_indifferent_access.tap do |attributes|
+ prepare_obj_class_attribute(attributes) unless special_class?
+ end
end
- def descendants
- type_computer = TypeComputer.new(self, nil)
- Workspace.current.obj_classes.map(&:name)
- .sort
- .map { |obj_class_name| type_computer.compute_type(obj_class_name) }
- .compact
+ def extract_obj_class_from_attributes(attributes)
+ if special_class? && (obj_class_name = attributes[:_obj_class] || attributes['_obj_class'])
+ if obj_class = type_computer.compute_type_without_fallback(obj_class_name)
+ obj_class
+ else
+ raise ObjClassNotFound
+ end
+ end
end
#
# Defines an attribute.
#
- # @api private
+ # @api public
#
# In order to be able to persist model data in CMS you have to define its attributes.
# By defining an attribute you tell Scrivito under which name its value should be persisted,
# which type of content it will contain etc, which values are allowed etc.
#
@@ -281,17 +308,18 @@
# the attribute +title+. Inherited attributes can be overridden, e.g. +SpecialPage+ can override
# the inherited attribute +title+ by defining its own +title+ with a different type for example.
#
# @param [Symbol, String] name name of the attribute.
# @param [Symbol, String] type type of the attribute. Scrivito supports following types: +string+,
- # +html+, +enum+, +multienum+, +widget+, +reference+, +referencelist+ and +binary+.
+ # +html+, +enum+, +multienum+, +widgetlist+, +reference+, +referencelist+ and +binary+.
# @param [Hash] options definition options.
#
# @option options [Symbol, String] :values allowed values for types +enum+ and +multienum+.
# If no values are given for that types, then an empty array will be assumed.
#
# @return nil
+ # @raise [Scrivito::ScrivitoError] if the +type+ is unknown
#
# @example Defining attributes
# class Page < ::Obj
# attribute :my_string, :string
# attribute 'my_html', 'my_html'
@@ -304,72 +332,122 @@
#
# Page.attribute_definitions[:my_string]
# #=> #<Scrivito::AttributeDefinition>
#
# Page.attribute_definitions[:my_string].type
- # #=> :string
+ # #=> "string"
#
# Page.attribute_definitions[:my_html].type
- # #=> :html
+ # #=> "html"
#
# Page.attribute_definitions[:my_enum].type
- # #=> :enum
+ # #=> "enum"
# Page.attribute_definitions[:my_enum].values
# #=> ["a", "b", "c"]
#
# Page.attribute_definitions[:my_multienum].type
- # #=> :multienum
+ # #=> "multienum"
# Page.attribute_definitions[:my_multienum].values
# #=> []
#
# @example Inheriting attributes
# class Page < ::Obj
- # attributes :title, :string
+ # attribute :title, :string
# end
#
# class SpecialPage < Page
# end
#
# SpecialPage.attribute_definitions[:title].type
- # #=> :string
+ # #=> "string"
#
# @example Overriding inherited attributes
# class Page < ::Obj
- # attributes :title, :string
+ # attribute :title, :string
# end
#
# class SpecialPage < Page
# attribute :title, :html
# end
#
# Page.attribute_definitions[:title].type
- # #=> :string
+ # #=> "string"
#
# SpecialPage.attribute_definitions[:title].type
- # #=> :html
+ # #=> "html"
#
def attribute(name, type, options = {})
- name, type, options = name.to_sym, type.to_sym, options.with_indifferent_access
- own_attribute_definitions[name] = AttributeDefinition.new(name, type, options)
+ name, type, options = name, type, options
+ assert_valid_attribute_name(name.to_s)
+ assert_valid_attribute_type(type.to_s)
+ own_attribute_definitions[name.to_s] = AttributeDefinition.new(name, type, options)
nil
end
+ #
+ # This method determines the description that is shown in the UI
+ # and defaults to class name. It can be overriden by a custom value.
+ #
+ # @api public
+ #
+ def description_for_editor
+ name
+ end
+
+ #
+ # Returns the attribute definitions.
+ #
+ # @api public
+ # @see Scrivito::AttributeContent.attribute
+ # @return [Scrivito::AttributeDefinitionCollection]
+ #
def attribute_definitions
AttributeDefinitionCollection.new(all_attribute_definitions)
end
protected
+ def assert_valid_attribute_name(name)
+ if name.starts_with?('_')
+ raise ScrivitoError,
+ "Invalid attribute name '#{name}'. Only system attributes can start with an underscore."
+ end
+ end
+
+ def assert_valid_attribute_type(type)
+ raise ScrivitoError, "Unknown attribute type '#{type}'" unless ATTRIBUTE_TYPES.include?(type)
+ end
+
def all_attribute_definitions
if superclass.respond_to?(:all_attribute_definitions, true)
superclass.all_attribute_definitions.merge(own_attribute_definitions)
else
own_attribute_definitions
end
end
def own_attribute_definitions
@own_attribute_definitions ||= {}
+ end
+
+ private
+
+ def special_class?
+ type_computer.special_class?(self)
+ end
+
+ def prepare_obj_class_attribute(attributes)
+ if obj_class = attributes['_obj_class']
+ assert_valid_obj_class(obj_class)
+ else
+ attributes.merge!('_obj_class' => to_s)
+ end
+ end
+
+ def assert_valid_obj_class(obj_class)
+ unless obj_class == to_s
+ raise ScrivitoError, "Cannot set _obj_class to #{obj_class.inspect} when creating #{self}"
+ end
end
end
end
end