require 'active_model/naming' module Scrivito # @api public # This class represents a CMS workspace, called "working copy" in the UI. A working copy lets # editors create and modify content independently of the published content or other working copies. # On creation, a working copy is based on the currently published content. # @see https://scrivito.com/about-working-copies About working copies class Workspace extend ActiveModel::Naming include ModelIdentity PublishPreventedDueToContentChange = Class.new(ScrivitoError) # Set the workspace to use for subsequent workspace operations. # @api public # @param [Scrivito::Workspace] workspace def self.current=(workspace) Thread.current[:scrivito_current_workspace] = workspace end def self.current_using_proc=(workspace_proc) Thread.current[:scrivito_current_workspace] = workspace_proc end # Returns the currently used workspace. # @api public # @return [Scrivito::Workspace] def self.current workspace = Thread.current[:scrivito_current_workspace] if workspace.respond_to?(:call) Thread.current[:scrivito_current_workspace] = workspace.call else Thread.current[:scrivito_current_workspace] ||= published end end # Returns all workspaces. # @api public # @return [Array] def self.all result_json = CmsRestApi.get('/workspaces') result_json['results'].map do |raw_data| Workspace.new(WorkspaceData.new(raw_data)) end end # @api public # Returns the published workspace. # # @return [Scrivito::Workspace] def self.published find("published") end def self.published_with_fallback cached_workspace_data = CmsBackend.find_workspace_data_from_cache('published') if cached_workspace_data workspace_data = begin CmsBackend.find_workspace_data_by_id('published', 0.5) rescue => e warn_backend_not_available(e.message) cached_workspace_data end from_workspace_data('published', workspace_data) else published end end # Find a workspace by its id. # @api public # @param [String] id # @return [Scrivito::Workspace] # @raise [Scrivito::ResourceNotFound] def self.find(id) cache.fetch(id) do workspace_data = CmsBackend.find_workspace_data_by_id(id) from_workspace_data(id, workspace_data) end end # Find a workspace by its title. # If multiple workspaces share the same title, one of them is returned. # If no workspace with the given title can be found, +nil+ is returned. # @api public # @param [String] title # @return [Scrivito::Workspace] def self.find_by_title(title) all.detect { |workspace| workspace.title == title } end # # Find a workspace by its title or ID and set it as the currently used workspace. # # @api public # # @param [String] title_or_id title or id of the workspace # @raise [Scrivito::ResourceNotFound] # @note This method is intended to be used in the Rails console. Please avoid using it in # application code. # # @example # Scrivito::Workspace.use("my working copy") # Scrivito::Workspace.current.title # # => "my working copy" # # Scrivito::Workspace.use("6a75fe694eeeb093") # Scrivito::Workspace.current.id # # => "6a75fe694eeeb093" # # # Raises Scrivito::ResourceNotFound: # Scrivito::Workspace.use("missing") # def self.use(title_or_id) self.current = find_by_title(title_or_id) || find(title_or_id) rescue ResourceNotFound raise ResourceNotFound, %{Could not find #{self} with title or ID "#{title_or_id}"} end delegate :content_state_id, :base_content_state_id, :content_state, :base_revision_id, :auto_update?, :base_content_state, to: :data # Create a new workspace. # @api public # @param [Hash] attributes # @option attributes [String] :title title of the workspace. # @option attributes [Boolean] :auto_update Specifies whether changes published intermediately # are applied automatically and instantly to the working copy. The default is +true+. Setting # this option to +false+ requires {#rebase rebasing} the working copy (manually) to keep it up # to date. # @example Create a workspace, providing its title: # Scrivito::Workspace.create(title: "Jane") # @example Create a workspace, providing auto_update: # # Workspace will not be updated until it is published. # Scrivito::Workspace.create(auto_update: false) # # @return [Scrivito::Workspace] def self.create(attributes) find(create_async(attributes).result['id']) end def self.create_async(attributes) Task.new do attributes = attributes.reverse_merge(auto_update: true) CmsRestApi.task_unaware_request(:post, 'workspaces', workspace: attributes) end end # Reloads the current workspace to reflect the changes that were made to it concurrently # since it was loaded. # @api public def self.reload current.reload end # @api public # # This method provides a string that can be used as part of a cache key. It changes # whenever any content ({Scrivito::BasicObj Obj} or {Scrivito::BasicWidget Widget}) # changes. Due to this, caches using the +cache_key+ are invalidated whenever # a CMS object in the working copy has been changed. # # Scrivito provides the {ScrivitoHelper#scrivito_cache scrivito_cache} method which # integrates the +cache_key+ with Rails' fragment caching. You might want to check whether # +scrivito_cache+ satisfies your needs before implementing your own solution. # # @return [String] A string that changes whenever the content of the working copy # changes. def cache_key @cache_key ||= Digest::SHA1.hexdigest("#{id}|#{content_state_id}") end def self.cache @cache ||= {} end def initialize(workspace_data) update_data(workspace_data) @id = data.id end # Reloads this workspace to reflect the changes that were made to it concurrently since # it was loaded. # @api public def reload update_data(method(:fetch_workspace_data)) end def eager_reload update_data(fetch_workspace_data) end def api_request(verb, path, payload = nil) response = CmsRestApi.public_send(verb, "#{backend_url}#{path}", payload) reload if [:post, :put, :delete].include?(verb) response end def create_obj(attributes) update_obj(attributes[:obj]['_id'], attributes) end def update_obj(obj_id, attributes) CmsBackend.write_obj(id, obj_id, attributes).tap { reload } end def task_unaware_api_request(verb, path, payload = nil) CmsRestApi.task_unaware_request(verb, "#{backend_url}#{path}", payload) end # Updates the attributes of this workspace. # @api public # @param [Hash] attributes # @return [Scrivito::Workspace] def update(attributes) raise ScrivitoError, 'published workspace is not modifiable' if published? CmsRestApi.put(backend_url, workspace: attributes) reload end # Destroy this workspace. # @api public def destroy reset_workspace_if_current CmsRestApi.delete(backend_url) end # Publish the changes that were made to this workspace. # @api public def publish publish_async.result Workspace.published.reload reset_workspace_if_current end def publish_async Task.new do task_unaware_api_request(:put, '/publish') end end # Rebases the current workspace from the published content in order to integrate the changes # that were published in the meantime. # @note This action is not required for auto-updated workspaces. # @api public def rebase rebase_async.result reload end def rebase_async Task.new do task_unaware_api_request(:put, '/rebase') end end # Returns the id of the workspace. # @api public # @return [String] def id @id end def revision_id data.revision_id end # Returns the title of the workspace if present. Otherwise, and for the published content, # an empty +String+ is returned. # @api public # @return [String] def title return '' if published? data.title || '' end # @api public # Returns the memberships (users and their roles) of this workspace. # @return [Scrivito::MembershipCollection] def memberships @memberships ||= MembershipCollection.new(self) end def data if @workspace_data.respond_to?(:call) @workspace_data = @workspace_data.call else @workspace_data end end # @api public # This method indicates whether the workspace is the published one. # # @return +true+ if the workspace is the published one def published? self.id == 'published' end def outdated? !published? && Workspace.published.revision.id != base_revision_id end def revision unless data.content_state_id? # reload data from changes feed in order to obtain content_state_id reload raise InternalError unless data.content_state_id? end @revision ||= Revision.new(id: revision_id, workspace: self) end def base_revision if base_revision_id @base_revision ||= Revision.new( id: base_revision_id, workspace: self, base: true ) end end def as_current(&block) old_workspace = Workspace.current begin Workspace.current = self yield ensure Workspace.current = old_workspace end end def assert_revertable raise ScrivitoError, 'published workspace is not modifiable' if published? end # Returns an {Scrivito::ObjCollection} of this working copy for accessing its CMS objects. # @api public # @return {Scrivito::ObjCollection} def objs @objs ||= ObjCollection.new(self) end def inspect "<#{self.class} id=\"#{id}\" title=\"#{title}\">" end private def update_data(new_data) @workspace_data = new_data # Clear all cached instance variables. @base_revision = nil @memberships = nil @revision = nil @cache_key = nil end def fetch_workspace_data CmsBackend.find_workspace_data_by_id(id) end def backend_url "/workspaces/#{id}" end def reset_workspace_if_current Workspace.cache.delete(id) if Workspace.current == self Workspace.current = Workspace.published end end private_class_method def self.from_workspace_data(id, data) unless data raise ResourceNotFound, "Could not find #{self} with id #{id}" end cache[id] = Workspace.new(data) end private_class_method def self.warn_backend_not_available(error_message) Warning.warn <<-EOS Couldn't connect to backend to fetch published workspace. #{error_message} Serving from cache. EOS end end end