require 'json'
require 'ostruct'
require 'kvom'
module RailsConnector
# The CMS file class
# @api public
class Obj
extend ActiveModel::Naming
include Kvom::ModelIdentity
include DateAttribute
include SEO
# Create a new Obj instance with the given values and attributes.
# Normally this method should not be used.
# Instead Objs should be loaded from the cms database.
def initialize(obj_data = {})
if !obj_data.respond_to?(:value_and_type_of)
obj_data = ObjDataFromHash.new(obj_data)
end
update_data(obj_data)
end
# instantiate an Obj instance from obj_data.
# May result in an instance of a subclass of Obj according to STI rules.
def self.instantiate(obj_data)
obj_class = obj_data.value_of("_obj_class")
Obj.compute_type(obj_class).new(obj_data)
end
# @api public
def id
read_attribute('_id')
end
### FINDERS ####################
# Find an Obj by it's id.
# If the paremeter is an Array containing ids, return a list of corresponding Objs.
# @api public
def self.find(id_or_list)
case id_or_list
when Array
find_objs_by(:id, id_or_list).map(&:first)
else
obj = find_objs_by(:id, [id_or_list.to_s]).first.first
obj or raise ResourceNotFound, "Could not find #{self} with id #{id_or_list}"
end
end
# Returns a {ObjSearchEnumerator} of all Objs.
# If invoked on a subclass of Obj, the result will be restricted to Obj of that subclass.
def self.all
if self.name == 'RailsConnector::Obj'
ObjSearchEnumerator.new(nil)
else
find_all_by_obj_class(self.name)
end
end
# Returns an {ObjSearchEnumerator} of all Objs with the given obj_class.
def self.find_all_by_obj_class(obj_class)
ObjSearchEnumerator.new([{:field => '_obj_class', :operator => 'equal', :value => obj_class}])
end
# Find the Obj with the given path.
# Returns nil if no matching Obj exists.
# @api public
def self.find_by_path(path)
find_objs_by(:path, [path]).first.first
end
def self.find_many_by_paths(pathes)
find_objs_by(:path, pathes).map(&:first)
end
# Find an Obj with the given name.
# If several Objs exist with the given name, one of them is chosen and returned.
# If no Obj with the name exits, nil is returned.
def self.find_by_name(name)
enum = ObjSearchEnumerator.new(
[{:field => '_name', :operator => 'equal', :value => name}], {:batch_size => 1})
enum.first
end
# Returns an {ObjSearchEnumerator} of all Objs with the given name.
def self.find_all_by_name(name)
ObjSearchEnumerator.new([{:field => '_name', :operator => 'equal', :value => name}])
end
# Return the Obj with the given permalink or nil if no matching Obj exists.
# @api public
def self.find_by_permalink(permalink)
find_objs_by(:permalink, [permalink]).first.first
end
# Return the Obj with the given permalink or raise ResourceNotFound if no matching Obj exists.
# @api public
def self.find_by_permalink!(permalink)
find_by_permalink(permalink) or
raise ResourceNotFound, "Could not find #{self} with permalink '#{permalink}'"
end
# accepts the name of an "obj_by" - view and a list of keys.
# returns a list of lists of Objs: a list of Objs for each given keys.
def self.find_objs_by(view, keys)
CmsBackend.find_obj_data_by(Workspace.current.data, view, keys).map do |list|
list.map { |obj_data| Obj.instantiate(obj_data) }
end
end
def to_param
id
end
def self.configure_for_content(mode)
# this method exists only for compatibility with the fiona connector.
end
# A CMS administrator can specify the obj_class for a given CMS object.
# In Rails, this could be either:
#
# * A valid and existing model name
# * A valid and non-existing model name
# * An invalid model name
#
# Rails' STI mechanism only considers the first case.
# In any other case, RailsConnector::Obj is used, except when explicitly asked
# for a model in the RailsConnector namespace (RailsConnector::Permission etc.)
def self.compute_type(type_name)
@compute_type_cache ||= {}
@compute_type_cache [type_name] ||= try_type { type_name.constantize } || self
end
# return the Obj that is the parent of this Obj.
# returns nil for the root Obj.
# @api public
def parent
root? ? nil : Obj.find_by_path(parent_path)
end
# Returns an Array of all the ancestor objects, starting at the root and ending at this object's parent.
# @api public
def ancestors
return [] if root?
ancestor_paths = parent_path.scan(/\/[^\/]+/).inject([""]) do |list, component|
list << list.last + component
end
ancestor_paths[0] = "/"
Obj.find_many_by_paths(ancestor_paths)
end
# return a list of all child Objs.
# @api public
def children
Obj.find_objs_by(:ppath, [path]).first
end
### ATTRIBUTES #################
# returns the Obj's path as a String.
# @api public
def path
read_attribute('_path') or raise "Obj without path"
end
# returns the Obj's name, i.e. the last component of the path.
# @api public
def name
if root?
""
else
path.match(/[^\/]+$/)[0]
end
end
def permissions
# FIXME permissions
@permissions ||= OpenStruct.new({
:live => permitted_groups,
:read => [],
:write => [],
:root => [],
:create_children => [],
})
end
def permitted_for_user?(user)
if permitted_groups.blank?
true
else
if user
(permitted_groups & user.live_server_groups).any?
else
false
end
end
end
# Returns the root Obj, i.e. the Obj with the path "/"
# @api public
def self.root
Obj.find_by_path("/") or raise ResourceNotFound, "Obj.root not found: There is no Obj with path '/'."
end
# Returns the homepage object. This can be overwritten in your application's +ObjExtensions+.
# Use Obj#homepage? to check if an object is the homepage.
# @api public
def self.homepage
root
end
# returns the obj's permalink.
# @api public
def permalink
read_attribute('_permalink')
end
# This method determines the controller that should be invoked when the Obj is requested.
# By default a controller matching the Obj's obj_class will be used.
# If the controller does not exist, the CmsController will be used as a fallback.
# Overwrite this method to force a different controller to be used.
# @api public
def controller_name
obj_class
end
# This method determines the action that should be invoked when the Obj is requested.
# The default action is 'index'.
# Overwrite this method to force a different action to be used.
# @api public
def controller_action_name
"index"
end
# Returns true if the current object is the homepage object.
# @api public
def homepage?
self == self.class.homepage
end
# Returns the title of the content or the name.
# @api public
def display_title
self.title || name
end
# @api public
def title
read_attribute('title')
end
# Returns the type of the object: :document, :publication, :image or :generic
def object_type
read_attribute('_obj_type').to_sym
end
# Returns true if image? or generic?
def binary?
[:image, :generic].include? object_type
end
# Returns true if object_type == :image
def image?
object_type == :image
end
# Returns true if object_type == :generic
def generic?
object_type == :generic
end
# Returns true if object_type == :publication (for folders)
def publication?
object_type == :publication
end
# Returns true if object_type == :document
def document?
object_type == :document
end
# Returns true if this object is active (time_when is in object's time interval)
# @api public
def active?(time_when = nil)
return false unless valid_from
time_then = time_when || Obj.preview_time
valid_from <= time_then && (!valid_until || time_then <= valid_until)
end
# compatibility with legacy apps.
def suppress_export
suppressed? ? 1 : 0
end
# Returns true if the Obj is suppressed.
# A suppressed Obj does not represent an entire web page, but only a part of a page
# (for example a teaser) and will not be delivered by the rails application
# as a standalone web page.
def suppressed?
read_attribute('_suppress_export') ? true : false
end
# Returns true if the export of the object is not suppressed and the content is active?
def exportable?(current_time = nil)
!suppressed? && active?(current_time)
end
# Returns the file name to which the Content.file_extension has been appended.
def filename
Rails.logger.warn(
"DEPRECATION WARNING: "\
"The Method Obj#filename is no longer supported. Please use Obj#name instead. "\
"From: #{caller[0]}"
)
name
end
# Returns an array with the names of groups that are permitted to access this Obj.
# This corresponds to the cms permission "permissionLiveServerRead".
def permitted_groups
# FIXME permissions not yet implemented in fiona 7
[]
end
# Returns true if this object is the root object.
# @api public
def root?
path == "/"
end
# Returns a list of exportable? children excluding the binary? ones unless :all is specfied.
# This is mainly used for navigations.
# @api public
def toclist(*args)
return [] unless publication?
time = args.detect {|value| value.kind_of? Time}
toclist = children.select{ |toc| toc.exportable?(time) }
toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all)
toclist
end
# Returns the sorted +toclist+, respecting sort order and type of this Obj.
# @api public
def sorted_toclist(*args)
list = self.toclist(*args)
return [] if list.blank?
cached_sort_key1 = self.sort_key1
cached_sort_type1 = self.sort_type1
sorted_list =
if cached_sort_key1.blank?
list.sort { |left_obj, right_obj| left_obj.name <=> right_obj.name }
else
cached_sort_key2 = self.sort_key2
cached_sort_type2 = self.sort_type2
cached_sort_key3 = self.sort_key3
cached_sort_type3 = self.sort_type3
list.sort do |left_obj, right_obj|
compare = compare_on_sort_key(left_obj, right_obj, cached_sort_key1, cached_sort_type1)
if compare == 0 && cached_sort_key2
compare = compare_on_sort_key(left_obj, right_obj, cached_sort_key2, cached_sort_type2)
if compare == 0 && cached_sort_key3
compare = compare_on_sort_key(left_obj, right_obj, cached_sort_key3, cached_sort_type3)
end
end
compare
end
end
return self.sort_order == "descending" ? sorted_list.reverse : sorted_list
end
def sort_order
read_attribute('_sort_order') == 1 ? "descending" : "ascending"
end
def sort_type1
converted_sort_type('_sort_type1')
end
def sort_type2
converted_sort_type('_sort_type2')
end
def sort_type3
converted_sort_type('_sort_type3')
end
def sort_key1
read_attribute('_sort_key1')
end
def sort_key2
read_attribute('_sort_key2')
end
def sort_key3
read_attribute('_sort_key3')
end
# Returns the Object with the given name next in the hierarchy
# returns nil if no object with the given name was found.
# @api public
def find_nearest(name)
obj = self.class.find_by_path(root? ? "/#{name}" : "#{path}/#{name}")
return obj if obj and obj.active?
parent.find_nearest(name) unless self.root?
end
# This should be a SET, because it's faster in this particular case.
OLD_INTERNAL_KEYS = Set.new(%w[
body
id
last_changed
name
obj_class
obj_type
object_type
path
permalink
sort_key1
sort_key2
sort_key3
sort_order
sort_type1
sort_type2
sort_type3
suppress_export
text_links
title
valid_from
valid_until
])
# Returns the value of an internal or external 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
if OLD_INTERNAL_KEYS.include?(key)
send(key)
elsif key.start_with?('_') && OLD_INTERNAL_KEYS.include?(internal_key = key[1..-1])
# For backwards compatibility reasons
send(internal_key)
elsif has_attribute?(key)
read_attribute(key)
else
nil
end
end
# Reloads the attributes of this object from the database.
# Notice that the ruby class of this Obj instance will NOT change,
# even if the obj_class in the database has changed.
# @api public
def reload
obj_data = CmsBackend.find_obj_data_by(Workspace.current.data, :id, [id.to_s]).first.first
update_data(obj_data)
end
def text_links
read_attribute('_text_links')
end
# @api public
def obj_class
read_attribute('_obj_class')
end
# @api public
def last_changed
read_attribute('_last_changed')
end
# @api public
def valid_from
read_attribute('_valid_from')
end
# @api public
def valid_until
read_attribute('_valid_until')
end
# For a binary Obj, the content_type is equal to the content_type of it's body (i.e. it's data).
# For non-binary Objs, a the default content_type is "text/html".
# Override this method in subclasses to define a different content_type.
# Note that only Objs with content_type "text/html"
# will be rendered with layout and templates by the DefaultCmsController.
# @api public
def content_type
if binary?
body_content_type
else
"text/html"
end
end
alias mime_type content_type
# returns the extension (the part after the last dot) from the Obj's name.
# returns an empty string if no extension is present in the Obj's name.
# @api public
def file_extension
File.extname(name)[1..-1] || ""
end
# Returns the body (main content) of the Obj for non-binary Objs.
# Returns nil for binary Objs.
# @api public
def body
if binary?
nil
else
StringTagging.tag_as_html(read_attribute('body'), self)
end
end
# for binary Objs body_length equals the file size
# for non-binary Objs body_length equals the number of characters in the body (main content)
# @api public
def body_length
if binary?
blob = find_blob
blob ? blob.length : 0
else
(body || "").length
end
end
# returns an URL to retrieve the Obj's body for binary Objs.
# returns nil for non-binary Objs.
# @api public
def body_data_url
if binary?
blob = find_blob
blob.url if blob
end
end
def body_data_path
# not needed/supported when using cloud connector.
nil
end
# returns the content type of the Obj's body for binary Objs.
# returns nil for non-binary Objs.
# @api public
def body_content_type
if binary?
blob = find_blob
if blob
blob.content_type
else
"application/octet-stream"
end
end
end
def to_liquid
LiquidSupport::ObjDrop.new(self)
end
def respond_to?(method_id, include_private=false)
if has_attribute?(method_id)
true
else
super
end
end
def self.preview_time=(t)
Thread.current[:preview_time] = t
end
def self.preview_time
Thread.current[:preview_time] || Time.now
end
def inspect
"<#{self.class} id=\"#{id}\" path=\"#{path}\">"
end
def has_attribute?(name)
data_from_cms.has_custom_attribute?(name.to_s)
end
private
attr_accessor :data_from_cms
def update_data(data)
self.data_from_cms = data
@attribute_cache = {}
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)
end
end
def prepare_attribute_value(attribute_value, attribute_type)
case attribute_type
when "markdown"
StringTagging.tag_as_markdown(attribute_value, self)
when "html"
StringTagging.tag_as_html(attribute_value, self)
when "date"
DateAttribute.parse(attribute_value) if attribute_value
when "linklist"
LinkList.new(attribute_value)
else
attribute_value
end
end
def parent_path
raise "parent_path called for root" if root?
path.gsub(/\/[^\/]+$/, "").presence || "/"
end
def as_date(value)
DateAttribute.parse(value) unless value.nil?
end
def find_blob
blob_spec = read_attribute('blob')
Blob.find(blob_spec["id"]) if blob_spec
end
def method_missing(method_name, *args)
if has_attribute?(method_name)
read_attribute(method_name.to_s)
else
super
end
end
def compare_on_sort_key(left_obj, right_obj, my_sort_key, my_sort_type)
left_value = left_obj[my_sort_key]
right_value = right_obj[my_sort_key]
if left_value.nil?
1
elsif right_value.nil?
-1
# hardcoded type check needed for speed
elsif left_value.is_a?(Time) && right_value.is_a?(Time)
left_value <=> right_value
else
if my_sort_type == "numeric"
(left_value.to_i rescue 0) <=> (right_value.to_i rescue 0)
else
left_value.to_s.downcase <=> right_value.to_s.downcase
end
end
end
def converted_sort_type(attribute)
read_attribute(attribute) == 1 ? "numeric" : "alphaNumeric"
end
class << self
private
def try_type
result = yield
result if subclass_of_obj(result)
rescue NameError
nil
end
def subclass_of_obj(klass)
if klass == Obj
true
else
klass.superclass && subclass_of_obj(klass.superclass)
end
end
end
end
end