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