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
@values["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:
Workspace.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(: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
def object_id # :nodoc:
obj_id
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(: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(: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)
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?
@values["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(: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
# Returns the value of the attribute specified by its name.
#
# Passing an invalid key will not raise an error, but return nil.
def [](raw_key)
key = raw_key.to_s
case key
when "name"
name
when "body"
body
else
read_attribute(key)
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 = Workspace.current.find_obj_data_by(:id, [id.to_s]).first.first
update_data(obj_data["values"], obj_data)
end
def last_changed
self[:last_changed]
end
def valid_from
self[:valid_from]
end
def valid_until
self[: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
@attributes.keys.map(&:to_sym)
end
def has_attribute?(name)
name_as_string = name.to_s
@values.has_key?(name_as_string) || @attributes.has_key?(name_as_string)
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
private
def update_data(values = {}, meta = {})
@values = values
@attributes = meta["attributes"] || {}
@attr_cache = {}
end
def read_attribute(name)
name = name.to_s
if name == "suppress_export"
suppress_export
else
raw = @values[name]
@attr_cache[name] ||= case type_of(name)
when :markdown
StringTagging.tag_as_markdown(raw, self)
when :html
StringTagging.tag_as_html(raw, self)
when :date
DateAttribute.parse raw if raw
when :linklist
if name == "text_links"
LinkList.new(raw.values)
else
LinkList.new(raw)
end
else
raw
end
end
end
def type_of(key)
key = key.to_s
case key
when "permission_live_server_read"
nil
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_id, *args) # :nodoc:
if has_attribute?(method_id)
read_attribute(method_id)
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
def converted_sort_key(attribute)
key = read_attribute(attribute)
case key
when "validUntil"
"valid_until"
when "validFrom"
"valid_from"
when "lastChanged"
"last_changed"
when "contentType"
"content_type"
else
key
end
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