module Bulldog
module Attachment
#
# Abstract base class of None and Base.
#
class Maybe
def initialize(record, name, stream)
@record = record
@name = name
@stream = stream
@value = stream && stream.target
@saved = value.is_a?(SavedFile)
end
attr_reader :record, :name, :stream, :value
#
# Return true if the original file for this attachment has been
# saved.
#
def saved?
@saved
end
#
# Set the saved flag.
#
attr_writer :saved
#
# Return the reflection for the attachment.
#
def reflection
@reflection ||= record.attachment_reflection_for(name)
end
#
# Return true if this object wraps the same IO, and is in the
# same state as the given Attachment.
#
def ==(other)
record == other.record &&
name == other.name &&
value == other.value &&
saved? == other.saved?
end
#
# Set the attachment type for this class.
#
# This will register it as the type of attachment to use for the
# given attachment type.
#
def self.handle(type)
self.attachment_type = type
Attachment.types_to_classes[type] = self
end
class_inheritable_accessor :attachment_type
#
# Return the class' attachment type.
#
def type
self.class.attachment_type
end
#
# Load the attachment.
#
# Called just after initializing the attachment, or after a
# reload on the new attachment.
#
def load
storable_attributes.each do |name, storable_attribute|
if (column_name = reflection.column_name_for_stored_attribute(name))
value = storable_attribute.value_for(self, :original)
record.send("#{column_name}=", value)
end
end
end
#
# Unload the attachment.
#
# Called before a reload on the old attachment, or before a
# destroy.
#
def unload
storable_attributes.each do |name, storable_attribute|
if (column_name = reflection.column_name_for_stored_attribute(name))
record.send("#{column_name}=", nil)
end
if storable_attribute.memoize?
send("memoized_#{name}").clear
end
end
end
#
# Refresh the attachment. This includes anything read from the
# file, such as stored attributes, and dimensions.
#
# Use this to update the record if the underlying file has been
# modified outside of Bulldog. Note that the file has to be
# initialized from a saved file for this to work.
#
# Example:
#
# article = Article.create(:photo => photo_file)
# article = Article.find(article.id)
#
# # ... file is modified by an outside force ...
#
# article.photo.reload
# article.photo.dimensions # now reflects the new dimensions
#
def reload
unload
stream.reload
load
end
#
# Set the stored attributes in the attachment from the values in
# the record.
#
def read_storable_attributes
storable_attributes.each do |name, storable_attribute|
if (column_name = reflection.column_name_for_stored_attribute(name))
value = record.send(column_name)
value = send("deserialize_#{name}", value) if storable_attribute.cast
if storable_attribute.per_style?
ivar = :"@original_#{name}"
else
ivar = :"@#{name}"
end
instance_variable_set(ivar, value)
end
end
end
#
# Return the path that the given style would be stored at.
#
# Unlike #path, this is not affected by whether or not the
# record is saved. It may depend on the attachment value,
# however, as some interpolations may be derived from the value
# assigned to the attachment (e.g., :extension).
#
def interpolate_path(style_name, params={})
template = reflection.path_template
style = reflection.styles[style_name]
Interpolation.interpolate(template, record, name, style, params)
end
#
# Return the URL that the given style would be found at.
#
# Unlike #url, this is not affected by whether or not the record
# is saved. It may be depend on the attachment value, however,
# as some interpolations may be derived from the value assigned
# to the attachment (e.g., :extension).
#
def interpolate_url(style_name, params={})
template = reflection.url_template
style = reflection.styles[style_name]
Interpolation.interpolate(template, record, name, style, params)
end
protected # ---------------------------------------------------
#
# Declare the given attribute as storable via
# Bulldog::Reflection::Configuration#store_attributes.
#
# Options:
#
# * :per_style - the attribute has a different value
# for each style. The access method takes a style name as an
# argument to select which one to return, and defaults to the
# attribute's default_style. Only :original is stored, and
# loaded after reading.
#
# * :memoize - memoize the value.
#
# * :cast - run the value through a serialize method
# before storing it in the database, and an unserialize
# method before initializing the attribute from the raw
# database value. The methods must be called
# #serialize_ATTRIBUTE and #unserialize_ATTRIBUTE.
#
def self.storable_attribute(name, options={}, &block)
params = {
:name => name,
:callback => block || name,
}.merge(options)
storable_attributes[name] = StorableAttribute.new(params)
if options[:memoize]
if options[:per_style]
class_eval <<-EOS, __FILE__, __LINE__+1
def #{name}_with_memoization(style_name=nil)
style_name ||= reflection.default_style
memoized_#{name}[style_name] ||= #{name}_without_memoization(style_name)
end
def memoized_#{name}
@memoized_#{name} ||= {}
end
EOS
else
class_eval <<-EOS, __FILE__, __LINE__+1
def #{name}_with_memoization
@#{name} ||= #{name}_without_memoization
end
EOS
end
alias_method_chain name, :memoization
end
end
#
# The list of storable attributes for this class.
#
class_inheritable_accessor :storable_attributes
self.storable_attributes = {}
class StorableAttribute
def initialize(attributes)
attributes.each do |name, value|
send("#{name}=", value)
end
end
def value_for(attachment, style_name)
value =
if callback.is_a?(Proc)
callback.call(attachment)
else
if per_style?
attachment.send(callback, style_name)
else
attachment.send(callback)
end
end
value = attachment.send("serialize_#{name}", value) if cast
value
end
attr_accessor :name, :callback, :per_style, :memoize, :cast
alias cast? cast
alias per_style? per_style
alias memoize? memoize
end
end
end
end