require 'json' require 'ostruct' require 'active_model/naming' module Scrivito # # 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 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 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}. # # @param [Hash] attributes for the new obj # @param [Hash] context in which the object creating should happen # @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} # 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 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 Dates 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 # 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 {Scrivito::BasicWidget Widgets} to change 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 # Obj.create(:widgets => [another_obj.widgets.first.copy]) # # # Changing a widget field # obj.update(:widgets => [obj.widgets.first]) # # # Clear a widget field # obj.update(:widgets => []) # def self.create(attributes = {}, context = {}) 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) json = Workspace.current.api_request(:post, '/objs', obj: api_attributes) obj = find(json['_id']) 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 Objs should be loaded 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 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]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 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]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 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. # # @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 obj class is "Pressrelease" and whose title contains "quarterly": # Obj.where(:_obj_class, :equals, 'Pressrelease').and(:title, :contains, 'quarterly').take(10) # @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 # @raise [ScrivitoError] if called directly on +BasicObj+. Use +Obj.where+ instead. # @return [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 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 directly on +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 a {ObjSearchEnumerator} of all Objs with the given +obj_class+. # @param [String] obj_class name of the obj 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 # 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 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 {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 {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. # @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 # 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. # 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+. # 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 +Array+ is returned, then it should include the desired classes. # Only this classes will be available. Order of the classes will be 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) 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. # # For an overview of which 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 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+: # 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) workspace.api_request(:put, "/objs/#{id}", obj: api_attributes) reload_data CmsRestApi::WidgetExtractor.notify_persisted_widgets(self, widget_properties) self end # Creates a copy of the +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') json = workspace.api_request(:post, '/objs', obj: copyable_attributes.merge(options)) self.class.find(json['_id']) end # 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 workspace.api_request(:delete, "/objs/#{id}") end def to_param id end # 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? 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] # @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 # return 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 {Scrivito::BasicObj Obj}'s path 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. # @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 "/" # @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. # @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. # 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. # 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: / # # 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 that is shown in the UI # and 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 this +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 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. # # Every Obj that has an attribute +blob+ of the type +binary+ 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. # # @api beta # # By default this method returns +false+. # Override in subclasses to fit your needs. # # @note Only relevant for binary Objs # @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. # @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 [] if binary? toclist = children toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all) toclist end # @param objs_to_be_sorted [Array] unsorted list of Objs # @param list [Array] list of Objs that defines the order # @return [Array] 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. 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+. # @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 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 workspace.reload reload_data end def reload_data id = self.id.to_s update_data -> { CmsBackend.instance.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 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 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 # of {Binary} is returned. # # This method returns the attribute +blob+ if it is of the type +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 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. # @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 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 details_view_path view_path('details') end def embed_view_path view_path('embed') 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, 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 +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 } 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 all_widgets_from_pool.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 all_widgets_from_pool read_widget_pool.keys.map do |widget_id| widget_from_pool(widget_id) end 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 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 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 private 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 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) DateAttribute.parse(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 workspace.api_request(:get, "/objs/#{id}") .except(*GENERATED_ATTRIBUTES) .except(*UNIQ_ATTRIBUTES) 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 view_path(view_name) "#{obj_class_name.underscore}/#{view_name}" 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) attribute_name == '_last_changed' ? DateAttribute.parse(attribute_value) : attribute_value 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(obj_attributes, obj = nil) widget_pool_attributes = CmsRestApi::WidgetExtractor.call(obj_attributes, obj) workspace = obj ? obj.revision.workspace : Workspace.current api_attributes = serialize_attributes(obj_attributes, widget_pool_attributes, workspace) 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) 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'] serialized_attributes[widget.id] = 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 end end end