require 'json' require 'ostruct' require 'active_model/naming' module Scrivito # # The abstract base class for CMS objects. # # 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) SYSTEM_ATTRIBUTES = AttributeDefinitionCollection.new( '_id' => PublicSystemAttributeDefinition.new(:_id, :string), '_last_changed' => PublicSystemAttributeDefinition.new(:_last_changed, :date), '_obj_class' => PublicSystemAttributeDefinition.new(:_obj_class, :string), '_path' => PublicSystemAttributeDefinition.new(:_path, :string), '_permalink' => PublicSystemAttributeDefinition.new(:_permalink, :string), '_conflicts' => AttributeDefinition.new(:_conflicts, nil), '_modification' => AttributeDefinition.new(:_modification, nil), '_widget_pool' => AttributeDefinition.new(:_widget_pool, nil), ) UNIQ_ATTRIBUTES = %w[ _id _path _permalink ].freeze GENERATED_ATTRIBUTES = %w[ _conflicts _last_changed _modification _obj_type _text_links ].freeze extend ActiveModel::Naming extend AttributeContent::ClassMethods include AttributeContent include ModelIdentity def self.type_computer @_type_computer ||= TypeComputer.new(Scrivito::BasicObj, ::Obj) end def self.reset_type_computer! @_type_computer = nil end # # Create a new {Scrivito::BasicObj Obj} in the CMS. # # @api public # # 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 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 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. # Obj.create(:reference => other_obj) # # @example You can upload files by passing a Ruby File object. # Obj.create(:blob => File.new("image.png")) # # @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'), # internal_link: Link.new(obj: other_obj, title: 'Other Obj') # ) # # @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 attributes can be set by passing a {String} value. # Obj.create(:title => "My Title") # # @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 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 copy of a widget # Obj.create(:widgets => [another_obj.widgets.first.copy]) # # # 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) api_attributes, widget_properties = prepare_attributes_for_rest_api(attributes) workspace = Workspace.current obj_data = workspace.create_obj(obj: api_attributes) obj = BasicObj.instantiate(obj_data) obj.revision = workspace.revision CmsRestApi::WidgetExtractor.notify_persisted_widgets(obj, widget_properties) obj end end # Create a new {Scrivito::BasicObj Obj} instance with the given values and attributes. # 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 def id read_attribute('_id') end def revision=(revision) raise "cannot change revision once set!" if @revision @revision = revision end def revision @revision or raise "revision not set!" end ### FINDERS #################### # 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]id_or_list # @return [Obj, Array] # @api public def self.find(id_or_list) Workspace.current.objs.find(id_or_list) end def self.find_by_id(id) Workspace.current.objs.find_by_id(id) end # 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]id_or_list # @return [Obj, Array] # @api public def self.find_including_deleted(id_or_list) Workspace.current.objs.find_including_deleted(id_or_list) end # Returns an {Scrivito::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 this # subclass. # # {Scrivito::ObjSearchEnumerator}s can be chained using one of the chainable methods # (e.g. {Scrivito::ObjSearchEnumerator#and} and {Scrivito::ObjSearchEnumerator#and_not}). # # @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] field See {Scrivito::ObjSearchEnumerator#and} # for details # @param [Symbol, String] operator See {Scrivito::ObjSearchEnumerator#and} for details # @param [String, Array] value See {Scrivito::ObjSearchEnumerator#and} for details # @param [Hash] boost See {Scrivito::ObjSearchEnumerator#and} for details # @raise [ScrivitoError] if called directly on {Scrivito::BasicObj}. Use +Obj.where+ instead. # @return [Scrivito::ObjSearchEnumerator] # @api public def self.where(field, operator, value, boost = nil) assert_not_basic_obj('.where') if self == ::Obj Workspace.current.objs.where(field, operator, value, boost) else Workspace.current.objs.where(:_obj_class, :equals, name) .and(field, operator, value, boost) end end # Returns an {Scrivito::ObjSearchEnumerator} of all {Scrivito::BasicObj Obj}s. # If invoked on a subclass of Obj, the result is restricted to instances of this subclass. # @return [Scrivito::ObjSearchEnumerator] # @raise [ScrivitoError] if called directly on +Scrivito::BasicObj+. Use +Obj.all+ instead. # @api public def self.all assert_not_basic_obj('.all') if self == ::Obj Workspace.current.objs.all else find_all_by_obj_class(name) end end # Returns an {Scrivito::ObjSearchEnumerator} of all CMS objects with the given +obj_class+. # @param [String] obj_class name of the object class. # @return [Scrivito::ObjSearchEnumerator] # @api public def self.find_all_by_obj_class(obj_class) Workspace.current.objs.find_all_by_obj_class(obj_class) end # Find the {Scrivito::BasicObj Obj} with the given path. # Returns +nil+ if no matching Obj exists. # @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 {Scrivito::BasicObj Obj} with the given name. # 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 an {Scrivito::ObjSearchEnumerator} of all CMS objects with the given name. # @param [String] name Name of the {Scrivito::BasicObj Obj}. # @return [Scrivito::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. # @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 raises # {Scrivito::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 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 whose path is +/products/shoes+, the argument must be +/products+. # The given parent path can also be +NilClass+. # # If +NilClass+ is returned, all page classes are made available. # By default, +NilClass+ is returned. # # 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] # @api public def self.valid_page_classes_beneath(parent_path) end def self.valid_page_ruby_classes_beneath(parent_path) result = assert_classes(valid_page_classes_beneath(parent_path), '.valid_page_classes_beneath') (result || Scrivito.models.pages.to_a).uniq end # Update the {Scrivito::BasicObj Obj} using the attributes provided. # # 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 +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] # } # } # ) def update(attributes) api_attributes, widget_properties = prepare_attributes_for_rest_api(attributes) update_data(workspace.update_obj(id, obj: api_attributes)) CmsRestApi::WidgetExtractor.notify_persisted_widgets(self, widget_properties) self end # 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: # 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') attributes_for_copy = self.class.with_default_id_attribute(copyable_attributes) attributes_for_copy = attributes_for_copy.with_indifferent_access.merge(options) attributes_for_copy = copy_binaries(attributes_for_copy) json = workspace.api_request(:post, '/objs', obj: attributes_for_copy) self.class.find(json['_id']) end # Destroys the {Scrivito::BasicObj Obj} in the current {Scrivito::Workspace}. # @api public def destroy if children.any? raise ClientError.new(I18n.t('scrivito.errors.models.basic_obj.has_children')) end workspace.api_request(:delete, "/objs/#{id}") end def to_param id end # 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 the parent of this object. # @return [Array] # @api public def ancestors return [] unless child_path? ancestor_paths = parent_path.scan(/\/[^\/]+/).inject([""]) do |list, component| list << list.last + component end ancestor_paths[0] = "/" Workspace.current.objs.find_by_paths(ancestor_paths) end # Returns a list of all child {Scrivito::BasicObj Obj}s. # @return [Array] # @api public def children return [] unless path workspace.objs.find_by_parent_path(path) end ### ATTRIBUTES ################# # Returns the path of the {Scrivito::BasicObj Obj} as a String. # @api public def path read_attribute('_path') end # 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}, 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 def self.generate_widget_pool_id SecureRandom.hex(4) end # Returns the permalink of the {Scrivito::BasicObj Obj}. # @api public def permalink read_attribute('_permalink') end # 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 end # 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 # 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 overriding {#slug}. # @return [String] # @api public def slug (title || '').parameterize end # # 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 end # # Calculates an 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] falls back to +Obj.description_for_editor+. # def display_title (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 the +Obj+. # # You can customize this part by overriding {#alt_description}. # # @return [String] # @api public def alt_description title end # @api public def title read_attribute('title') end # @api public # 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 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 objects, this method decides whether the image transformations should # be applied by default. # # @api public # # By default, this method returns +false+. # Override it in subclasses to fit your needs. # # @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 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 useful for creating navigations. # @return [Array] # @api public def toclist(*args) return [] if binary? toclist = children toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all) toclist end # @api public # # Returns a list of children that are sorted according to the order specifed in the # +child_order+ attribute. The same sort order is used when rendering navigations using # the {ScrivitoHelper#scrivito_tag_list scrivito_tag_list} helper. # # @return [Array] def sorted_toclist if sortable_toclist? (child_order & toclist) + (toclist - child_order).sort_by(&:id) else toclist end end def sortable_toclist? has_attribute?(:child_order) end # 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 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) else super end end def has_attribute?(key) key = key.to_s if SYSTEM_KEYS.include?(key) true else super end end # 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 end def reload_data id = self.id.to_s update_data -> { CmsBackend.find_obj_data_by(workspace.revision, :id, [id]).first.first } end # @api public def last_changed read_attribute('_last_changed') end def new?(revision=workspace.base_revision) quick_modification(revision) == "new" end def deleted?(revision=workspace.base_revision) quick_modification(revision) == "deleted" end def modification(revision=workspace.base_revision) quick_modification = quick_modification(revision) if ObjData === quick_modification if data_from_cms == quick_modification Modification::UNMODIFIED else Modification::EDITED end else quick_modification end end # 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 # UNMODIFIED or EDITED. Which one it is can be determined by comparing # the returned ObjData. def quick_modification(revision) return Modification::UNMODIFIED unless revision modification_attr = read_attribute('_modification') return modification_attr if revision == workspace.base_revision 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 Modification::DELETED else # Obj exists in both revisions, leave the actual comparions # up to the caller data_for_comparison end else if modification_attr == "deleted" # Obj does not exist in either revision Modification::UNMODIFIED else # Obj exists in current but not in comparision revision Modification::NEW end end end def widget_data_for_revision(id, revision) if revision_obj_data = cms_data_for_revision(revision) revision_obj_data.value_of('_widget_pool')[id] end end def in_revision(revision) if obj_data = cms_data_for_revision(revision) obj = Obj.instantiate(obj_data) obj.revision = revision obj end end 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 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 objects. # Returns +nil+ for binary objects. # @return [String] # @api public def body if binary? nil else read_attribute('body') end end # @api public # 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 +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 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 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 # available to the public if the binary is set. # # See {Scrivito::Binary#url} for details # @return [String, nil] def binary_url binary.try(:url) end # @api public # # Allows accessing the meta data for binary +Objs+. # # @example Accessing meta data # obj.meta_data['width'] # => 150 # obj.meta_data['content_type'] # => 'image/jpeg' # # @see Scrivito::MetaDataCollection List of available meta data attributes. # # @return [Scrivito::MetaDataCollection] The collection of available meta data def meta_data binary.try(:meta_data) || MetaDataCollection.new({}) 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 raise "The method `body_content_type' was removed. Please use `binary_content_type' instead" end def inspect "<#{self.class} id=\"#{id}\" path=\"#{path}\">" 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 # @api public # Allows accessing the {Scrivito::BasicWidget Widgets} of this Obj. # # @example Access a widget by its id # obj.widgets['widget_id'] # # @return [Scrivito::WidgetCollection] def widgets @widgets ||= WidgetCollection.new(self) end # for internal testing purposes only def blob_id find_blob.try(:id) end # # Reverts all changes made to the +Obj+ in the current workspace. # # @api public # # @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 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+. # def revert assert_revertable if modification == Modification::EDITED base_revision_path = "revisions/#{workspace.base_revision_id}/objs/#{id}" previous_attributes = CmsRestApi.get(base_revision_path).except('_id') previous_widget_pool = previous_attributes['_widget_pool'] ids_of_new_widgets = read_widget_pool.keys - previous_widget_pool.keys ids_of_new_widgets.each { |widget_id| previous_widget_pool[widget_id] = nil } previous_attributes = reset_blank_attributes(previous_attributes) workspace.api_request(:put, "/objs/#{id}", obj: previous_attributes) reload end end def restore_widget(widget_id) Workspace.current.assert_revertable return if modification == Modification::UNMODIFIED if modification == Modification::DELETED raise ScrivitoError, 'Can not restore a widget inside a deleted obj' end unless widget = in_revision(Workspace.current.base_revision).widgets[widget_id] raise ResourceNotFound, "Could not find widget with id #{widget_id}" end container = widget.container if container.kind_of?(BasicWidget) && !widgets[container.id] raise ScrivitoError, 'Can not restore a widget inside a deleted widget' end widget_copy = widget.copy_for_restore(read_widget_pool.keys) attribute_name = widget.container_attribute_name current_container = widgets[container.id] || self current_container.update(attribute_name => current_container[attribute_name].insert(widget.container_attribute_index, widget_copy)) end def mark_resolved workspace.api_request(:put, "/objs/#{id}", obj: {_conflicts: nil}) reload end def find_container_and_attribute_name_for_widget(widget_id) if attribute_name = find_attribute_containing_widget(widget_id) return [self, attribute_name] end contained_widgets.each do |container_widget| if attribute_name = container_widget.find_attribute_containing_widget(widget_id) return [container_widget, attribute_name] end end [nil, nil] end def widget_data_from_pool(widget_id) read_widget_pool[widget_id] end def has_conflict? read_attribute('_conflicts') != nil end def publishable? !has_conflict? end def generate_widget_pool_id 10.times do id = self.class.generate_widget_pool_id return id if widget_data_from_pool(id).nil? end raise ScrivitoError.new('Could not generate a new unused widget id') end def contained_widgets read_widget_pool.keys.map(&method(:widget_from_pool)) end def parent_path ParentPath.of(path) unless root? end def outdated? return false if workspace.published? base_revision = workspace.base_revision published_revision = Workspace.published.revision return false if base_revision == published_revision 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 has_conflict? || future_conflict?(target_workspace) raise TransferModificationsConflictError, "Transfer will result in a conflict. " \ "Please update the current and target workspace and ensure they are conflict free." end if in_revision(target_workspace.revision).try(:modification) raise TransferModificationsModifiedError, "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 future_conflict?(target_workspace) base_revision = workspace.base_revision target_base_revision = target_workspace.base_revision base_revision != target_base_revision && cms_data_for_revision(base_revision) != cms_data_for_revision(target_base_revision) end def create_in(target_workspace) target_workspace.api_request(:post, '/objs', obj: get_attributes) end def update_in(target_workspace) update_attributes = reset_blank_attributes(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 reset_modifications case when new? then workspace.api_request(:delete, "/objs/#{id}") when deleted? then self.class.restore(id) else revert end end def cms_data_for_revision(revision) return nil unless revision result = CmsBackend.find_obj_data_by(revision, "id", [id]) obj_data = result.first.first if obj_data && obj_data.value_of("_modification") != "deleted" obj_data end end def read_widget_pool read_attribute('_widget_pool') 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 as_date(value) DateConversion.deserialize_from_backend(value) unless value.nil? end def find_blob read_attribute('blob') end def workspace if revision.workspace revision.workspace else raise ScrivitoError, "No workspace set for this obj" end end def child_path? !path.nil? && !root? end 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}") end def copy_binaries(attributes) attribute_defintions = self.class.find_attribute_definitions(obj_class) 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 end def binary_title binary.filename if binary? && binary end def has_system_attribute?(attribute_name) !!SYSTEM_ATTRIBUTES[attribute_name] end def has_public_system_attribute?(attribute_name) SYSTEM_ATTRIBUTES[attribute_name].is_a?(PublicSystemAttributeDefinition) end def type_of_system_attribute(attribute_name) SYSTEM_ATTRIBUTES[attribute_name].try(:type) end def value_of_system_attribute(attribute_name) attribute_value = data_from_cms.value_of(attribute_name) if attribute_name == '_last_changed' DateConversion.deserialize_from_backend(attribute_value) else attribute_value end end def reset_blank_attributes(attributes) widget_pool = attributes['_widget_pool'] widget_pool.each do |id, widget_attributes| if widget_attributes widget_pool[id] = Widget.reset_blank_attributes(widget_attributes) end end self.class.reset_blank_attributes(attributes) 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 # # Restores a previously deleted +Obj+. # # @api public # # @raise [ScrivitoError] If the current workspace is +published+. 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) end 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, obj_id ) [api_attributes, widget_pool_attributes] end 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 def serialize_obj_attributes(serializer, obj_attributes) serializer.serialize(obj_attributes, find_attribute_definitions(obj_attributes['_obj_class']) || attribute_definitions) end def serialize_widget_pool_attributes(serializer, widget_pool_attributes) {}.tap do |serialized_attributes| widget_pool_attributes.each_pair do |widget, widget_attributes| obj_class = widget_attributes['_obj_class'] widget_serializer = AttributeSerializer.new(obj_class, serializer.obj_id) serialized_attributes[widget.id] = widget_serializer.serialize(widget_attributes, find_attribute_definitions(obj_class, BasicWidget) || widget.attribute_definitions) end 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