require 'json'
require 'ostruct'
require 'kvom'
module RailsConnector
# The CMS file class
class Obj
extend ActiveModel::Naming
include Kvom::ModelIdentity
include DateAttribute
include StringTagging
include SEO
include ObjBody
extend PathConversion
include PathConversion
# 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(values = {}, meta = {})
update_data(values, meta)
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) # :nodoc:
obj_class = obj_data["values"]["_obj_class"]
Obj.compute_type(obj_class).new(
obj_data["values"], obj_data
)
end
def id
read_raw_attribute_value('_id')
end
### FINDERS ####################
# Find an Obj by it's id.
# If the paremeter is an Array containing ids, return a list of corresponding Objs.
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
# (notice: not yet implemented)
# Returns a list of all Objs.
# If invoked on a subclass of Obj, the result will be restricted to Obj of that subclass.
def self.all # :nodoc:
raise "not yet implemented!"
end
# (notice: not yet implemented)
# returns an Array of all Objs with the given obj_class.
def self.find_all_by_obj_class(obj_class) # :nodoc:
raise "not yet implemented!"
end
# Find the Obj with the given path.
# Returns nil if no matching Obj exists.
def self.find_by_path(path)
find_objs_by(:path, [path]).first.first
end
def self.find_by_path_list(path_list) # :nodoc:
find_by_path(path_from_list(path_list))
end
def self.find_many_by_paths(pathes) # :nodoc:
find_objs_by(:path, pathes).map(&:first)
end
# (notice: not yet implemented)
# 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) # :nodoc:
raise "not yet implemented!"
end
# (notice: not yet implemented)
# Find all Objs with the given name.
def self.find_all_by_name(name) # :nodoc:
raise "not yet implemented!"
end
# Return the Obj with the given permalink or nil if no matching Obj exists.
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.
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) # :nodoc:
Revision.current.find_obj_data_by(view, keys).map do |list|
list.map { |obj_data| Obj.instantiate(obj_data) }
end
end
def to_param # :nodoc:
id
end
def self.configure_for_content(mode) # :nodoc:
# 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 explicitely asked
# for a model in the RailsConnector namespace (RailsConnector::Permission etc.)
def self.compute_type(type_name) # :nodoc:
@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.
def parent
root? ? nil : Obj.find_by_path_list(path_list[0..-2])
end
# Returns an Array of all the ancestor objects, starting at the root and ending at this object's parent.
def ancestors
return [] if root?
ancestor_paths = path_list[0..-2].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.
def children
Obj.find_objs_by(:ppath, [path]).first
end
### ATTRIBUTES #################
# returns the Obj's path as a String.
def path
path_from_list(path_list)
end
def path_list # :nodoc:
read_attribute_value('_path') || []
end
# returns the Obj's name, i.e. the last component of the path.
def name
path_list.last || ""
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 "/"
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.
def self.homepage
root
end
# returns the obj's permalink.
def permalink
read_attribute_value('_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.
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.
def controller_action_name
"index"
end
# Returns true if the current object is the homepage object.
def homepage?
self == self.class.homepage
end
# Returns the title of the content or the name.
def display_title
self.title || name
end
def title
read_attribute_value('title')
end
# Returns the type of the object: :document, :publication, :image or :generic
def object_type
read_attribute_value('_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)
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 # :nodoc:
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_raw_attribute_value('_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.
def root?
path_list.empty?
end
# Returns a list of exportable? children excluding the binary? ones unless :all is specfied.
# This is mainly used for navigations.
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.
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 # :nodoc:
read_attribute_value('_sort_order') == 1 ? "descending" : "ascending"
end
def sort_type1 # :nodoc:
converted_sort_type('_sort_type1')
end
def sort_type2 # :nodoc:
converted_sort_type('_sort_type2')
end
def sort_type3 # :nodoc:
converted_sort_type('_sort_type3')
end
def sort_key1 # :nodoc:
converted_sort_key('_sort_key1')
end
def sort_key2 # :nodoc:
converted_sort_key('_sort_key2')
end
def sort_key3 # :nodoc:
converted_sort_key('_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.
def find_nearest(name)
obj = self.class.find_by_path_list(path_list + [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.
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.
def reload
obj_data = Revision.current.find_obj_data_by(:id, [id.to_s]).first.first
update_data(obj_data["values"], obj_data)
end
def text_links
read_attribute_value('_text_links')
end
def obj_class
read_attribute_value('_obj_class')
end
def last_changed
read_attribute_value('_last_changed')
end
def valid_from
read_attribute_value('_valid_from')
end
def valid_until
read_attribute_value('_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.
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.
def file_extension
File.extname(name)[1..-1] || ""
end
def to_liquid # :nodoc:
LiquidSupport::ObjDrop.new(self)
end
def respond_to?(method_id, include_private=false) # :nodoc:
if has_attribute?(method_id)
true
else
super
end
end
# Returns a list of the names of all custom attributes defined for this Obj as Symbols.
# A custom attribute is a user-defined attribute, i.e. one that is not built-in in the CMS.
def custom_attribute_names
obj_class_instance.attributes.map(&:to_sym)
end
def self.preview_time=(t) # :nodoc:
Thread.current[:preview_time] = t
end
def self.preview_time # :nodoc:
Thread.current[:preview_time] || Time.now
end
def inspect
"<#{self.class} id=\"#{id}\" path=\"#{path}\">"
end
def has_attribute?(name)
obj_class_instance.has_attribute?(name)
end
private
def obj_class_instance
ObjClass.by_name(obj_class)
end
def update_data(values = {}, meta = {})
@values = values || {}
meta ||= {}
@rtc_ref = meta['rtc_ref']
@ext_ref = meta['ext_ref']
@attribute_cache = {}
end
def read_attribute(attribute_name)
attribute_type = Attribute.type_of(attribute_name)
if type_of(attribute_name) == attribute_type
read_attribute_value(attribute_name, attribute_type)
else
default_attribute_value(attribute_type)
end
end
def read_attribute_value(attribute_name, attribute_type = nil)
@attribute_cache.fetch(attribute_name) do
attribute_type ||= type_of(attribute_name)
attribute_value = read_raw_attribute_value(attribute_name)
@attribute_cache[attribute_name] =
prepare_attribute_value(attribute_name, attribute_type, attribute_value)
end
end
def prepare_attribute_value(attribute_name, attribute_type, attribute_value)
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
if attribute_name == '_text_links'
LinkList.new(attribute_value && attribute_value.values)
else
LinkList.new(attribute_value)
end
else
attribute_value
end
end
def default_attribute_value(attribute_type)
case attribute_type
when :linklist
LinkList.new(nil)
when :multienum
[]
else
nil
end
end
def attributes
@attributes ||= @rtc_ref ? attributes_from_rtc : {}
end
def attributes_from_rtc
rtc = DictStorage.get(@rtc_ref)
if rtc['obj_classes'] && rtc['attributes'] && oc = rtc['obj_classes'][@values['_obj_class']]
rtc['attributes'].inject({}) do |attrs, (name, value)|
attrs[name] = value if oc['attributes'] && oc['attributes'].include?(name)
attrs
end
else
{}
end
end
def type_of(key)
key = key.to_s
case key
when "_text_links"
:linklist
when "_valid_from"
:date
when "_valid_until"
:date
when "_last_changed"
:date
when "title"
:html
else
if attr_def = attributes[key]
type = attr_def["type"]
type.to_sym if type
end
end
end
def as_date(value)
DateAttribute.parse(value) unless value.nil?
end
def method_missing(method_name, *args) # :nodoc:
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_value(attribute) == 1 ? "numeric" : "alphaNumeric"
end
def converted_sort_key(attribute)
key = read_attribute_value(attribute)
case key
when "_valid_until"
"_valid_until"
when "_valid_from"
"_valid_from"
when "_last_changed"
"_last_changed"
else
key
end
end
def read_raw_attribute_value(attribute_name)
return @values[attribute_name] if @values.key?(attribute_name)
if @ext_ref && (attribute_name == "_text_links" || ?_ != attribute_name[0])
extend_values_with_dict_storage_values
end
@values[attribute_name]
end
def extend_values_with_dict_storage_values
# may raise Kvom::Storage::NotFound
values = DictStorage.get(@ext_ref)
@values.reverse_merge!(values)
@ext_ref = nil
@values
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