# -*- coding: utf-8 -*- """Module that provides functionality for content manipulation.""" from Products.Archetypes.interfaces.base import IBaseObject from Products.CMFCore.interfaces import ISiteRoot from Products.CMFCore.WorkflowCore import WorkflowException from plone.api import portal from plone.api.exc import InvalidParameterError from plone.api.validation import at_least_one_of from plone.api.validation import mutually_exclusive_parameters from plone.api.validation import required_parameters from plone.app.uuid.utils import uuidToObject from plone.uuid.interfaces import IUUID from zope.component import getMultiAdapter from zope.component import getSiteManager from zope.container.interfaces import INameChooser from zope.interface import Interface from zope.interface import providedBy import random import transaction _marker = [] @required_parameters('container', 'type') @at_least_one_of('id', 'title') def create( container=None, type=None, id=None, title=None, safe_id=False, **kwargs ): """Create a new content item. :param container: [required] Container object in which to create the new object. :type container: Folderish content object :param type: [required] Type of the object. :type type: string :param id: Id of the object. If the id conflicts with another object in the container, a suffix will be added to the new object's id. If no id is provided, automatically generate one from the title. If there is no id or title provided, raise a ValueError. :type id: string :param title: Title of the object. If no title is provided, use id as the title. :type title: string :param safe_id: When False, the given id will be enforced. If the id is conflicting with another object in the target container, raise an InvalidParameterError. When True, choose a new, non-conflicting id. :type safe_id: boolean :returns: Content object :raises: KeyError, :class:`~plone.api.exc.MissingParameterError`, :class:`~plone.api.exc.InvalidParameterError` :Example: :ref:`content_create_example` """ # Create a temporary id if the id is not given content_id = not safe_id and id or str(random.randint(0, 99999999)) if title: kwargs['title'] = title try: container.invokeFactory(type, content_id, **kwargs) except UnicodeDecodeError: # UnicodeDecodeError is a subclass of ValueError, # so will be swallowed below unless we re-raise it here raise except ValueError as e: if ISiteRoot.providedBy(container): allowed_types = container.allowedContentTypes() types = [allowed_type.id for allowed_type in allowed_types] else: try: types = container.getLocallyAllowedTypes() except AttributeError: raise InvalidParameterError( "Cannot add a '%s' object to the container." % type ) raise InvalidParameterError( "Cannot add a '{0}' object to the container.\n" "Allowed types are:\n" "{1}\n" "{2}".format(type, '\n'.join(sorted(types)), e.message) ) content = container[content_id] # Archetypes specific code if IBaseObject.providedBy(content): # Will finish Archetypes content item creation process, # rename-after-creation and such content.processForm() if not id or (safe_id and id): # Create a new id from title chooser = INameChooser(container) derived_id = id or title new_id = chooser.chooseName(derived_id, content) # kacee: we must do a partial commit, else the renaming fails because # the object isn't in the zodb. # Thus if it is not in zodb, there's nothing to move. We should # choose a correct id when # the object is created. # maurits: tests run fine without this though. transaction.savepoint(optimistic=True) content.aq_parent.manage_renameObject(content_id, new_id) return content @mutually_exclusive_parameters('path', 'UID') @at_least_one_of('path', 'UID') def get(path=None, UID=None): """Get an object. :param path: Path to the object we want to get, relative to the portal root. :type path: string :param UID: UID of the object we want to get. :type UID: string :returns: Content object :raises: ValueError, :Example: :ref:`content_get_example` """ if path: site = portal.get() site_absolute_path = '/'.join(site.getPhysicalPath()) if not path.startswith('{0}'.format(site_absolute_path)): path = '{0}{1}'.format(site_absolute_path, path) try: return site.restrictedTraverse(path) except (KeyError, AttributeError): return None # When no object is found don't raise an error elif UID: return uuidToObject(UID) @required_parameters('source') @at_least_one_of('target', 'id') def move(source=None, target=None, id=None, safe_id=False): """Move the object to the target container. :param source: [required] Object that we want to move. :type source: Content object :param target: Target container to which the source object will be moved. If no target is specified, the source object's container will be used as a target, effectively making this operation a rename (:ref:`content_rename_example`). :type target: Folderish content object :param id: Pass this parameter if you want to change the id of the moved object on the target location. If the new id conflicts with another object in the target container, a suffix will be added to the moved object's id. :type id: string :param safe_id: When False, the given id will be enforced. If the id is conflicting with another object in the target container, raise a InvalidParameterError. When True, choose a new, non-conflicting id. :type safe_id: boolean :returns: Content object that was moved to the target location :raises: KeyError ValueError :Example: :ref:`content_move_example` """ source_id = source.getId() # If no target is given the object is probably renamed if target: target.manage_pasteObjects( source.aq_parent.manage_cutObjects(source_id)) else: target = source if id: return rename(obj=target[source_id], new_id=id, safe_id=safe_id) else: return target[source_id] @required_parameters('obj', 'new_id') def rename(obj=None, new_id=None, safe_id=False): """Rename the object. :param obj: [required] Object that we want to rename. :type obj: Content object :param new_id: New id of the object. :type new_id: string :param safe_id: When False, the given id will be enforced. If the id is conflicting with another object in the container, raise a InvalidParameterError. When True, choose a new, non-conflicting id. :type safe_id: boolean :returns: Content object that was renamed :Example: :ref:`content_rename_example` """ obj_id = obj.getId() if safe_id: try: chooser = INameChooser(obj) except TypeError: chooser = INameChooser(obj.aq_parent) new_id = chooser.chooseName(new_id, obj) obj.aq_parent.manage_renameObject(obj_id, new_id) return obj.aq_parent[new_id] @required_parameters('source') @at_least_one_of('target', 'id') def copy(source=None, target=None, id=None, safe_id=False): """Copy the object to the target container. :param source: [required] Object that we want to copy. :type source: Content object :param target: Target container to which the source object will be moved. If no target is specified, the source object's container will be used as a target. :type target: Folderish content object :param id: Id of the copied object on the target location. If no id is provided, the copied object will have the same id as the source object - however, if the new object's id conflicts with another object in the target container, a suffix will be added to the new object's id. :type id: string :param safe_id: When True, the given id will be enforced. If the id is conflicting with another object in the target container, raise a InvalidParameterError. When True, choose a new, non-conflicting id. :type safe_id: boolean :returns: Content object that was created in the target location :raises: KeyError, ValueError :Example: :ref:`content_copy_example` """ source_id = source.getId() if target is None: target = source.aq_parent target.manage_pasteObjects(source.aq_parent.manage_copyObjects(source_id)) if id: return rename(obj=target[source_id], new_id=id, safe_id=safe_id) else: return target[source_id] @required_parameters('obj') def delete(obj=None): """Delete the object. :param obj: [required] Object that we want to delete. :type obj: Content object :raises: ValueError :Example: :ref:`content_delete_example` """ obj.aq_parent.manage_delObjects([obj.getId()]) @required_parameters('obj') def get_state(obj=None, default=_marker): """Get the current workflow state of the object. :param obj: [required] Object that we want to get the state for. :type obj: Content object :param default: Returned if no workflow is defined for the object. :returns: Object's current workflow state, or `default`. :rtype: string :raises: Products.CMFCore.WorkflowCore.WorkflowException :Example: :ref:`content_get_state_example` """ workflow = portal.get_tool('portal_workflow') if default is not _marker and not workflow.getWorkflowsFor(obj): return default # This still raises WorkflowException when the workflow state is broken, # ie 'review_state' is absent return workflow.getInfoFor(ob=obj, name='review_state') @required_parameters('obj', 'transition') def transition(obj=None, transition=None): """Perform a workflow transition for the object. :param obj: [required] Object for which we want to perform the workflow transition. :type obj: Content object :param transition: [required] Name of the workflow transition. :type transition: string :raises: :class:`~plone.api.exc.MissingParameterError`, :class:`~plone.api.exc.InvalidParameterError` :Example: :ref:`content_transition_example` """ workflow = portal.get_tool('portal_workflow') try: workflow.doActionFor(obj, transition) except WorkflowException: transitions = [ action['id'] for action in workflow.listActions(object=obj) ] raise InvalidParameterError( "Invalid transition '{0}'.\n" "Valid transitions are:\n" "{1}".format(transition, '\n'.join(sorted(transitions))) ) @required_parameters('name', 'context', 'request') def get_view(name=None, context=None, request=None): """Get a BrowserView object. :param name: [required] Name of the view. :type name: string :param context: [required] Context on which to get view. :type context: context object :param request: [required] Request on which to get view. :type request: request object :raises: :class:`~plone.api.exc.MissingParameterError`, :class:`~plone.api.exc.InvalidParameterError` :Example: :ref:`content_get_view_example` """ try: return getMultiAdapter((context, request), name=name) except: # get a list of all views so we can display their names in the error # msg sm = getSiteManager() views = sm.adapters.lookupAll( required=(providedBy(context), providedBy(request)), provided=Interface, ) views_names = [view[0] for view in views] raise InvalidParameterError( "Cannot find a view with name '{0}'.\n" "Available views are:\n" "{1}".format(name, '\n'.join(sorted(views_names))) ) @required_parameters('obj') def get_uuid(obj=None): """Get the object's Universally Unique IDentifier (UUID). :param obj: [required] Object we want its UUID. :type obj: Content object :returns: Object's UUID :rtype: string :raises: ValueError :Example: :ref:`content_get_uuid_example` """ return IUUID(obj)