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 delegate :attribute_definitions, to: :class def has_attribute?(attribute_name) has_public_system_attribute?(attribute_name) || has_custom_attribute?(attribute_name) end 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 read_attribute(attribute_name) @attribute_cache.fetch(attribute_name) do @attribute_cache[attribute_name] = value_of_attribute(attribute_name) end end def type_of_attribute(attribute_name) type_of_system_attribute(attribute_name) || attribute_definitions[attribute_name].try(:type) end def respond_to?(method_id, include_private = false) has_custom_attribute?(method_id) || super end 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 # @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 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. # # @api public # @param [String] field_name Name of the widget field. # @return [nil, Array] # 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 elsif deleted?(revision) Modification::DELETED else cms_data_in_revision = cms_data_for_revision(revision) if cms_data_in_revision 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 # # 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 # @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 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 def data_from_cms if @data_from_cms.respond_to?(:call) @data_from_cms = @data_from_cms.call else @data_from_cms end end private attr_writer :data_from_cms 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) 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 end 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 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 attribute_deserializer @attribute_deserializer ||= AttributeDeserializer.new(self, workspace) end 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 prepare_attributes_for_instantiation(attributes) attributes.with_indifferent_access.tap do |attributes| prepare_obj_class_attribute(attributes) unless special_class? end end 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 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. # # Attributes are inherited, e.g. if a model +Page+ defines an attribute +title+ of type +string+ # and a model +SpecialPage+ inherits from +Page+, then the model +SpecialPage+ will also have # 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+, +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' # attribute :my_enum, :enum, values: %w[a b c] # attribute :my_multienum, :multienum # end # # Page.attribute_definitions # #=> # # # Page.attribute_definitions[:my_string] # #=> # # # Page.attribute_definitions[:my_string].type # #=> "string" # # Page.attribute_definitions[:my_html].type # #=> "html" # # Page.attribute_definitions[:my_enum].type # #=> "enum" # Page.attribute_definitions[:my_enum].values # #=> ["a", "b", "c"] # # Page.attribute_definitions[:my_multienum].type # #=> "multienum" # Page.attribute_definitions[:my_multienum].values # #=> [] # # @example Inheriting attributes # class Page < ::Obj # attribute :title, :string # end # # class SpecialPage < Page # end # # SpecialPage.attribute_definitions[:title].type # #=> "string" # # @example Overriding inherited attributes # class Page < ::Obj # attribute :title, :string # end # # class SpecialPage < Page # attribute :title, :html # end # # Page.attribute_definitions[:title].type # #=> "string" # # SpecialPage.attribute_definitions[:title].type # #=> "html" # def attribute(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