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