require 'json' require 'ostruct' module RailsConnector # The CMS file class # @api public class BasicObj WIDGET_POOL_ATTRIBUTE_NAME = '_widget_pool'.freeze extend ActiveModel::Naming include AttributeContent include ModelIdentity def self.type_computer @_type_computer ||= TypeComputer.new(RailsConnector::BasicObj, ::Obj) end def self.reset_type_computer! @_type_computer = nil end # Create a new {BasicObj 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 # @api public def id read_attribute('_id') end ### FINDERS #################### # Find an {BasicObj Obj} by its id. # If the paremeter is an Array containing ids, return a list of corresponding Objs. # @param [String, Integer, Array]id_or_list # @return [Obj, Array] # @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 Obj with id #{id_or_list}" end end # Returns a {ObjSearchEnumerator} with the given initial subquery consisting of the four arguments. # # Note that +field+ and +value+ can also be arrays for searching several fields or searching for several values. # # {ObjSearchEnumerator}s can be chained using one of the chainable methods (e.g. {ObjSearchEnumerator#and} and {ObjSearchEnumerator#and_not}). # # @example Look for the first 10 Objs whose ObjClass is "Pressrelease" and whose title contains "quarterly": # Obj.where(:_obj_class, :equals, 'Pressrelease').and(:title, :contains, 'quarterly').take(10).map{ |obj| obj.valid_from } # @param [Symbol, String, Array] field See {ObjSearchEnumerator#and} for details # @param [Symbol, String] operator See {ObjSearchEnumerator#and} for details # @param [String, Array] value See {ObjSearchEnumerator#and} for details # @param [Hash] boost See {ObjSearchEnumerator#and} for details # @return [ObjSearchEnumerator] # @api public def self.where(field, operator, value, boost = nil) ObjSearchEnumerator.new(nil).and(field, operator, value, boost) end # Returns a {ObjSearchEnumerator} of all {BasicObj Obj}s. # If invoked on a subclass of Obj, the result will be restricted to instances of that subclass. # @return [ObjSearchEnumerator] # @api public def self.all if superclass == RailsConnector::BasicObj search_for_all else find_all_by_obj_class(name) end end # Returns a {ObjSearchEnumerator} of all Objs with the given +obj_class+. # @param [String] obj_class Name of the ObjClass. # @return [ObjSearchEnumerator] # @api public def self.find_all_by_obj_class(obj_class) search_for_all.and(:_obj_class, :equals, obj_class) end # Find the {BasicObj Obj} with the given path. # Returns +nil+ if no matching Obj exists. # @param [String] path Path of the {BasicObj Obj}. # @return [Obj] # @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 {BasicObj Obj} with the given name. # If several Objs with the given name exist, an arbitrary one of these Objs is chosen and returned. # If no Obj with the name exits, +nil+ is returned. # @param [String] name Name of the {BasicObj Obj}. # @return [Obj] # @api public def self.find_by_name(name) where(:_name, :equals, name).batch_size(1).first end # Returns a {ObjSearchEnumerator} of all Objs with the given name. # @param [String] name Name of the {BasicObj Obj}. # @return [ObjSearchEnumerator] # @api public def self.find_all_by_name(name) where(:_name, :equals, name) end # Returns the {BasicObj Obj} with the given permalink, or +nil+ if no matching Obj exists. # @param [String] permalink The permalink of the {BasicObj Obj}. # @return [Obj] # @api public def self.find_by_permalink(permalink) find_objs_by(:permalink, [permalink]).first.first end # Returns the {BasicObj Obj} with the given permalink, or raise ResourceNotFound if no matching Obj exists. # @param [String] permalink The permalink of the {BasicObj Obj}. # @return [Obj] # @api public def self.find_by_permalink!(permalink) find_by_permalink(permalink) or raise ResourceNotFound, "Could not find Obj 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| BasicObj.instantiate(obj_data) } end end # Hook method to control which page classes should be available for a page with given path. # Override it to allow only certain classes or none. # Must return either +NilClass+, or +Array+. # # Be aware that the given argument is a parent path. # E.g. when creating a page with path +/products/shoes+ then the argument will be +/products+. # # If +NilClass+ is returned, then all possible classes will be available. # By default +NilClass+ is returned. # # If +Array+ is returned, then it should include desired class names. # Each class name must be either a +String+ or a +Symbol+. # Only this class names will be available. Order of the class names will be preserved. # # @param [String] parent_path Path of the parent obj # @return [NilClass, Array] # @api public def self.valid_page_classes_beneath(parent_path) end def to_param id end # return the {BasicObj Obj} that is the parent of this Obj. # returns +nil+ for the root Obj. # @api public def parent root? ? nil : BasicObj.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. # @return [Array] # @api public def ancestors return [] if root? ancestor_paths = parent_path.scan(/\/[^\/]+/).inject([""]) do |list, component| list << list.last + component end ancestor_paths[0] = "/" BasicObj.find_many_by_paths(ancestor_paths) end # return a list of all child {BasicObj Obj}s. # @return [Array] # @api public def children self.class.find_objs_by(:ppath, [path]).first end ### ATTRIBUTES ################# # returns the {BasicObj Obj}'s path as a String. # @api public def path read_attribute('_path') or raise 'Obj without path' end # returns the {BasicObj 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 {BasicObj Obj}, i.e. the Obj with the path "/" # @return [Obj] # @api public def self.root BasicObj.find_by_path("/") or raise ResourceNotFound, "Obj.root not found: There is no Obj with path '/'." end # Returns the homepage obj. This can be overwritten in your application's +Obj+. # Use {#homepage?} to check if an obj is the homepage. # @return [Obj] # @api public def self.homepage root end # @api private def self.generate_widget_pool_id SecureRandom.hex(4) 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. # @return [String] # @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. # @return [String] # @api public def controller_action_name "index" end # Returns true if the current obj is the {.homepage} obj. # @api public def homepage? self == self.class.homepage end # This method is used to calculate a part of a URL of this Obj. # # The routing schema: / # # The default is {http://apidock.com/rails/ActiveSupport/Inflector/parameterize parameterize} # on +obj.title+. # # You can customize this part by overwriting {#slug}. # @return [String] # @api public def slug (title || '').parameterize end # Returns the title of the content or the name. # @return [String] # @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. # @api public # @deprecated Active is deprecated without substitution. def active? Deprecation.warn_method('Obj#active?') return false unless valid_from valid_from <= Time.now && (!valid_until || Time.now <= valid_until) end # compatibility with legacy apps. def suppress_export Deprecation.warn_method('Obj#suppress_export?') suppressed? ? 1 : 0 end # Returns true if the {BasicObj 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? Deprecation.warn_method('Obj#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? Deprecation.warn_method('Obj#exportable?') !suppressed? && active? end # Returns the file name to which the Content.file_extension has been appended. def filename Deprecation.warn_method('Obj#filename', 'Obj#name') 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 children excluding the binary? ones unless :all is specfied. # This is mainly used for navigations. # @return [Array] # @api public def toclist(*args) return [] unless publication? toclist = children toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all) toclist end # Returns the sorted +toclist+, respecting sort order and type of this Obj. # @return [Array] # @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. # @param [String] name # @return [Obj] # @api public def find_nearest(name) obj = self.class.find_by_path(root? ? "/#{name}" : "#{path}/#{name}") if obj obj elsif !self.root? parent.find_nearest(name) end 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) else super 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 # @return [String] # @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 its body (i.e. its 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. # @return [String] # @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. # @return [String] # @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. # @return [String] # @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. # @return [String] # @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. # @return [String] # @api public def body_content_type if binary? blob = find_blob if blob blob.content_type else "application/octet-stream" end end end def inspect "<#{self.class} id=\"#{id}\" path=\"#{path}\">" end def destroy if children.any? raise ClientError.new(I18n.t('rails_connector.errors.models.basic_obj.has_children'), 412) end CmsRestApi.delete("workspaces/#{Workspace.current.id}/objs/#{id}") end def edit_view_path "#{obj_class.underscore}/edit" end def widget_from_pool(widget_id) widget_data = widget_data_from_pool(widget_id) instantiate_widget(widget_id, widget_data) if widget_data end private def widget_data_from_pool(widget_id) read_widget_pool[widget_id] end def read_widget_pool read_attribute(WIDGET_POOL_ATTRIBUTE_NAME) end def instantiate_widget(widget_id, widget_data) BasicWidget.instantiate(widget_data).tap do |widget| widget.id = widget_id widget.obj = self 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 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 search_for_all ObjSearchEnumerator.new(nil).batch_size(1000) end end end end