Beispiel #1
0
def add_retract_transitions(wf):
    """Set up any retract transitions (from terminal states) for all workflows.
    """
    def get_retract_is_allowed(from_state_id):
        if from_state_id in reusable_retract_conditions:
            return reusable_retract_conditions[from_state_id]

        def retract_is_allowed(context):
            """A workflow condition, for system use only, for auto "retract" transition.
            """
            if IFeatureAudit.providedBy(context):
                changes = domain.get_changes(context, "workflow")
                if changes:
                    prev_change = changes[0].seq_previous
                    if prev_change:
                        return from_state_id == prev_change.audit.status
            return True

        reusable_retract_conditions[from_state_id] = retract_is_allowed
        return retract_is_allowed

    def get_roles(transition):
        original_roles = transition.user_data.get("_roles", [])
        allowed_roles = set()
        # transitions to source
        tx_to_source = wf.get_transitions_to(transition.source)
        for tx in tx_to_source:
            if tx.source is None:
                allowed_roles.update(original_roles)
            else:
                for role in tx.user_data.get("_roles", []):
                    if role in original_roles:
                        allowed_roles.add(role)
        if not len(allowed_roles):
            allowed_roles = original_roles
        return allowed_roles

    transitions = wf._transitions_by_id.values()
    terminal_transitions = [
        transition for transition in transitions
        if (wf.get_transitions_from(transition.destination) == []
            and transition.source
            and transition.trigger in [interfaces.MANUAL, interfaces.SYSTEM])
    ]
    from bungeni import _, translate
    for transition in terminal_transitions:
        roles = get_roles(transition)
        if roles:
            tid = get_tid(transition.destination, transition.source)
            if tid in wf._transitions_by_id:
                # we already have reverse transition, skip...
                continue
            title = _("revert_transition_title",
                      default="Undo - ${title}",
                      mapping={"title": translate(transition.title)})
            title = "Undo - %s" % (transition.title)
            pid = "bungeni.%s.wf.%s" % (wf.name, tid)
            ntransition = Transition(title,
                                     transition.destination,
                                     transition.source,
                                     trigger=interfaces.MANUAL,
                                     condition=get_retract_is_allowed(
                                         transition.destination),
                                     permission=pid,
                                     roles=roles)
            transitions.append(ntransition)
            zcml_transition_permission(pid, title, roles)
            log.debug("Workflow [%s] adding retract transition: %r", wf.name,
                      ntransition.id)
    wf.refresh(wf._states_by_id.values(), transitions)
def add_retract_transitions(wf):
    """Set up any retract transitions (from terminal states) for all workflows.
    """
    def get_retract_is_allowed(from_state_id):
        if from_state_id in reusable_retract_conditions:
            return reusable_retract_conditions[from_state_id]
        def retract_is_allowed(context):
            """A workflow condition, for system use only, for auto "retract" transition.
            """
            if IFeatureAudit.providedBy(context):
                changes = domain.get_changes(context, "workflow")
                if changes:
                    prev_change = changes[0].seq_previous
                    if prev_change:
                        return from_state_id == prev_change.audit.status
            return True
        reusable_retract_conditions[from_state_id] = retract_is_allowed
        return retract_is_allowed

    def get_roles(transition):
        original_roles = transition.user_data.get("_roles", [])
        allowed_roles = set()
        # transitions to source
        tx_to_source = wf.get_transitions_to(transition.source)
        for tx in tx_to_source:
            if tx.source is None:
                allowed_roles.update(original_roles)
            else:
                for role in tx.user_data.get("_roles", []):
                    if role in original_roles:
                        allowed_roles.add(role)
        if not len(allowed_roles):
            allowed_roles = original_roles
        return allowed_roles
    
    transitions = wf._transitions_by_id.values()
    terminal_transitions = [ 
        transition for transition in transitions
        if (wf.get_transitions_from(transition.destination) == [] and
            transition.source and 
            transition.trigger in [interfaces.MANUAL, interfaces.SYSTEM])
    ]
    from bungeni import _, translate
    for transition in terminal_transitions:
        roles = get_roles(transition)
        if roles:
            tid = get_tid(transition.destination, transition.source)
            if tid in wf._transitions_by_id:
                # we already have reverse transition, skip...
                continue
            title = _("revert_transition_title",
                default="Undo - ${title}",
                mapping={
                    "title": translate(transition.title)
                }
            )
            title = "Undo - %s" % (transition.title)
            pid = "bungeni.%s.wf.%s" % (wf.name, tid)
            ntransition = Transition(title, transition.destination,
                transition.source, 
                trigger=interfaces.MANUAL,
                condition=get_retract_is_allowed(transition.destination),
                permission=pid,
                roles=roles)
            transitions.append(ntransition)
            zcml_transition_permission(pid, title, roles)
            log.debug("Workflow [%s] adding retract transition: %r", 
                    wf.name, ntransition.id)
    wf.refresh(wf._states_by_id.values(), transitions)
