lib/scrivito/basic_obj.rb in scrivito_sdk-0.18.1 vs lib/scrivito/basic_obj.rb in scrivito_sdk-0.30.0.rc1

- old
+ new

@@ -1,11 +1,15 @@ require 'json' require 'ostruct' require 'active_model/naming' module Scrivito - # The CMS file class + # The abstract base class for cms objects. + # + # @note Please do not use {Scrivito::BasicObj} directly, + # as it is intended as an abstract class. + # Always use {Obj} or a subclass of {Obj}. # @api public class BasicObj UNIQ_ATTRIBUTES = %w[ _id _path @@ -31,20 +35,20 @@ def self.reset_type_computer! @_type_computer = nil end - # Create a new {BasicObj Obj} in the cms + # Create a new {Scrivito::BasicObj Obj} in the cms # # This allows you to set the different attributes types of an obj by # providing a hash with the attributes names as key and the values you want # to set as values # - # @example Reference lists have to be provided as an Array of {BasicObj Objs} + # @example Reference lists have to be provided as an Array of {Scrivito::BasicObj Objs} # Obj.create(:reference_list => [other_obj]) # - # @example Passing an {BasicObj Obj} allows you to set a reference + # @example Passing an {Scrivito::BasicObj Obj} allows you to set a reference # Obj.create(:reference => other_obj) # # @example you can upload files by passing a ruby File object # Obj.create(:blob => File.new("image.png")) # @@ -70,11 +74,11 @@ # Obj.create(:title => "My Title") # # @example Arrays of {String Strings} allow you to set multi enum fields # Obj.create(:tags => ["ruby", "rails"]) # - # @example Simply pass an Array of {BasicWidget Widgets} to change a widget field. See {BasicWidget#clone Widget#clone} on how to clone a widget. + # @example Simply pass an Array of {Scrivito::BasicWidget Widgets} to change a widget field. See {Scrivito::BasicWidget#clone Widget#clone} on how to clone a widget. # # Add new widgets # Obj.create(:widgets => [Widget.new(_obj_class: 'TitleWidget', title: 'My Title')]) # # # Add a widget clone # Obj.create(:widgets => [another_obj.widgets.first.clone]) @@ -85,21 +89,21 @@ # # Clear a widget field # obj.update(:widgets => []) # # @api public # @param [Hash] attributes - # @return [Obj] the newly created {BasicObj Obj} + # @return [Obj] the newly created {Scrivito::BasicObj Obj} def self.create(attributes) attributes = with_default_obj_class(attributes) api_attributes, widget_properties = prepare_attributes_for_rest_api(attributes, nil) json = Workspace.current.api_request(:post, '/objs', obj: api_attributes) obj = find(json['_id']) CmsRestApi::WidgetExtractor.notify_persisted_widgets(obj, widget_properties) obj end - # Create a new {BasicObj Obj} instance with the given values and attributes. + # Create a new {Scrivito::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(attributes = {}) update_data(ObjDataFromHash.new(attributes)) end @@ -118,11 +122,11 @@ @revision or raise "revision not set!" end ### FINDERS #################### - # Find a {BasicObj Obj} by its id. + # Find a {Scrivito::BasicObj Obj} by its id. # If the parameter is an Array containing ids, return a list of corresponding Objs. # @param [String, Integer, Array<String, Integer>]id_or_list # @return [Obj, Array<Obj>] # @api public def self.find(id_or_list) @@ -131,11 +135,11 @@ def self.find_by_id(id) Workspace.current.objs.find_by_id(id) end - # Find a {BasicObj Obj} by its id. + # Find a {Scrivito::BasicObj Obj} by its id. # If the parameter is an Array containing ids, return a list of corresponding Objs. # The results include deleted objects as well. # @param [String, Integer, Array<String, Integer>]id_or_list # @return [Obj, Array<Obj>] # @api public @@ -145,32 +149,48 @@ # 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}). + # @note If invoked on a subclass of Obj, the result will be restricted to instances of that subclass. # + # {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) # @param [Symbol, String, Array<Symbol, String>] field See {ObjSearchEnumerator#and} for details # @param [Symbol, String] operator See {ObjSearchEnumerator#and} for details # @param [String, Array<String>] value See {ObjSearchEnumerator#and} for details # @param [Hash] boost See {ObjSearchEnumerator#and} for details + # @raise [ScrivitoError] if called on a subclass of +Obj+ with no corresponding {ObjClass} + # @raise [ScrivitoError] if called directly on +BasicObj+. Use +Obj.where+ instead. # @return [ObjSearchEnumerator] # @api public def self.where(field, operator, value, boost = nil) - Workspace.current.objs.where(field, operator, value, boost) + assert_not_basic_obj('.where') + if self == ::Obj + Workspace.current.objs.where(field, operator, value, boost) + else + assert_has_obj_class('.where') + Workspace.current.objs.where(:_obj_class, :equals, name) + .and(field, operator, value, boost) + end end - # Returns a {ObjSearchEnumerator} of all {BasicObj Obj}s. + # Returns a {ObjSearchEnumerator} of all {Scrivito::BasicObj Obj}s. # If invoked on a subclass of Obj, the result will be restricted to instances of that subclass. # @return [ObjSearchEnumerator] + # @raise [ScrivitoError] if called on a subclass of +Obj+ with no corresponding {ObjClass} + # @raise [ScrivitoError] if called directly on +BasicObj+. Use +Obj.all+ instead. # @api public def self.all - if superclass == Scrivito::BasicObj + assert_not_basic_obj('.all') + if self == ::Obj Workspace.current.objs.all else + assert_has_obj_class('.all') find_all_by_obj_class(name) end end # Returns a {ObjSearchEnumerator} of all Objs with the given +obj_class+. @@ -179,47 +199,49 @@ # @api public def self.find_all_by_obj_class(obj_class) Workspace.current.objs.find_all_by_obj_class(obj_class) end - # Find the {BasicObj Obj} with the given path. + # Find the {Scrivito::BasicObj Obj} with the given path. # Returns +nil+ if no matching Obj exists. - # @param [String] path Path of the {BasicObj Obj}. + # @param [String] path Path of the {Scrivito::BasicObj Obj}. # @return [Obj] # @api public def self.find_by_path(path) Workspace.current.objs.find_by_path(path) end - # Find an {BasicObj Obj} with the given name. + # Find an {Scrivito::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}. + # @param [String] name Name of the {Scrivito::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}. + # @param [String] name Name of the {Scrivito::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}. + # Returns the {Scrivito::BasicObj Obj} with the given permalink, or +nil+ if no matching Obj + # exists. + # @param [String] permalink The permalink of the {Scrivito::BasicObj Obj}. # @return [Obj] # @api public def self.find_by_permalink(permalink) Workspace.current.objs.find_by_permalink(permalink) 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}. + # Returns the {Scrivito::BasicObj Obj} with the given permalink, or raise ResourceNotFound if + # no matching Obj exists. + # @param [String] permalink The permalink of the {Scrivito::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}'" @@ -243,14 +265,14 @@ # @return [NilClass, Array<Symbol, String>] # @api public def self.valid_page_classes_beneath(parent_path) end - # Update the {BasicObj Obj} with the attributes provided. + # Update the {Scrivito::BasicObj Obj} with the attributes provided. # # For an overview of which values you can set via this method see the - # documentation of {BasicObj.create Obj.create}. + # documentation of {Scrivito::BasicObj.create Obj.create}. # # Additionally, +update+ accepts a +_widget_pool+ hash in +attributes+ to modify widgets. # The keys of +_widget_pool+ are widget instances, the values are the modified attributes of # these particular widgets. # @@ -297,11 +319,11 @@ options = options.stringify_keys.assert_valid_keys('_path', '_id', '_permalink') json = workspace.api_request(:post, '/objs', obj: copyable_attributes.merge(options)) self.class.find(json['_id']) end - # Destroys the {BasicObj Obj} in the current {Workspace} + # Destroys the {Scrivito::BasicObj Obj} in the current {Workspace} # @api public def destroy if children.any? raise ClientError.new(I18n.t('scrivito.errors.models.basic_obj.has_children'), 412) end @@ -310,16 +332,16 @@ def to_param id end - # return the {BasicObj Obj} that is the parent of this Obj. + # return the {Scrivito::BasicObj Obj} that is the parent of this Obj. # returns +nil+ for the root Obj. # @api public def parent if child_path? - BasicObj.find_by_path(parent_path) + workspace.objs.find_by_path(parent_path) end end # Returns an Array of all the ancestor objects, starting at the root and ending at this object's parent. # @return [Array<Obj>] @@ -332,43 +354,45 @@ end ancestor_paths[0] = "/" Workspace.current.objs.find_by_paths(ancestor_paths) end - # return a list of all child {BasicObj Obj}s. + # return a list of all child {Scrivito::BasicObj Obj}s. # @return [Array<Obj>] # @api public def children return [] unless path workspace.objs.find_by_parent_path(path) end ### ATTRIBUTES ################# - # returns the {BasicObj Obj}'s path as a String. + # returns the {Scrivito::BasicObj Obj}'s path as a String. # @api public def path read_attribute('_path') end - # returns the {BasicObj Obj}'s name, i.e. the last component of the path. + # returns the {Scrivito::BasicObj Obj}'s name, i.e. the last component of the path. # @api public def name if child_path? path.match(/[^\/]+$/)[0] else "" end end - # Returns the root {BasicObj Obj}, i.e. the Obj with the path "/" + # Returns the root {Scrivito::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 '/'." + BasicObj.find_by_path('/') or raise ResourceNotFound, + '"Obj.root" not found: There is no "Obj" with path "/". '\ + 'Maybe you forgot the migration when setting up your Scrivito application? '\ + 'Try "rake scrivito:migrate" and "rake scrivito:migrate:publish".' 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] @@ -425,32 +449,57 @@ # @api public def slug (title || '').parameterize end - # This method determines the description that is shown in the changes list. - # It can be overriden by a custom value. + # + # This method determines the description that is shown in the UI + # and defaults to {Scrivito::BasicObj#display_title}. It can be overriden by a custom value. + # # @api public + # def description_for_editor - slug.presence || path + display_title end - # Returns the title of the content or the name. - # @return [String] + # + # Calculates appropriate title for an +Obj+. + # # @api public + # + # @return [String] {Scrivito::Binary#filename} if +Obj+ is +binary+ and has a +filename+. + # @return [String] {Scrivito::BasicObj#title} if +Obj+ has a non-empty +title+. + # @return [String] a placeholder +<untitled MyClass>+ otherwise. + # def display_title - self.title || name + (binary_title || title).presence || "<untitled #{obj_class_name}>" end # @api public def title read_attribute('title') end - # Returns true if image? or generic? + # @api public + # This method indicates if the Obj represents binary data. Binaries are + # handled differently in that they are not rendered using the normal layout + # but sent as a file. Examples of binary resources are Images or PDFs. + # + # By default every Obj that has an attribute +blob+ of the type +binary+ is + # considered a binary + # + # @note you can override this method to indicate that an Obj is binary, + # if the default behavior doesn't fit your needs + # + # @return true if this Obj represents a binary resource. def binary? - [:image, :generic].include?(read_attribute('_obj_type').to_sym) + if obj_type = read_attribute('_obj_type') + [:image, :generic].include?(obj_type.to_sym) + else + blob_attribute = obj_class.attributes['blob'] + blob_attribute && blob_attribute.type == 'binary' + end end # Returns true if this object is the root object. # @api public def root? @@ -466,13 +515,14 @@ toclist = children toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all) toclist end - # @param objs_to_be_sorted [Array<BasicObj>] unsorted list of Objs - # @param list [Array<BasicObj>] list of Objs that defines the order - # @return [Array<BasicObj>] a sorted list of Objs. Any objs present in +objs_to_be_sorted+ but not in +list+ are appended at the end, sorted by +Obj#id+ + # @param objs_to_be_sorted [Array<Scrivito::BasicObj>] unsorted list of Objs + # @param list [Array<Scrivito::BasicObj>] list of Objs that defines the order + # @return [Array<Scrivito::BasicObj>] a sorted list of Objs. Any objs present in + # +objs_to_be_sorted+ but not in +list+ are appended at the end, sorted by +Obj#id+ def self.sort_by_list(objs_to_be_sorted, list) (list & objs_to_be_sorted) + (objs_to_be_sorted - list).sort_by(&:id) end # This should be a SET, because it's faster in this particular case. @@ -480,10 +530,11 @@ body _id _last_changed _path _permalink + _obj_class title ]) # 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+. @@ -581,23 +632,12 @@ obj end 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 + raise "The method `content_type' and `mime_type' were removed. Please use `binary_content_type' instead" 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. @@ -617,46 +657,59 @@ else read_attribute('body') 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 + # This method is intended for Objs that represent binary resources like + # images or pdf documents. If this Obj represents a binary file, an instance + # of {Binary} is returned. + # The default implementation returns the attribute "blob" (if available and + # of type +binary+). + # @note You may override this method in your subclasses. + # @return [Binary, nil] + def binary + self[:blob] if self[:blob].is_a?(Binary) 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 + # This method returns the length in bytes of the binary of this obj + # @return [Fixnum] If no binary is set it will return 0 + def binary_length + binary.try(:content_length) || 0 end - # returns the content type of the Obj's body for binary Objs. - # returns +nil+ for non-binary Objs. - # @return [String] # @api public + # This method returns the content type of the binary of this obj if it is set. + # @return [String, nil] + def binary_content_type + binary.try(:content_type) + end + + # @api public + # This method returns the url under which the content of this binary is + # available to the public if the binary is set. + # + # See {Binary#url} for details + # @return [String, nil] + def binary_url + binary.try(:url) + end + + def body_length + raise %( + The method `body_length' was removed. Please use + `binary_length' or `body.length' instead + ) + end + + def body_data_url + raise "The method `body_data_url' was removed. Please use `binary_url' instead" + end + def body_content_type - if binary? - blob = find_blob - if blob - blob.content_type - else - "application/octet-stream" - end - end + raise "The method `body_content_type' was removed. Please use `binary_content_type' instead" end def inspect "<#{self.class} id=\"#{id}\" path=\"#{path}\">" end @@ -673,14 +726,25 @@ # for internal testing purposes only def blob_id find_blob.try(:id) end - # Reverts changes of this object. - # After calling this method it's as if this object has been never modified in the current working copy. - # This method does not work with +new+ or +deleted+ objects. - # This method also does also not work for the +published+ workspace or the +rtc+ working copy. + # + # Reverts all changes made to the +Obj+ in the current workspace. + # + # @api public + # + # @note This method does not support +Obj+s, which are +new+. + # Please use {Scrivito::BasicObj#destroy Obj#destroy} to destroy them. + # @note This method does not support +Obj+s, which are +deleted+. + # Please use {Scrivito::BasicObj.restore Obj.restore} to restore them. + # + # @raise [ScrivitoError] If the current workspace is +published+. + # @raise [ScrivitoError] If the current workspace is the +rtc+ workspace. + # @raise [ScrivitoError] If the +Obj+ is +new+. + # @raise [ScrivitoError] If the +Obj+ is +deleted+. + # def revert assert_revertable if modification == Modification::EDITED base_revision_path = "revisions/#{workspace.base_revision_id}/objs/#{id}" @@ -757,10 +821,16 @@ # #=> true def to_h data_from_cms.to_h.except(*GENERATED_ATTRIBUTES) end + def parent_path + unless root? + path.gsub(/\/[^\/]+$/, '').presence || '/' + end + end + private def cms_data_for_revision(revision) if revision CmsBackend.instance.find_obj_data_by(revision, "id", [id]).first.first @@ -783,22 +853,16 @@ 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 + read_attribute('blob') end def workspace if revision.workspace revision.workspace @@ -821,16 +885,42 @@ .except(*UNIQ_ATTRIBUTES) end def assert_revertable workspace.assert_revertable - raise "revert not supported for binary objs" if binary? if modification == Modification::NEW || modification == Modification::DELETED raise ScrivitoError, "cannot revert changes, since obj is #{modification}." end end + def binary_title + binary.filename if binary? && binary + end + class << self + + def assert_not_basic_obj(method_name) + if self == Scrivito::BasicObj + raise ScrivitoError, "Can not call #{method_name} on Scrivito::BasicObj." + + " Only call it on Obj or subclasses of Obj" + end + end + + def assert_has_obj_class(method_name) + unless Workspace.current.obj_classes[name].present? + raise ScrivitoError, "#{name} has no corresponding ObjClass." + + " Please use Obj.#{method_name} instead." + end + end + + # + # Restores a previously deleted +Obj+. + # + # @api public + # + # @raise [ScrivitoError] If the current workspace is +published+. + # @raise [ScrivitoError] If the current workspace is the +rtc+ workspace. + # def restore(obj_id) Workspace.current.assert_revertable base_revision_path = "revisions/#{Workspace.current.base_revision_id}/objs/#{obj_id}" obj_attributes = CmsRestApi.get(base_revision_path).merge('_id' => obj_id) Workspace.current.api_request(:post, '/objs', obj: obj_attributes)