# encoding: UTF-8 module Spontaneous::Prototypes # FieldPrototype represents the class-level view of a type field. # It contains information on the type of the field and the options # passed in the type declaration and is responsible for transforming # serialized field data from the db into a field instance. # # options - A hash containing options that control the behaviour of the # field (default: {}) # # :default - The default value for new fields. This accepts either a # value (which is either a String or responds to #to_s) # or a Proc value generator which can accept 1 argument # that is the instance that the field is attached to. # :title - The title that should be used to label the field in the UI. # This defaults to the 'titleized' version of the field name, # e.g. ':field_name' becomes 'Field Name'. # :comment - An optional String comment to be displayed in the UI # (default: ""). # :list - A Boolean flag determining whether to show the field in the # list view (default: true). # :fallback - Provides a way of supplying a fallback value for an empty # field. # # Other options are dependent on the type of field. # # Examples # # Pass a Proc as the default value for a field: # # field :title, default: proc { |page| "This is page #{page.slug}" } # # Assign a field with a fallback: # # class Something < Piece # field :a # field :b, fallback: :a # end # # instance = Something.new(a: "The value of A") # instance.a.value #=> "The value of A" # instance.b.value #=> "The value of A" # instance.b = "Now B" # instance.b.value #=> "Now B" # class FieldPrototype attr_reader :owner, :name, :options def initialize(owner, name, type, options={}, blocks = [], &block) @owner = owner @name = name @extend = [blocks].flatten.push(block).compact # if the type is nil then try the name, this will assign sensible defaults # to fields like 'image' or 'date' @base_class = Spontaneous::Field[type || name] parse_options(@base_class, options) field_class_name = "#{name.to_s.camelize}Field" owner.const_set(field_class_name, instance_class) self end def schema_name "field/#{owner.schema_id}/#{name}" end def schema_id Spontaneous.schema.uids[@_inherited_schema_id] || Spontaneous.schema.to_id(self) end def schema_owner owner end def owner_sid schema_owner.schema_id end # alias_method :id, :schema_id def title(new_title=nil) self.title = new_title if new_title @title || @options[:title] || default_title end def title=(new_title) @title = new_title end def default_title @name.to_s.titleize end def parse_options(field_class, options) @options = default_options(field_class).merge(options) end def default_options(field_class) {default: '', comment: false, list: true}.merge(field_class.default_options) end def instance_class @_instance_class ||= create_instance_class end def create_instance_class base_class = @base_class Class.new(@base_class).tap do |instance_class| # although we're subclassing the base field class, we don't want the ui # to use a different editor. FieldClass::editor_class is used in the serialisation # routine # instance_class.singleton_class.send(:define_method, :editor_class) do # base_class.editor_class # end @extend.each { |block| instance_class.class_eval(&block) if block } instance_class.prototype = self end end def field_class instance_class end def default(instance = nil) case (default = @options[:default]) when Proc default[instance] else default end end def dynamic_default? @options[:default].is_a?(Proc) end def comment @options[:comment] end def fallback @options[:fallback] end # default read level is None, i.e. every logged in user can read the field def read_level level_name = @options[:read_level] || @options[:user_level] || :none Spontaneous::Permissions[level_name] end # default write level is the first level above None def write_level level_name = @options[:write_level] || @options[:user_level] || Spontaneous::Permissions::UserLevel.minimum.to_sym Spontaneous::Permissions[level_name] end def in_index?(index) search(index.site).in_index?(index) end def index_id(index) search(index.site).index_id(index) end def options_for_index(index) search(index.site).field_definition(index) end # TODO: it's wrong to have to be passing the site to this call # as there's only ever one site and we shouldn't be memoizing # a method call with a param. # Must centralize the testing of a prototype for inclusion into # an index - either into the index or the site itself. # We can't just recalculate this on the fly because indexing # needs to be reasonably performant. def search(site) @search ||= S::Search::Field.new(site, self, @options[:index]) end def inherit_schema_id(schema_id) @_inherited_schema_id = schema_id.to_s end def merge(subclass_owner, field_type, subclass_options, &subclass_block) options = @options.merge(subclass_options) self.class.new(subclass_owner, name, field_type, options, @extend, &subclass_block).tap do |prototype| prototype.inherit_schema_id self.schema_id end end def to_field(instance, database_values=nil) using_default_values = database_values.nil? values = { :name => self.name } values[:unprocessed_value] = default(instance) if using_default_values values.update(database_values || {}) field = self.instance_class.new(values, using_default_values) field.prototype = self field end def export(user) { name: name.to_s, schema_id: schema_id.to_s, type: instance_class.editor_class, title: title, comment: comment || "", list: @options[:list] || false, writable: Spontaneous::Permissions.has_level?(user, write_level) }.merge(instance_class.export(user)) end end end