Beispiel #3
0
def _load(workflow_name, workflow):
    """ (workflow_name:str, workflow:etree_doc) -> Workflow
    """
    workflow_title = xas(workflow, "title")
    naming.MSGIDS.add(workflow_title)
    workflow_description = xas(workflow, "description")
    naming.MSGIDS.add(workflow_description)
    transitions = []
    states = []
    note = xas(workflow, "note")

    # initial_state, in XML indicated with a transition.@source=""
    initial_state = None

    ZCML_PROCESSED = bool(workflow_name in ZCML_WORKFLOWS_PROCESSED)
    if not ZCML_PROCESSED:
        ZCML_WORKFLOWS_PROCESSED.add(workflow_name)
        ZCML_LINES.append(ZCML_INDENT)
        ZCML_LINES.append(ZCML_INDENT)
        ZCML_LINES.append("%s<!-- %s -->" % (ZCML_INDENT, workflow_name))

    def validate_id(id, tag):
        """Ensure that ID values are unique within the same XML doc scope.
        """
        m = "Invalid element <%s> id=%r in workflow %r" % (tag, id,
                                                           workflow_name)
        assert id not in validate_id.wuids, "%s -- id not unique in workflow document" % (
            m)
        validate_id.wuids.add(id)

    validate_id.wuids = set()  # unique IDs in this XML workflow file

    def get_from_state(state_id):
        if state_id is None:
            return
        for state in states:
            if state.id == state_id:
                return state
        raise ValueError("Invalid value: permissions_from_state=%r" %
                         (state_id))

    def check_not_global_grant(pid, role):
        # ensure global and local assignments are distinct
        # (global: global_pid_roles, workflow_name)
        global_proles = global_pid_roles.get(pid, "")
        assert role not in global_proles, (
            "Workflow [%s] may not mix "
            "global and local granting of a same permission [%s] to a "
            "same role [%s].") % (workflow_name, pid, role)

    # permission_actions -> permissions for this type
    for (key, permission_action) in capi.schema.qualified_permission_actions(
            workflow_name,
            xas(workflow, "permission_actions", "").split()):
        pid = "bungeni.%s.%s" % (key, permission_action)
        title = "%s %s" % (permission_action,
                           naming.split_camel(naming.model_name(key)))
        # !+ add to a Workflow.defines_permissions list
        ZCML_LINES.append('%s<permission id="%s" title="%s" />' %
                          (ZCML_INDENT, pid, title))
        provideUtility(Permission(pid), IPermission, pid)

    # global grants
    global_pid_roles = {}  # {pid: [role]}
    global_grants = get_permissions_from_allows(workflow_name, workflow)
    for (setting, pid, role) in global_grants:
        # for each global permission, build list of roles it is set to
        global_pid_roles.setdefault(pid, []).append(role)
        assert setting and pid and role, \
            "Global grant must specify valid permission/role" #!+RNC
        # !+ add to a Workflow.global_grants list
        ZCML_LINES.append('%s<grant permission="%s" role="%s" />' %
                          (ZCML_INDENT, pid, role))
        # no real need to check that the permission and role of a global grant
        # are properly registered in the system -- an error should be raised
        # by the zcml if either is not defined.
        rpm.grantPermissionToRole(pid, role, check=False)

    for perm, roles in global_pid_roles.items():
        # assert roles mix limitations for state permissions
        assert_distinct_permission_scopes(perm, roles, workflow_name,
                                          "global grants")

    # all workflow features (enabled AND disabled)
    workflow_features = load_features(workflow_name, workflow)
    enabled_feature_names = [None] + [
        f.name for f in workflow_features if f.enabled
    ]
    # !+EVENT_FEATURE_TYPES add each as enabled_feature_names, or extend the
    # facet quailifer by feature_name(sub_type_key).facet_ref?
    # workflow facets
    workflow_facets = load_facets(workflow_name, workflow)

    # states
    for s in workflow.iterchildren("state"):
        # @id
        state_id = xas(s, "id")
        assert state_id, "Workflow State must define @id"  #!+RNC
        validate_id(state_id, "state")

        # @actions - transition-to-state actions
        state_actions = []
        for action_name in xas(s, "actions", "").split():
            state_actions.append(capi.get_workflow_action(action_name))

        # @permissions_from_state
        permissions = []  # [ tuple(bool:int, permission:str, role:str) ]
        # state.@permissions_from_state : to reduce repetition and enhance
        # maintainibility of workflow XML files, a state may inherit ALL
        # permissions defined by the specified state. NO other permissions
        # may be specified by this state.
        from_state = get_from_state(xas(s, "permissions_from_state"))
        parent_permissions = xab(s, "parent_permissions")
        if parent_permissions:
            pass  # no own permission definitions allowed
        elif from_state:
            # assimilate (no more no less) the state's permissions !+tuple, use same?
            permissions[:] = from_state.permissions
        else:
            used_facets_fq = resolve_state_facets(workflow_name,
                                                  workflow_facets,
                                                  enabled_feature_names, s)

            # assimilate permissions from facets from None and all enabled features
            def add_facet_permissions(facet):
                for perm in facet.permissions:
                    check_add_assign_permission(workflow_name, permissions,
                                                perm)
                    check_not_global_grant(perm[1], perm[2])

            for (feature_name, qtk) in used_facets_fq:
                # debug check that feature is enabled
                assert feature_name in enabled_feature_names
                facet = used_facets_fq[(feature_name, qtk)]
                if facet is not None:
                    add_facet_permissions(facet)

        # states
        state_title = xas(s, "title")
        naming.MSGIDS.add(state_title)
        states.append(
            State(
                state_id,
                state_title,
                xas(s, "note"),
                state_actions,
                permissions,
                parent_permissions,
                xab(s, "obsolete"),
            ))

    STATE_IDS = [s.id for s in states]

    # transitions
    for t in workflow.iterchildren("transition"):
        title = xas(t, "title")
        naming.MSGIDS.add(title)
        # sources, empty string -> initial_state
        sources = t.get("source").split() or [initial_state]
        assert len(sources) == len(set(sources)), \
            "Transition contains duplicate sources [%s]" % (sources)
        for source in sources:
            if source is not initial_state:
                assert source in STATE_IDS, \
                    "Unknown transition source state [%s]" % (source)
        # destination must be a valid state
        destination = t.get("destination")
        assert destination in STATE_IDS, \
            "Unknown transition destination state [%s]" % (destination)

        # optionals -- only set on kw IFF explicitly defined
        kw = {}
        for i in TRANS_ATTRS_OPTIONALS:
            val = xas(t, i)
            if not val:
                # we let setting of defaults be handled downstream
                continue
            kw[i] = val

        # data up-typing
        #
        # trigger
        if "trigger" in kw:
            kw["trigger"] = trigger_value_map[kw["trigger"]]
        # roles -> permission - one-to-one per transition
        roles = capi.schema.qualified_roles(kw.pop("roles", "").split())
        if not is_zcml_permissionable(t):
            assert not roles, "Workflow [%s] - non-permissionable transition " \
                "does not allow @roles [%s]." % (workflow_name, roles) #!+RNC
            kw["permission"] = None  # None -> CheckerPublic
        # !+CAN_EDIT_AS_DEFAULT_TRANSITION_PERMISSION(mr, oct-2011) this feature
        # is functional (uncomment following elif clause) but as yet not enabled.
        #
        # Advantage would be that it would be easier to keep transitions
        # permissions in sync with object permissions (set in state) as the
        # majority of transition require exactly this as privilege; for the
        # occassional transition needing a different privilege, the current
        # transition.@roles mechanism may be used to make this explicit.
        #
        # Need to consider implications further; the zope_principal_role_map db
        # table, that caches contextual roles for principals, should probably
        # first be reworked to be db-less (as for zope_role_permission_map).
        #
        #elif not roles:
        #    # then as fallback transition permission use can modify object
        #    kw["permission"] = "bungeni.%s.Edit" % (workflow_name) # fallback permission
        else:
            # Dedicated permission for XML multi-source transition.
            # Given that, irrespective of how sources are grouped into
            # multi-source XML <transition> elements, there may be only *one*
            # path from any given *source* to any given *destination* state,
            # it suffices to use only the first source element + the destination
            # to guarantee a unique identifier for an XML transition element.
            tid = get_tid(sources[0] or "", destination)
            pid = "bungeni.%s.wf.%s" % (workflow_name, tid)
            if not ZCML_PROCESSED:
                zcml_transition_permission(pid, title, roles)
                # remember list of roles from xml
                kw["_roles"] = roles
            kw["permission"] = pid
        # python resolvables
        if "condition" in kw:
            kw["condition"] = capi.get_workflow_condition(kw["condition"])
        # numeric
        if "order" in kw:
            kw["order"] = float(kw["order"])  # ValueError if not numeric
        # bool
        if "require_confirmation" in kw:
            try:
                kw["require_confirmation"] = misc.as_bool(
                    kw["require_confirmation"])
                assert kw["require_confirmation"] is not None  #!+RNC
            except:
                raise ValueError("Invalid transition value "
                                 '[require_confirmation="%s"]' %
                                 (t.get("require_confirmation")))
        # multiple-source transitions are really multiple "transition paths"
        for source in sources:
            args = (title, source, destination)
            transitions.append(Transition(*args, **kw))
            log.debug("[%s] adding transition [%s-%s] [%s]", workflow_name,
                      source or "", destination, kw)

    wf = Workflow(workflow_name, workflow_features, workflow_facets, states,
                  transitions, global_grants, workflow_title,
                  workflow_description, note)
    add_retract_transitions(wf)
    return wf
