src/pyams_content/workflow/basic.py
changeset 338 f37b5995a48c
child 956 a8723fffbaf6
equal deleted inserted replaced
337:9a3e4f9cc8f5 338:f37b5995a48c
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 from datetime import datetime
       
    18 
       
    19 # import interfaces
       
    20 from pyams_content.interfaces import PUBLISH_CONTENT_PERMISSION, MANAGE_SITE_ROOT_PERMISSION, WEBMASTER_ROLE, \
       
    21     PILOT_ROLE, MANAGER_ROLE, OWNER_ROLE, READER_ROLE, MANAGE_CONTENT_PERMISSION, CREATE_CONTENT_PERMISSION
       
    22 from pyams_content.shared.common.interfaces import IWfSharedContentRoles
       
    23 from pyams_content.workflow.interfaces import IBasicWorkflow
       
    24 from pyams_content.shared.common.interfaces import IManagerRestrictions
       
    25 from pyams_security.interfaces import IRoleProtectedObject
       
    26 from pyams_workflow.interfaces import IWorkflowStateLabel, IWorkflowState, IWorkflow, IWorkflowPublicationInfo, \
       
    27     IWorkflowVersions, IWorkflowInfo, ObjectClonedEvent
       
    28 
       
    29 # import packages
       
    30 from pyams_utils.adapter import adapter_config, ContextAdapter
       
    31 from pyams_utils.date import format_datetime
       
    32 from pyams_utils.registry import utility_config, get_current_registry
       
    33 from pyams_utils.request import check_request
       
    34 from pyams_workflow.workflow import Transition, Workflow
       
    35 from zope.copy import copy
       
    36 from zope.interface import implementer
       
    37 from zope.location import locate
       
    38 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
       
    39 
       
    40 from pyams_content import _
       
    41 
       
    42 
       
    43 DRAFT = 'draft'
       
    44 PUBLISHED = 'published'
       
    45 ARCHIVED = 'archived'
       
    46 DELETED = 'deleted'
       
    47 
       
    48 STATES_IDS = (DRAFT,
       
    49               PUBLISHED,
       
    50               ARCHIVED,
       
    51               DELETED)
       
    52 
       
    53 STATES_LABELS = (_("Draft"),
       
    54                  _("Published"),
       
    55                  _("Archived"),
       
    56                  _("Deleted"))
       
    57 
       
    58 STATES_VOCABULARY = SimpleVocabulary([SimpleTerm(STATES_IDS[i], title=t)
       
    59                                       for i, t in enumerate(STATES_LABELS)])
       
    60 
       
    61 STATES_HEADERS = {DRAFT: _("draft created"),
       
    62                   PUBLISHED: _("published"),
       
    63                   ARCHIVED: _("archived")}
       
    64 
       
    65 UPDATE_STATES = (DRAFT, )
       
    66 '''Default state available to contributors in update mode'''
       
    67 
       
    68 READONLY_STATES = (ARCHIVED, DELETED)
       
    69 '''Retired and archived contents can't be modified'''
       
    70 
       
    71 PROTECTED_STATES = (PUBLISHED, )
       
    72 '''Protected states are available to webmasters in update mode'''
       
    73 
       
    74 MANAGER_STATES = ()
       
    75 '''No custom state available to managers!'''
       
    76 
       
    77 VISIBLE_STATES = PUBLISHED_STATES = (PUBLISHED, )
       
    78 
       
    79 WAITING_STATES = ()
       
    80 
       
    81 RETIRED_STATES = ()
       
    82 
       
    83 ARCHIVED_STATES = (ARCHIVED, )
       
    84 
       
    85 
       
    86 #
       
    87 # Workflow conditions
       
    88 #
       
    89 
       
    90 def can_manage_content(wf, context):
       
    91     """Check if a manager can handle content"""
       
    92     request = check_request()
       
    93     # grant access to webmaster
       
    94     if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
       
    95         return True
       
    96     # local content managers can manage content
       
    97     principal_id = request.principal.id
       
    98     if principal_id in context.managers:
       
    99         return True
       
   100     # shared tool managers can manage content if restrictions apply
       
   101     restrictions = IManagerRestrictions(context).get_restrictions(principal_id)
       
   102     return restrictions and restrictions.check_access(context, permission=PUBLISH_CONTENT_PERMISSION, request=request)
       
   103 
       
   104 
       
   105 def can_create_new_version(wf, context):
       
   106     """Check if we can create a new version"""
       
   107     # can't create new version when previous draft already exists
       
   108     versions = IWorkflowVersions(context)
       
   109     if versions.has_version(DRAFT):
       
   110         return False
       
   111     request = check_request()
       
   112     # grant access to webmaster
       
   113     if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
       
   114         return True
       
   115     # grant access to owner, creator and local contributors
       
   116     principal_id = request.principal.id
       
   117     if principal_id in context.owner | {context.creator} | context.contributors:
       
   118         return True
       
   119     # grant access to local content managers
       
   120     if principal_id in context.managers:
       
   121         return True
       
   122     # grant access to shared tool managers if restrictions apply
       
   123     restrictions = IManagerRestrictions(context).get_restrictions(principal_id)
       
   124     return restrictions and restrictions.check_access(context, permission=CREATE_CONTENT_PERMISSION, request=request)
       
   125 
       
   126 
       
   127 def can_delete_version(wf, context):
       
   128     """Check if we can delete a draft version"""
       
   129     request = check_request()
       
   130     # grant access to webmaster
       
   131     if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
       
   132         return True
       
   133     # grant access to owner, creator and local contributors
       
   134     principal_id = request.principal.id
       
   135     if principal_id in context.owner | {context.creator} | context.contributors:
       
   136         return True
       
   137     # grant access to local content managers
       
   138     if principal_id in context.managers:
       
   139         return True
       
   140     # grant access to shared tool managers if restrictions apply
       
   141     restrictions = IManagerRestrictions(context).get_restrictions(principal_id)
       
   142     return restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION, request=request)
       
   143 
       
   144 
       
   145 #
       
   146 # Workflow actions
       
   147 #
       
   148 
       
   149 def publish_action(wf, context):
       
   150     """Publish version"""
       
   151     request = check_request()
       
   152     translate = request.localizer.translate
       
   153     publication_info = IWorkflowPublicationInfo(context)
       
   154     publication_info.publication_date = datetime.utcnow()
       
   155     publication_info.publisher = request.principal.id
       
   156     version_id = IWorkflowState(context).version_id
       
   157     for version in IWorkflowVersions(context).get_versions((PUBLISHED, )):
       
   158         if version is not context:
       
   159             IWorkflowInfo(version).fire_transition_toward(ARCHIVED,
       
   160                                                           comment=translate(_("Published version {0}")).format(version_id))
       
   161 
       
   162 
       
   163 def archive_action(wf, context):
       
   164     """Remove readers when a content is archived"""
       
   165     roles = IWfSharedContentRoles(context, None)
       
   166     if roles is not None:
       
   167         IRoleProtectedObject(context).revoke_role(READER_ROLE, roles.readers)
       
   168 
       
   169 
       
   170 def clone_action(wf, context):
       
   171     """Create new version"""
       
   172     result = copy(context)
       
   173     locate(result, context.__parent__)
       
   174     registry = get_current_registry()
       
   175     registry.notify(ObjectClonedEvent(result, context))
       
   176     return result
       
   177 
       
   178 
       
   179 def delete_action(wf, context):
       
   180     """Delete draft version, and parent if single version"""
       
   181     versions = IWorkflowVersions(context)
       
   182     versions.remove_version(IWorkflowState(context).version_id)
       
   183 
       
   184 
       
   185 #
       
   186 # Workflow transitions
       
   187 #
       
   188 
       
   189 init = Transition(transition_id='init',
       
   190                   title=_("Initialize"),
       
   191                   source=None,
       
   192                   destination=DRAFT,
       
   193                   history_label=_("Draft creation"))
       
   194 
       
   195 draft_to_published = Transition(transition_id='draft_to_published',
       
   196                                 title=_("Publish"),
       
   197                                 source=DRAFT,
       
   198                                 destination=PUBLISHED,
       
   199                                 permission=PUBLISH_CONTENT_PERMISSION,
       
   200                                 condition=can_manage_content,
       
   201                                 action=publish_action,
       
   202                                 menu_css_class='fa fa-fw fa-thumbs-o-up',
       
   203                                 view_name='wf-publish.html',
       
   204                                 history_label=_("Content published"),
       
   205                                 notify_roles={WEBMASTER_ROLE, PILOT_ROLE, MANAGER_ROLE, OWNER_ROLE},
       
   206                                 notify_message=_("published the content « {0} »"),
       
   207                                 order=1)
       
   208 
       
   209 published_to_archived = Transition(transition_id='published_to_archived',
       
   210                                    title=_("Archive content"),
       
   211                                    source=PUBLISHED,
       
   212                                    destination=ARCHIVED,
       
   213                                    permission=PUBLISH_CONTENT_PERMISSION,
       
   214                                    condition=can_manage_content,
       
   215                                    action=archive_action,
       
   216                                    menu_css_class='fa fa-fw fa-archive',
       
   217                                    view_name='wf-archive.html',
       
   218                                    history_label=_("Content archived"),
       
   219                                    notify_roles={WEBMASTER_ROLE, PILOT_ROLE, MANAGER_ROLE, OWNER_ROLE},
       
   220                                    notify_message=_("archived content « {0} »"),
       
   221                                    order=2)
       
   222 
       
   223 published_to_draft = Transition(transition_id='published_to_draft',
       
   224                                 title=_("Create new version"),
       
   225                                 source=PUBLISHED,
       
   226                                 destination=DRAFT,
       
   227                                 permission=CREATE_CONTENT_PERMISSION,
       
   228                                 condition=can_create_new_version,
       
   229                                 action=clone_action,
       
   230                                 menu_css_class='fa fa-fw fa-file-o',
       
   231                                 view_name='wf-clone.html',
       
   232                                 history_label=_("New version created"),
       
   233                                 order=3)
       
   234 
       
   235 archived_to_draft = Transition(transition_id='archived_to_draft',
       
   236                                title=_("Create new version"),
       
   237                                source=ARCHIVED,
       
   238                                destination=DRAFT,
       
   239                                permission=CREATE_CONTENT_PERMISSION,
       
   240                                condition=can_create_new_version,
       
   241                                action=clone_action,
       
   242                                menu_css_class='fa fa-fw fa-file-o',
       
   243                                view_name='wf-clone.html',
       
   244                                history_label=_("New version created"),
       
   245                                order=4)
       
   246 
       
   247 delete = Transition(transition_id='delete',
       
   248                     title=_("Delete version"),
       
   249                     source=DRAFT,
       
   250                     destination=DELETED,
       
   251                     permission=MANAGE_CONTENT_PERMISSION,
       
   252                     condition=can_delete_version,
       
   253                     action=delete_action,
       
   254                     menu_css_class='fa fa-fw fa-trash',
       
   255                     view_name='wf-delete.html',
       
   256                     history_label=_("Version deleted"),
       
   257                     order=99)
       
   258 
       
   259 wf_transitions = {init,
       
   260                   draft_to_published,
       
   261                   published_to_archived,
       
   262                   published_to_draft,
       
   263                   archived_to_draft,
       
   264                   delete}
       
   265 
       
   266 
       
   267 @implementer(IBasicWorkflow)
       
   268 class BasicWorkflow(Workflow):
       
   269     """PyAMS basic workflow"""
       
   270 
       
   271 
       
   272 @adapter_config(context=IBasicWorkflow, provides=IWorkflowStateLabel)
       
   273 class WorkflowStateLabelAdapter(ContextAdapter):
       
   274     """Generic state label adapter"""
       
   275 
       
   276     @staticmethod
       
   277     def get_label(content, request=None, format=True):
       
   278         if request is None:
       
   279             request = check_request()
       
   280         translate = request.localizer.translate
       
   281         state = IWorkflowState(content)
       
   282         header = STATES_HEADERS.get(state.state)
       
   283         if header is not None:
       
   284             state_label = translate(header)
       
   285             if format:
       
   286                 state_label = translate(_('{state} {date}')).format(state=state_label,
       
   287                                                                     date=format_datetime(state.state_date))
       
   288         else:
       
   289             state_label = translate(_("Unknown state"))
       
   290         return state_label
       
   291 
       
   292 
       
   293 @adapter_config(name=DRAFT, context=IBasicWorkflow, provides=IWorkflowStateLabel)
       
   294 class DraftWorkflowStateLabelAdapter(ContextAdapter):
       
   295     """Draft state label adapter"""
       
   296 
       
   297     @staticmethod
       
   298     def get_label(content, request=None, format=True):
       
   299         if request is None:
       
   300             request = check_request()
       
   301         translate = request.localizer.translate
       
   302         state = IWorkflowState(content)
       
   303         if len(state.history) <= 2:
       
   304             header = STATES_HEADERS.get(state.state)
       
   305             if header is not None:
       
   306                 if state.version_id == 1:
       
   307                     state_label = translate(header)
       
   308                 else:
       
   309                     state_label = translate(_("new version created"))
       
   310             else:
       
   311                 state_label = translate(_("Unknown state"))
       
   312         else:
       
   313             state_label = translate(_('publication refused'))
       
   314         if format:
       
   315             state_label = translate(_('{state} {date}')).format(state=state_label,
       
   316                                                                 date=format_datetime(state.state_date))
       
   317         return state_label
       
   318 
       
   319 
       
   320 wf = BasicWorkflow(wf_transitions,
       
   321                    states=STATES_VOCABULARY,
       
   322                    initial_state=DRAFT,
       
   323                    update_states=UPDATE_STATES,
       
   324                    readonly_states=READONLY_STATES,
       
   325                    protected_states=PROTECTED_STATES,
       
   326                    manager_states=MANAGER_STATES,
       
   327                    published_states=VISIBLE_STATES,
       
   328                    waiting_states=WAITING_STATES,
       
   329                    retired_states=RETIRED_STATES,
       
   330                    archived_states=ARCHIVED_STATES,
       
   331                    auto_retired_state=ARCHIVED)
       
   332 
       
   333 
       
   334 @utility_config(name='PyAMS basic workflow', provides=IWorkflow)
       
   335 class WorkflowUtility(object):
       
   336     """PyAMS basic workflow utility
       
   337 
       
   338     This is a basic workflow implementation for PyAMS contents.
       
   339     It only implements three states which are *draft*, *published* and *archived*.
       
   340     """
       
   341 
       
   342     def __new__(cls):
       
   343         return wf