lib/spontaneous/box.rb
# encoding: UTF-8
module Spontaneous
class Box
include Enumerable
include Spontaneous::Model::Core::SchemaHierarchy
include Spontaneous::Model::Core::Fields
include Spontaneous::Model::Core::Styles
include Spontaneous::Model::Core::Serialisation
include Spontaneous::Model::Core::Render
include Spontaneous::Model::Box::Comment
include Spontaneous::Model::Box::AllowedTypes
include Spontaneous::Model::Core::Permissions
include Spontaneous::Model::Core::Media
include Spontaneous::Model::Core::ContentHash::BoxMethods
# use underscores to protect against field name conflicts
attr_reader :_name, :_prototype, :owner
attr_accessor :template_params
# Public: the parent of a Box is the same as its owner,
# i.e. the Content object that contains it.
#
# Returns: the owning Content object
alias_method :parent, :owner
class << self
attr_reader :mapper
end
def self.page?
false
end
def self.is_box?
true
end
# Used in the instance that a subclass is re-opening a box definition
# In that case the box prototype is created by a BoxPrototype#merge
# call and at that point we force the box instance class to use the same
# schema id as its parent so that content is always connected to the originating
# definition in the supertype rather than the customised version in the subclass
def self.schema_id=(schema_id)
@schema_id = schema_id
end
def self.schema_id
mapper.schema.uids[@schema_id] || mapper.schema.to_id(self)
end
# This is overridden by anonymous classes defined by box prototypes
# See BoxPrototype#create_instance_class
def self.schema_name
Spontaneous::Schema.schema_name('type', nil, name)
end
def self.supertype
if self == Spontaneous::Box
nil
else
superclass
end
end
def self.supertype?
!supertype.nil?
end
def self.owner_sid
nil
end
def initialize(name, prototype, owner)
@_name, @_prototype, @owner = name.to_sym, prototype, owner
@field_initialization = false
end
def model
@owner.model
end
def dataset
unordered_dataset.order(Sequel.asc(:box_position))
end
def unordered_dataset
@owner.model.where!(owner_id: @owner.id, box_sid: schema_id)
end
# All renderable objects must implement #target to enable aliases & content objects
# to be treated identically
def target
self
end
def renderable
self
end
def render(format = :html, params = {}, parent_context = nil)
render_inline(format, params, parent_context)
end
def render_using(renderer, format = :html, params = {}, parent_context = nil)
render_inline_using(renderer, format, params, parent_context)
end
def page?
false
end
alias_method :is_page?, :page?
def is_box?
true
end
def schema_id
self.class.schema_id
end
# A boxes "identity" must be a combination of its owner's id and
# its schema_id.
def id
[owner.id, schema_id.to_s].join("/")
end
def schema_name
_name.to_s
end
def owner_sid
nil
end
def schema_owner
nil
end
def formats
owner.formats
end
def media_id
"#{owner.padded_id}/#{schema_id}".freeze
end
def position
_prototype.position
end
def box_name
_name
end
def label
_name.to_s
end
def reload
owner.reload
end
def reload_box
mapper.clear_cache(scope_cache_key)
@field_store = nil
end
# needed by Render::Context
def box?(box_name)
false
end
def field_store
@field_store ||= (owner.box_field_store(self) || initialize_fields)
end
# don't like this
def initialize_fields
field_store = nil
if default_values = _prototype.field_defaults
field_store = []
default_values.each do |field_name, value|
if self.field?(field_name)
field = self.class.field_prototypes[field_name].to_field(self)
field.value = value
field_store << field.serialize_db
end
end
end
field_store
end
def field_modified!(modified_field = nil)
save_fields!
end
def save_fields(fields = nil)
save_fields!(fields)
save
end
# Use @serialized_fields to temporarily overwrite the value of
# #serialized_fields because this call may be coming from an async
# process that only wants to update a subset of the field values
# and because we don't have direct access to the serialization
# store we have to control our serialization output.
# TODO: Make boxes responsible for directly writing their serialized
# form
def save_fields!(fields = nil)
@modified = true
@serialized_fields = update_serialized_fields(fields)
owner.box_modified!(self)
@serialized_fields = nil
end
def serialize_db
{ box_id: schema_id.to_s, fields: serialized_fields }
end
def serialized_fields
@serialized_fields || fields.serialize_db
end
def self.resolve_style(box)
Spontaneous::BoxStyle.new(box)
end
def self.style_class
Spontaneous::BoxStyle
end
def style
resolve_style(self)
end
# Container represents the object one level up from us,
# which in this case is the parent Content instance.
def container
owner
end
def content_instance
owner
end
# A pointer to the containing page. This may not be the same as the
# `owner` of the box in the case where the box is owned by a Piece.
def page
owner.page
end
# A convenience method to return the root of the page tree
def root
page.root
end
def site
owner.site
end
# Used to determine the page to use to define the path of any
# contained pages.
#
# Overwrite this to set up custom paths for pages.
#
# It can either return a page instance (in which case
# child pages will be based on the #path of the returned
# instance) or a string (which will form the root of the
# generated paths:
#
# Page.box :sections
#
# Page.box :custom do
# def path_origin
# root
# end
# end
#
# home = Page.create # set up a new site homepage
# section = Page.create(slug: 'a-section')
# home.sections << section # add section page to the root
# section.custom.path_origin.path #=> "/"
#
# child = Page.create(slug: 'child')
# section.custom << child
# child.path #=> "/child" # this would normally be "/a-section/child"
#
def path_origin
page
end
# This is used by new pages to generate the path component of the container.
def path!
case (origin = path_origin)
when @owner.content_model
origin.path!
else
origin.to_s
end
end
alias_method :to_page, :page
def depth
owner.content_depth
end
def adopt(content, index = -1)
insert(index, content)
content.save
# kinda feel like this should be dealt with internally by the page
# but don't care enough to start messing with the path propagation
# methods...
content.propagate_path_changes if content.is_page?
end
def push(content)
insert(-1, content)
end
alias_method :<<, :push
def insert(index, content)
owner.save if owner.new?
@modified = true
inserted = contents.insert(index, content)
content.after_insertion
owner.save_after_insertion(content)
inserted
rescue RuntimeError
raise Spontaneous::ReadOnlyScopeModificationError.new(self)
end
def insert_after(entry, content)
entry_id = case entry
when Fixnum
entry
when String
Integer(entry)
when model.content_model, entry.respond_to?(:id)
entry.id
else
entry.to_i
end
index = contents.index { |e| e.id == entry_id }
insert(index + 1, content)
end
def set_position(content, new_position)
@modified = true
contents.set_position(content, new_position)
end
def modified?
@modified
end
# Returns a list of the ids of the content within the box
#
# This is designed to be fast by not requiring the actual
# loading of the box contents.
def ids
contents.ids
end
def contents
return [] if owner.new?
mapper.with_cache(scope_cache_key) { read_only(contents!) }
end
def read_only(contents)
contents.freeze if model.visible_only?
contents
end
# If you want to over-ride a box with a custom contents array then
# re-define this method, not #contents above.
def contents!
Spontaneous::Collections::BoxContents.new(self)
end
# Called by BoxContents instances to actually load the box contents. This
# allows for a single request to load the contents of all an item's boxes.
def load_contents
owner.box_contents(self)
end
def scope_cache_key
@scope_cache_key ||= ['box', owner.id, schema_id.to_s].join(':').freeze
end
def mapper
model.mapper
end
def pieces
contents.select { |e| e.is_a?(Spontaneous::Model::Piece) }
end
def [](index)
contents[index]
end
def index(entry)
contents.index(entry)
end
def wrap_page(page)
contents.wrap_page(page)
end
def each
return enum_for(:each) unless block_given?
contents.each(&Proc.new)
end
def clear
clear!
end
def clear!
contents.each do |content|
content.destroy(false)
end
contents.clear
end
def destroy(origin)
each do |content|
content.destroy(false, origin)
end
mapper.clear_cache(scope_cache_key)
end
def empty?
contents.empty?
end
def last
contents.last
end
def length
contents.length
end
alias_method :size, :length
def iterable
contents
end
# An implementation of the Array#sample method
def sample(n = 1)
contents.sample(n = 1)
end
# An implementation of the Array#sample method that
# doesn't load the entire box contents
def sample!
contents.sample!
end
def content_destroyed(content)
contents.content_destroyed(content)
rescue RuntimeError => e
raise Spontaneous::ReadOnlyScopeModificationError.new(self)
end
def export(user = nil)
shallow_export(user).merge({
entries: contents.map { |p| p.export(user) }
})
end
def shallow_export(user)
{
name: _prototype.name.to_s,
id: _prototype.schema_id.to_s,
fields: self.class.readable_fields(user).map { |name| fields[name].export(user) }
}
end
def alias_export(user)
{ name: _prototype.name.to_s, type:self.class.ui_class, type_id: _prototype.schema_id.to_s }
end
# only called directly after saving a boxes fields so
# we don't need to return the entries
def serialise_http(user)
Spontaneous.serialise_http(shallow_export(user))
end
def writable?(user, content_type = nil)
return true if Spontaneous::Permissions.has_level?(user, Spontaneous::Permissions.root)
box_writable = self.owner.box_writable?(user, _name)
if content_type
allowed = self.allowed_type(content_type)
box_writable && allowed && allowed.addable?(user)
else
box_writable
end
end
def readable?(user)
self.owner.box_readable?(user, _name)
end
def start_inline_edit_marker
"spontaneous:previewedit:start:box id:#{schema_id}"
end
def end_inline_edit_marker
"spontaneous:previewedit:end:box id:#{schema_id}"
end
def save
owner.save
end
def ==(obj)
super or (obj.is_a?(Box) && (self._prototype == obj._prototype) && (self.owner == obj.owner))
end
def to_a
contents.dup
end
# It would seem obvious to return the same value as #to_a here but if we
# do that then any list of boxes that is then flattened will transform
# into a list of box contents, which isn't really what you’d expect
def to_ary
nil
end
def respond_to_missing?(method_name, include_private = false)
contents.respond_to?(method_name, include_private)
end
def method_missing(method_name, *args)
if block_given?
contents.send(method_name, *args, &Proc.new)
else
contents.send(method_name, *args)
end
end
end
end