def _load(workflow_name, workflow):
    """ (workflow_name:str, workflow:etree_doc) -> Workflow
    """
    workflow_title = xas(workflow, "title")
    naming.MSGIDS.add(workflow_title)
    workflow_description = xas(workflow, "description")
    naming.MSGIDS.add(workflow_description)
    transitions = []
    states = []
    note = xas(workflow, "note")
    
    # initial_state, in XML indicated with a transition.@source=""
    initial_state = None
    
    ZCML_PROCESSED = bool(workflow_name in ZCML_WORKFLOWS_PROCESSED)
    if not ZCML_PROCESSED:
        ZCML_WORKFLOWS_PROCESSED.add(workflow_name)
        ZCML_LINES.append(ZCML_INDENT)
        ZCML_LINES.append(ZCML_INDENT)
        ZCML_LINES.append("%s<!-- %s -->" % (ZCML_INDENT, workflow_name))
    
    def validate_id(id, tag):
        """Ensure that ID values are unique within the same XML doc scope.
        """
        m = "Invalid element <%s> id=%r in workflow %r" % (tag, id, workflow_name)
        assert id not in validate_id.wuids, "%s -- id not unique in workflow document" % (m)
        validate_id.wuids.add(id)
    validate_id.wuids = set() # unique IDs in this XML workflow file
    
    def get_from_state(state_id):
        if state_id is None:
            return
        for state in states:
            if state.id == state_id:
                return state
        raise ValueError("Invalid value: permissions_from_state=%r" % (state_id))
    
    def check_not_global_grant(pid, role):
        # ensure global and local assignments are distinct
        # (global: global_pid_roles, workflow_name)
        global_proles = global_pid_roles.get(pid, "")
        assert role not in global_proles, ("Workflow [%s] may not mix "
            "global and local granting of a same permission [%s] to a "
            "same role [%s].") % (workflow_name, pid, role)
    
    # permission_actions -> permissions for this type
    for (key, permission_action) in capi.schema.qualified_permission_actions(
            workflow_name, xas(workflow, "permission_actions", "").split()
        ):
        pid = "bungeni.%s.%s" % (key, permission_action)
        title = "%s %s" % (
            permission_action, naming.split_camel(naming.model_name(key)))
        # !+ add to a Workflow.defines_permissions list
        ZCML_LINES.append(
            '%s<permission id="%s" title="%s" />' % (ZCML_INDENT, pid, title))
        provideUtility(Permission(pid), IPermission, pid)
    
    # global grants
    global_pid_roles = {} # {pid: [role]}
    global_grants = get_permissions_from_allows(workflow_name, workflow)
    for (setting, pid, role) in global_grants:
        # for each global permission, build list of roles it is set to
        global_pid_roles.setdefault(pid, []).append(role)
        assert setting and pid and role, \
            "Global grant must specify valid permission/role" #!+RNC
        # !+ add to a Workflow.global_grants list
        ZCML_LINES.append(
            '%s<grant permission="%s" role="%s" />' % (ZCML_INDENT, pid, role))
        # no real need to check that the permission and role of a global grant 
        # are properly registered in the system -- an error should be raised 
        # by the zcml if either is not defined.
        rpm.grantPermissionToRole(pid, role, check=False)

    for perm, roles in global_pid_roles.items():
        # assert roles mix limitations for state permissions
        assert_distinct_permission_scopes(perm, roles, workflow_name, "global grants")
    
    # all workflow features (enabled AND disabled)
    workflow_features = load_features(workflow_name, workflow)
    enabled_feature_names = [None] + [ 
        f.name for f in workflow_features if f.enabled ]
    # !+EVENT_FEATURE_TYPES add each as enabled_feature_names, or extend the 
    # facet quailifer by feature_name(sub_type_key).facet_ref?
    # workflow facets
    workflow_facets = load_facets(workflow_name, workflow)
    
    # states
    for s in workflow.iterchildren("state"):
        # @id
        state_id = xas(s, "id")
        assert state_id, "Workflow State must define @id" #!+RNC
        validate_id(state_id, "state")
        
        # @actions - transition-to-state actions
        state_actions = []
        for action_name in xas(s, "actions", "").split():
            state_actions.append(capi.get_workflow_action(action_name))
        
        # @permissions_from_state
        permissions = [] # [ tuple(bool:int, permission:str, role:str) ]
        # state.@permissions_from_state : to reduce repetition and enhance 
        # maintainibility of workflow XML files, a state may inherit ALL 
        # permissions defined by the specified state. NO other permissions 
        # may be specified by this state. 
        from_state = get_from_state(xas(s, "permissions_from_state"))
        parent_permissions = xab(s, "parent_permissions")
        if parent_permissions:
            pass # no own permission definitions allowed
        elif from_state:
            # assimilate (no more no less) the state's permissions !+tuple, use same?
            permissions[:] = from_state.permissions
        else:
            used_facets_fq = resolve_state_facets(
                workflow_name, workflow_facets, enabled_feature_names, s)
            # assimilate permissions from facets from None and all enabled features
            def add_facet_permissions(facet):
                for perm in facet.permissions:
                    check_add_assign_permission(workflow_name, permissions, perm)
                    check_not_global_grant(perm[1], perm[2])
            for (feature_name, qtk) in used_facets_fq:
                # debug check that feature is enabled
                assert feature_name in enabled_feature_names
                facet = used_facets_fq[(feature_name, qtk)]
                if facet is not None:
                    add_facet_permissions(facet)
        
        # states
        state_title = xas(s, "title")
        naming.MSGIDS.add(state_title)
        states.append(
            State(state_id, state_title,
                xas(s, "note"),
                state_actions, permissions,
                parent_permissions,
                xab(s, "obsolete"),
            )
        )
    
    STATE_IDS = [ s.id for s in states ]
    
    # transitions
    for t in workflow.iterchildren("transition"):
        title = xas(t, "title")
        naming.MSGIDS.add(title)
        # sources, empty string -> initial_state
        sources = t.get("source").split() or [initial_state]
        assert len(sources) == len(set(sources)), \
            "Transition contains duplicate sources [%s]" % (sources)
        for source in sources:
            if source is not initial_state:
                assert source in STATE_IDS, \
                    "Unknown transition source state [%s]" % (source)
        # destination must be a valid state
        destination = t.get("destination")
        assert destination in STATE_IDS, \
            "Unknown transition destination state [%s]" % (destination)
        
        # optionals -- only set on kw IFF explicitly defined
        kw = {}
        for i in TRANS_ATTRS_OPTIONALS:
            val = xas(t, i)
            if not val:
                # we let setting of defaults be handled downstream
                continue
            kw[i] = val
        
        # data up-typing
        #
        # trigger
        if "trigger" in kw:
            kw["trigger"] = trigger_value_map[kw["trigger"]]
        # roles -> permission - one-to-one per transition
        roles = capi.schema.qualified_roles(kw.pop("roles", "").split())
        if not is_zcml_permissionable(t):
            assert not roles, "Workflow [%s] - non-permissionable transition " \
                "does not allow @roles [%s]." % (workflow_name, roles) #!+RNC
            kw["permission"] = None # None -> CheckerPublic
        # !+CAN_EDIT_AS_DEFAULT_TRANSITION_PERMISSION(mr, oct-2011) this feature
        # is functional (uncomment following elif clause) but as yet not enabled. 
        #
        # Advantage would be that it would be easier to keep transitions 
        # permissions in sync with object permissions (set in state) as the 
        # majority of transition require exactly this as privilege; for the 
        # occassional transition needing a different privilege, the current 
        # transition.@roles mechanism may be used to make this explicit. 
        #
        # Need to consider implications further; the zope_principal_role_map db 
        # table, that caches contextual roles for principals, should probably 
        # first be reworked to be db-less (as for zope_role_permission_map).
        #
        #elif not roles:
        #    # then as fallback transition permission use can modify object
        #    kw["permission"] = "bungeni.%s.Edit" % (workflow_name) # fallback permission
        else:
            # Dedicated permission for XML multi-source transition.
            # Given that, irrespective of how sources are grouped into 
            # multi-source XML <transition> elements, there may be only *one* 
            # path from any given *source* to any given *destination* state, 
            # it suffices to use only the first source element + the destination 
            # to guarantee a unique identifier for an XML transition element.
            tid = get_tid(sources[0] or "", destination)
            pid = "bungeni.%s.wf.%s" % (workflow_name, tid)
            if not ZCML_PROCESSED:
                zcml_transition_permission(pid, title, roles)
                # remember list of roles from xml
                kw["_roles"] = roles
            kw["permission"] = pid
        # python resolvables
        if "condition" in kw:
            kw["condition"] = capi.get_workflow_condition(kw["condition"])
        # numeric
        if "order" in kw:
            kw["order"] = float(kw["order"]) # ValueError if not numeric
        # bool
        if "require_confirmation" in kw:
            try:
                kw["require_confirmation"] = misc.as_bool(kw["require_confirmation"])
                assert kw["require_confirmation"] is not None #!+RNC
            except:
                raise ValueError("Invalid transition value "
                    '[require_confirmation="%s"]' % (
                        t.get("require_confirmation")))
        # multiple-source transitions are really multiple "transition paths"
        for source in sources:
            args = (title, source, destination)
            transitions.append(Transition(*args, **kw))
            log.debug("[%s] adding transition [%s-%s] [%s]",
                workflow_name, source or "", destination, kw)
    
    wf = Workflow(workflow_name,
        workflow_features, workflow_facets, states, transitions, global_grants,
        workflow_title, workflow_description, note)
    add_retract_transitions(wf)
    return wf