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