lib/scrivito/basic_obj.rb in scrivito_sdk-0.66.0 vs lib/scrivito/basic_obj.rb in scrivito_sdk-0.70.0.rc1

- old
+ new

@@ -2,15 +2,23 @@ require 'ostruct' require 'active_model/naming' module Scrivito # - # The abstract base class for cms objects. + # The abstract base class for CMS objects. # - # @note Please do not use {Scrivito::BasicObj} directly, + # A CMS object is a collection of properties and their values, as defined + # by its object class. These properties can be accessed in views, either + # directly as the object itself is rendered, or indirectly when other objects + # are rendered. The description of an image, for example, can be retrieved + # from within any view that requires it, e.g. a page on which the image is + # displayed. + # + # @note Please do not use {Scrivito::BasicObj} directly # as it is intended as an abstract class. # Always use {Obj} or a subclass of {Obj}. + # @see http://scrivito.com/objects-widgets-classes CMS objects, widgets, and classes # @api public # class BasicObj PublicSystemAttributeDefinition = Class.new(AttributeDefinition) @@ -57,68 +65,69 @@ # # Create a new {Scrivito::BasicObj Obj} in the CMS. # # @api public # - # 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. It also considers the - # defaults set via {Scrivito::AttributeContent::ClassMethods#default_for Obj.default_for}. + # This allows you to set the different attributes of an Obj by providing a hash containing the + # attribute names as keys and the corresponding values you want to set. The defaults set via + # {Scrivito::AttributeContent::ClassMethods#default_for Obj.default_for} are taken into account. # - # @param [Hash] attributes for the new obj - # @param [Hash] context in which the object creating should happen + # @param [Hash] attributes of the new obj + # @param [Hash] context in which the object is created # @option context [Scrivito::User] :scrivito_user current visitor # @return [Obj] the newly created {Scrivito::BasicObj Obj} # # @see Scrivito::AttributeContent::ClassMethods#default_for # - # @example Reference lists have to be provided as an Array of {Scrivito::BasicObj Objs} + # @example Provide reference lists as an Array of {Scrivito::BasicObj Obj}. # Obj.create(:reference_list => [other_obj]) # - # @example Passing an {Scrivito::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 + # @example You can upload files by passing a Ruby File object. # Obj.create(:blob => File.new("image.png")) # - # @example Link list can be set as an Array of {Link Links} + # @example A link list can be set as an Array of {Link Links}. # Obj.create(:link_list => [ # # external link # Link.new(:url => "http://www.example.com", :title => "Example"), # # internal link # Link.new(:obj => other_obj, :title => "Other Obj") # ]) # # @example Passing a {Link Link} allows you to set a link. # Obj.create( - # external_link: Link.new(url: 'http://www.example.com', title: 'Example') + # external_link: Link.new(url: 'http://www.example.com', title: 'Example'), # internal_link: Link.new(obj: other_obj, title: 'Other Obj') # ) # - # @example Dates attributes accept Time, Date and their subclasses (DateTime for example) + # @example Date attributes accept Time, Date and their subclasses (DateTime, for example). # Obj.create(:date => Time.new) # Obj.create(:date => Date.now) # - # @example String, html and enum can be set by passing a {String} value + # @example String, html and enum attributes can be set by passing a {String} value. # Obj.create(:title => "My Title") # - # @example Arrays of {String Strings} allow you to set multi enum fields + # @example Multienum attributes can be set using an Array of {String Strings}. # Obj.create(:tags => ["ruby", "rails"]) # - # @example Simply pass an Array of {Scrivito::BasicWidget Widgets} to change a widget field. See {Scrivito::BasicWidget#copy Widget#copy} on how to copy a widget. + # @example Simply pass an Array of {Scrivito::BasicWidget Widgets} to populate a widget field. See {Scrivito::BasicWidget#copy Widget#copy} on how to copy a widget. # # Add new widgets # Obj.create(:widgets => [Widget.new(_obj_class: 'TitleWidget', title: 'My Title')]) # - # # Add a widget copy + # # Add a copy of a widget # Obj.create(:widgets => [another_obj.widgets.first.copy]) # - # # Changing a widget field + # # Change a widget field # obj.update(:widgets => [obj.widgets.first]) # # # Clear a widget field # obj.update(:widgets => []) # def self.create(attributes = {}, context = {}) + attributes = with_default_id_attribute(attributes) if obj_class = extract_obj_class_from_attributes(attributes) obj_class.create(attributes, context) else attributes = build_attributes_with_defaults(attributes, context) attributes = prepare_attributes_for_instantiation(attributes) @@ -129,12 +138,12 @@ obj end end # 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. + # Normally, this method should not be used. + # Instead, CMS objects should be retrieved from the CMS database. def initialize(attributes = {}) update_data(ObjDataFromHash.new(attributes)) end # @api public @@ -151,12 +160,14 @@ @revision or raise "revision not set!" end ### FINDERS #################### - # Find a {Scrivito::BasicObj Obj} by its id. - # If the parameter is an Array containing ids, return a list of corresponding Objs. + # Find an {Scrivito::BasicObj Obj} by its id. + # If the parameter is an Array of ids, the list of corresponding objects is returned. + # @example Find several CMS objects at once: + # Obj.find(['id1', 'id2']) # @param [String, Integer, Array<String, Integer>]id_or_list # @return [Obj, Array<Obj>] # @api public def self.find(id_or_list) Workspace.current.objs.find(id_or_list) @@ -164,31 +175,40 @@ def self.find_by_id(id) Workspace.current.objs.find_by_id(id) end - # Find a {Scrivito::BasicObj Obj} by its id. - # If the parameter is an Array containing ids, return a list of corresponding Objs. + # Find an {Scrivito::BasicObj Obj} by its id. + # If the parameter is an Array containing ids, return a list of corresponding objects. # The results include deleted objects as well. # @param [String, Integer, Array<String, Integer>]id_or_list # @return [Obj, Array<Obj>] # @api public def self.find_including_deleted(id_or_list) Workspace.current.objs.find_including_deleted(id_or_list) end - # Returns a {ObjSearchEnumerator} with the given initial subquery consisting of the four arguments. + # Returns an {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. # - # @note If invoked on a subclass of Obj, the result will be restricted to instances of that subclass. + # @note If invoked on a subclass of Obj, the result will be restricted to instances of this 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 obj class is "Pressrelease" and whose title contains "quarterly": + # @example Look for objects containing "Lorem", boosting headline matches: + # Obj.where(:*, :contains, 'Lorem', headline: 2).to_a + # + # @example Look for the first 10 objects whose object class is "Pressrelease" and whose title contains "quarterly": # Obj.where(:_obj_class, :equals, 'Pressrelease').and(:title, :contains, 'quarterly').take(10) + # + # @example Look for all objects whose class is "Item". The path should start with a defined location. Furthermore, select only items of a particular category: + # Obj.where(:_obj_class, :equals, 'Item').and(:_path, :starts_with, '/en/items/').select do |item| + # item.categories.include?(category) + # end + # # @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 directly on +BasicObj+. Use +Obj.where+ instead. @@ -202,12 +222,12 @@ Workspace.current.objs.where(:_obj_class, :equals, name) .and(field, operator, value, boost) end end - # 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. + # Returns an {ObjSearchEnumerator} of all {Scrivito::BasicObj Obj}s. + # If invoked on a subclass of Obj, the result is restricted to instances of this subclass. # @return [ObjSearchEnumerator] # @raise [ScrivitoError] if called directly on +BasicObj+. Use +Obj.all+ instead. # @api public def self.all assert_not_basic_obj('.all') @@ -216,12 +236,12 @@ 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 obj class. + # Returns an {ObjSearchEnumerator} of all CMS objects with the given +obj_class+. + # @param [String] obj_class name of the object class. # @return [ObjSearchEnumerator] # @api public def self.find_all_by_obj_class(obj_class) Workspace.current.objs.find_all_by_obj_class(obj_class) end @@ -234,59 +254,59 @@ def self.find_by_path(path) Workspace.current.objs.find_by_path(path) end # 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. + # If several objects with the given name exist, an arbitrary one is chosen and returned. + # If no Obj with this name exists, +nil+ is returned. # @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. + # Returns an {ObjSearchEnumerator} of all CMS objects with the given name. # @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 {Scrivito::BasicObj Obj} with the given permalink, or +nil+ if no matching Obj - # exists. + # 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 {Scrivito::BasicObj Obj} with the given permalink, or raise ResourceNotFound if + # Returns the {Scrivito::BasicObj Obj} with the given permalink, or raises 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}'" 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. + # Hook method that lets you control which page classes are made available for pages with the given path. + # Override it to allow only specific classes or none at all. # 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+. + # E.g., when creating a page whose path is +/products/shoes+, the argument must be +/products+. # The given parent path can also be +NilClass+. # - # If +NilClass+ is returned, then all possible classes will be available. - # By default +NilClass+ is returned. + # If +NilClass+ is returned, all page classes are made available. + # By default, +NilClass+ is returned. # - # If +Array+ is returned, then it should include the desired classes. - # Only this classes will be available. Order of the classes will be preserved. + # If an +Array+ is returned, it is expected to contain the available classes. + # The order of the classes is preserved. # # @param [String, NilClass] parent_path Path of the parent obj # @return [NilClass, Array<Class>] # @api public def self.valid_page_classes_beneath(parent_path) @@ -295,31 +315,45 @@ def self.valid_page_ruby_classes_beneath(parent_path) assert_classes(valid_page_classes_beneath(parent_path), '.valid_page_classes_beneath') || Scrivito.models.pages.to_a end - # Update the {Scrivito::BasicObj Obj} with the attributes provided. + # Update the {Scrivito::BasicObj Obj} using the attributes provided. # - # For an overview of which values you can set via this method see the + # For an overview of the values you can set via this method, see the # 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. # # @api public # @param [Hash] attributes + # @example Update the URL of a link: + # link = obj.my_link + # link.url = "http://www.example.com" + # obj.update(my_link: link) + # + # @example Update binary attributes: + # obj.update(blob: img_obj.binary) + # obj.update(thumbnail: File.new("/path/to/small.jpg")) + # + # @example Remove the first and the last widget from a widget field: + # obj.update( + # my_widget_field: obj[:my_widget_field][1..-2] + # ) + # # @example Move the +widget_to_move+ widget from the +left+ widget field of the +two_column_widget1+ widget to +left+ of +two_column_widget2+: # obj.update( # _widget_pool: { # two_column_widget1 => {left: two_column_widget1.left - [widget_to_move]}, # two_column_widget2 => {left: two_column_widget2.left + [widget_to_move]} # }, # headline: "Some widgets were moved!" # ) # - # @example Move the +widget_to_move+ widget from the +right+ widget field of the +two_column_widget1+ widget to the top-level widget field +main_content+: + # @example Move the +widget_to_move+ widget from the +right+ widget field of the +two_column_widget1+ widget to the top-level +main_content+ widget field: # obj.update( # main_content: @obj.main_content + [widget_to_move], # _widget_pool: { # two_column_widget1 => { # right: two_column_widget1.right - [widget_to_move] @@ -332,28 +366,31 @@ reload_data CmsRestApi::WidgetExtractor.notify_persisted_widgets(self, widget_properties) self end - # Creates a copy of the +Obj+. + # Creates a copy of the {Scrivito::BasicObj Obj}. # @api public # @param [Hash] options # @option options [String,Symbol] :_path (nil) the path of the copy. # @option options [String,Symbol] :_id (nil) the id of the copy. # @option options [String,Symbol] :_permalink (nil) the permalink of the copy. # @raise [ArgumentError] if +options+ includes invalid keys. # @return [Obj] the created copy - # @example Copy a blog post. + # @example Copy a blog post: # blog_post = Obj.find_by_path('/blog/first_post') # blog_post.copy(_path: '/blog/second_post') def copy(options={}) options = options.stringify_keys.assert_valid_keys('_path', '_id', '_permalink') - json = workspace.api_request(:post, '/objs', obj: copyable_attributes.merge(options)) + attributes_for_copy = self.class.with_default_id_attribute(copyable_attributes) + attributes_for_copy = copy_binaries(attributes_for_copy) + + json = workspace.api_request(:post, '/objs', obj: attributes_for_copy.merge(options)) self.class.find(json['_id']) end - # Destroys the {Scrivito::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 @@ -362,20 +399,20 @@ def to_param id end - # return the {Scrivito::BasicObj Obj} that is the parent of this Obj. - # returns +nil+ for the root Obj. + # Returns the {Scrivito::BasicObj Obj} that is the parent of this Obj. + # Returns +nil+ for the root Obj. # @api public def parent if child_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. + # Returns an Array of all the ancestor objects, starting at the root and ending at the parent of this object. # @return [Array<Obj>] # @api public def ancestors return [] unless child_path? @@ -384,93 +421,79 @@ end ancestor_paths[0] = "/" Workspace.current.objs.find_by_paths(ancestor_paths) end - # return a list of all child {Scrivito::BasicObj Obj}s. + # Returns 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 {Scrivito::BasicObj Obj}'s path as a String. + # Returns the path of the {Scrivito::BasicObj Obj} as a String. # @api public def path read_attribute('_path') end - # returns the {Scrivito::BasicObj Obj}'s name, i.e. the last component of the path. + # Returns the name of the {Scrivito::BasicObj Obj}. The name is the last component of a path. # @api public def name if child_path? path.match(/[^\/]+$/)[0] else "" end end - # Returns the root {Scrivito::BasicObj Obj}, i.e. the Obj with the path "/" + # Returns the root {Scrivito::BasicObj Obj}, the object whose path is "/". # @return [Obj] # @api public def self.root 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 "bundle exec rake scrivito:migrate scrivito:migrate:publish".' end - # Returns the homepage obj. This can be overridden in your application's +Obj+. - # Use {#homepage?} to check if an obj is the homepage. - # @return [Obj] - # @api public - def self.homepage - root - end - def self.generate_widget_pool_id SecureRandom.hex(4) end - # returns the obj's permalink. + # Returns the permalink of the {Scrivito::BasicObj Obj}. # @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. + # This method determines the controller to be invoked when the +Obj+ is requested. + # By default, a controller matching the obj_class of the Obj is used. + # If the controller does not exist, the CmsController is used as a fallback. # Override this method to force a different controller to be used. # @return [String] # @api public def controller_name obj_class_name end - # This method determines the action that should be invoked when the +Obj+ is requested. + # This method determines the action to be invoked when the +Obj+ is requested. # The default action is 'index'. # Override 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: <code><em><obj.id></em>/<em><obj.slug></em></code> + # The routing schema: <code><em><obj.slug></em>-<em><obj.id></em></code> # # The default is {http://apidock.com/rails/ActiveSupport/Inflector/parameterize parameterize} # on +obj.title+. # # You can customize this part by overriding {#slug}. @@ -479,12 +502,12 @@ def slug (title || '').parameterize end # - # This method determines the description that is shown in the UI - # and defaults to {Scrivito::BasicObj#display_title}. It can be overridden by a custom value. + # This method determines the description shown in the UI. + # It defaults to {Scrivito::BasicObj#display_title}. It can be overridden by a custom value. # # @api public # def description_for_editor display_title @@ -503,11 +526,11 @@ (binary_title || title).presence || self.class.description_for_editor end # The alt description of an +Obj+ used for {ScrivitoHelper#scrivito_image_tag}. # - # By default this method returns the +title+ of this +Obj+. + # By default, this method returns the +title+ of the +Obj+. # # You can customize this part by overriding {#alt_description}. # # @return [String] # @api public @@ -519,78 +542,78 @@ def title read_attribute('title') end # @api public - # This method indicates if the Obj represents binary data. Binaries are + # This method indicates whether 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. # - # Every Obj that has an attribute +blob+ of the type +binary+ is + # Every Obj that has a +blob+ attribute of the +binary+ type is # considered a binary # # @return true if this Obj represents a binary resource. def binary? blob_attribute_definition = attribute_definitions['blob'] blob_attribute_definition.present? && blob_attribute_definition.type == 'binary' end # - # When delivering binary Objs, this method decides whether the image transformations should be - # applied by default. + # When delivering binary objects, this method decides whether the image transformations should + # be applied by default. # - # @api beta + # @api public # - # By default this method returns +false+. - # Override in subclasses to fit your needs. + # By default, this method returns +false+. + # Override it in subclasses to fit your needs. # - # @note Only relevant for binary Objs + # @note Only relevant for binary objects # @see Scrivito::Configuration.default_image_transformation= # @see Scrivito::Binary#transform # def apply_image_transformation? false end - # Returns true if this object is the root object. + # Returns true if this Obj is the root Obj. # @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. + # This is useful for creating navigations. # @return [Array<Obj>] # @api public def toclist(*args) return [] if binary? toclist = children toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all) toclist end - # @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+ + # @param objs_to_be_sorted [Array<Scrivito::BasicObj>] unsorted list of CMS objects + # @param list [Array<Scrivito::BasicObj>] list of objects that defines the order + # @return [Array<Scrivito::BasicObj>] a sorted list of objects. Objects present in + # +objs_to_be_sorted+ but not in +list+ are appended to 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. + # This should be a Set, because it's faster in this particular case. SYSTEM_KEYS = Set.new(%w[ body _id _last_changed _path _permalink _obj_class title ]) - # Returns the value of an system or custom attribute specified by its name. - # Passing an invalid key will not raise an error, but return +nil+. + # Returns the value of a system or custom 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 SYSTEM_KEYS.include?(key) read_attribute(key) @@ -607,12 +630,12 @@ 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, + # Reloads the attributes of this Obj from the database. + # Note 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 workspace.reload reload_data @@ -650,14 +673,14 @@ else quick_modification end end - # similar to modification, but faster if you are only interested in + # Similar to modification but faster if you are only interested in # "new" and "deleted". - # this method sometimes does not return a string, but an instance of - # ObjData instead. this indicates that the modification is either + # This method sometimes does not return a string, but an instance of + # ObjData instead. This indicates that the modification is either # UNMODIFIED or EDITED. Which one it is can be determined by comparing # the returned ObjData. def quick_modification(revision) return Modification::UNMODIFIED unless revision @@ -667,11 +690,11 @@ data_for_comparison = cms_data_for_revision(revision) if data_for_comparison.present? if modification_attr == 'deleted' - # Obj exists in comparison revision, but not in current + # Obj exists in comparison revision but not in current Modification::DELETED else # Obj exists in both revisions, leave the actual comparions # up to the caller data_for_comparison @@ -679,11 +702,11 @@ else if modification_attr == "deleted" # Obj does not exist in either revision Modification::UNMODIFIED else - # Obj exists in current, but not in comparision revision + # Obj exists in current but not in comparision revision Modification::NEW end end end @@ -705,20 +728,20 @@ def content_type 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. + # Returns the name extension of the Obj (the part after the last dot). + # Returns an empty string if the name of the Obj doesn't have an extension. # @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. + # Returns the body (main content) of the Obj for non-binary objects. + # Returns +nil+ for binary objects. # @return [String] # @api public def body if binary? nil @@ -726,37 +749,37 @@ read_attribute('body') end end # @api public - # This method is intended for Objs that represent binary resources like - # images or pdf documents. If this Obj represents a binary file, an instance + # This method is intended for CMS objects that represent binary resources like + # images or PDF documents. If the Obj represents a binary file, an instance # of {Binary} is returned. # - # This method returns the attribute +blob+ if it is of the type +binary+. + # This method returns the +blob+ attribute if its type is +binary+. # # @return [Binary, nil] def binary self[:blob] if self[:blob].is_a?(Binary) end # @api public - # This method returns the length in bytes of the binary of this obj - # @return [Fixnum] If no binary is set it will return 0 + # This method returns the byte length of the binary of the Obj. + # @return [Fixnum] If no binary is set, 0 is returned. def binary_length binary.try(:content_length) || 0 end # @api public - # This method returns the content type of the binary of this obj if it is set. + # This method returns the content type of the binary of this Obj if the binary 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 + # 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 @@ -794,11 +817,11 @@ widget_data = widget_data_from_pool(widget_id) instantiate_widget(widget_id, widget_data) if widget_data end # @api public - # Allows accessing the {Scrivito::BasicWidget Widgets} of this Obj + # Allows accessing the {Scrivito::BasicWidget Widgets} of this Obj. # # @example Access a widget by its id # obj.widgets['widget_id'] # # @return [Scrivito::WidgetCollection] @@ -814,13 +837,13 @@ # # Reverts all changes made to the +Obj+ in the current workspace. # # @api public # - # @note This method does not support +Obj+s, which are +new+. + # @note This method does not support +Obj+s that are +new+. # Please use {Scrivito::BasicObj#destroy Obj#destroy} to destroy them. - # @note This method does not support +Obj+s, which are +deleted+. + # @note This method does not support +Obj+s that are +deleted+. # Please use {Scrivito::BasicObj.restore Obj.restore} to restore them. # # @raise [ScrivitoError] If the current workspace is +published+. # @raise [ScrivitoError] If the +Obj+ is +new+. # @raise [ScrivitoError] If the +Obj+ is +deleted+. @@ -914,14 +937,10 @@ def parent_path ParentPath.of(path) unless root? end - def as_client_json - data_from_cms.to_h.except(*GENERATED_ATTRIBUTES) - end - def outdated? return false if workspace.published? base_revision = workspace.base_revision published_revision = Workspace.published.revision @@ -930,12 +949,68 @@ return true if has_conflict? cms_data_for_revision(base_revision) != cms_data_for_revision(published_revision) end + def transfer_modifications_to(target_workspace) + return unless modification + if in_revision(target_workspace.revision).try(:modification) + raise TransferModificationsError, "Already modified in workspace #{target_workspace.id}" + end + copy_modifications_to(target_workspace) + reset_modifications + end + private + def copy_modifications_to(target_workspace) + case + when new? then create_in(target_workspace) + when deleted? then destroy_in(target_workspace) + else update_in(target_workspace) + end + end + + def create_in(target_workspace) + target_workspace.api_request(:post, '/objs', obj: get_attributes) + end + + def update_in(target_workspace) + update_attributes = fill_in_missing_attributes_as_nil(copyable_attributes) + target_workspace.api_request(:put, "/objs/#{id}", obj: update_attributes) + end + + def destroy_in(target_workspace) + target_workspace.api_request(:delete, "/objs/#{id}") + end + + def fill_in_missing_attributes_as_nil(attributes, type_computer=Obj.type_computer) + obj_class = type_computer.compute_type_without_fallback(attributes['_obj_class']) + missing_attributes = obj_class.attribute_definitions.map(&:name) - attributes.keys + + if attributes['_widget_pool'] + attributes['_widget_pool'].each do |id, widget_attributes| + attributes['_widget_pool'][id] = fill_in_missing_attributes_as_nil( + widget_attributes, Widget.type_computer) + end + end + + missing_attributes.each do |attribute_name| + attributes[attribute_name] = nil + end + + attributes + end + + def reset_modifications + case + when new? then destroy + when deleted? then self.class.restore(id) + else revert + end + end + def cms_data_for_revision(revision) return nil unless revision result = CmsBackend.instance.find_obj_data_by(revision, "id", [id]) obj_data = result.first.first @@ -979,15 +1054,40 @@ def prepare_attributes_for_rest_api(attributes) self.class.prepare_attributes_for_rest_api(attributes, self) end def copyable_attributes + get_attributes.except(*GENERATED_ATTRIBUTES).except(*UNIQ_ATTRIBUTES) + end + + def get_attributes workspace.api_request(:get, "/objs/#{id}") - .except(*GENERATED_ATTRIBUTES) - .except(*UNIQ_ATTRIBUTES) end + def copy_binaries(attributes) + attribute_defintions = self.class.find_attribute_definitions(obj_class_name) + destination_obj_id = attributes.fetch(:_id) + + Hash[attributes.map do |name, value| + if value && attribute_defintions[name].try(:type) == 'binary' + binary = self[name] + [name, ['binary', copy_binary(destination_obj_id, binary)]] + else + [name, value] + end + end] + end + + def copy_binary(destination_obj_id, binary) + normalized_id = CmsRestApi.normalize_path_component(binary.id) + CmsRestApi.put("/blobs/#{normalized_id}/copy", { + destination_obj_id: destination_obj_id, + filename: binary.filename, + content_type: binary.content_type, + }) + end + def assert_revertable workspace.assert_revertable if modification == Modification::NEW || modification == Modification::DELETED raise ScrivitoError, "cannot revert changes, since obj is #{modification}." end @@ -1037,27 +1137,31 @@ 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) end - def prepare_attributes_for_rest_api(obj_attributes, obj = nil) - widget_pool_attributes = CmsRestApi::WidgetExtractor.call(obj_attributes, obj) + def prepare_attributes_for_rest_api(attributes, obj = nil) + widget_pool_attributes, obj_attributes = CmsRestApi::WidgetExtractor.call(attributes, obj) + obj_id = obj ? obj.id : obj_attributes.fetch(:_id) workspace = obj ? obj.revision.workspace : Workspace.current - api_attributes = serialize_attributes(obj_attributes, widget_pool_attributes, workspace) + api_attributes = serialize_attributes( + obj_attributes, widget_pool_attributes, workspace, obj_id + ) + if obj widget_pool = api_attributes['_widget_pool'] widget_gc = WidgetGarbageCollection.new(obj, {obj => obj_attributes}.merge(widget_pool_attributes)) widget_gc.widgets_to_delete.each { |widget| widget_pool[widget.id] = nil } end [api_attributes, widget_pool_attributes] end - def serialize_attributes(obj_attributes, widget_pool_attributes, workspace) - serializer = AttributeSerializer.new(obj_attributes['_obj_class'] || name) + def serialize_attributes(obj_attributes, widget_pool_attributes, workspace, obj_id) + serializer = AttributeSerializer.new(obj_attributes['_obj_class'] || name, obj_id) serialized_attributes = serialize_obj_attributes(serializer, obj_attributes) serialized_attributes['_widget_pool'] = serialize_widget_pool_attributes(serializer, widget_pool_attributes) serialized_attributes end @@ -1077,9 +1181,13 @@ end end def find_attribute_definitions(obj_class, basic_class = self) basic_class.type_computer.compute_type(obj_class).attribute_definitions if obj_class + end + + def with_default_id_attribute(attributes) + attributes.reverse_merge(_id: SecureRandom.hex(8)) end end end end