Example #1
0
def get_loaded_workflow(workflow_name):
    """Retrieve the previously loaded named workflow.
    """
    try:
        return Workflow.get_singleton(workflow_name)
    except KeyError:
        return None # no such workflow (or workflow not yet loaded)
Example #2
0
def load_workflow(type_key, ti):
    """Load (from XML definition), set the workflow instance, and setup (once).
    """
    if ti.workflow_key is None:
        return
    assert ti.workflow is None
    workflow_file_key = ti.workflow_key  # workflow file name

    # workflow_name -> what becomes workflow.name (and ti.permissions_type_key)
    workflow_name = workflow_file_key
    if ti.custom and type_key != workflow_file_key:
        workflow_name = type_key
    # !+ only (non-custom type) "address" wf is multi-used by user_address/group_address

    try:
        # retrieve
        ti.workflow = Workflow.get_singleton(workflow_name)
        log.warn("Already Loaded WORKFLOW: %s.xml as %r - %s" %
                 (workflow_file_key, workflow_name, ti.workflow))
    except KeyError:
        # load
        ti.workflow = xmlimport.load(workflow_file_key, workflow_name)
        log.info("Loaded WORKFLOW: %s.xml as %r - %s" %
                 (workflow_file_key, workflow_name, ti.workflow))
        # debug info
        for state_key, state in ti.workflow.states.items():
            log.debug("   STATE: %s %s" % (state_key, state))
            for p in state.permissions:
                log.debug("          %s" % (p, ))

    # setup
    if ti.workflow:
        apply_customization_workflow(type_key, ti)
        register_specific_workflow_adapter(ti)
Example #3
0
def load_workflow(type_key, ti):
    """Load (from XML definition), set the workflow instance, and setup (once).
    """
    if ti.workflow_key is None:
        return
    assert ti.workflow is None
    workflow_file_key = ti.workflow_key # workflow file name
    
    # workflow_name -> what becomes workflow.name (and ti.permissions_type_key)
    workflow_name = workflow_file_key 
    if ti.custom and type_key != workflow_file_key:
        workflow_name = type_key         
    # !+ only (non-custom type) "address" wf is multi-used by user_address/group_address
    
    try:
        # retrieve
        ti.workflow = Workflow.get_singleton(workflow_name)
        log.warn("Already Loaded WORKFLOW: %s.xml as %r - %s" % (
            workflow_file_key, workflow_name, ti.workflow))
    except KeyError:
        # load
        ti.workflow = xmlimport.load(workflow_file_key, workflow_name)
        log.info("Loaded WORKFLOW: %s.xml as %r - %s" % (
            workflow_file_key, workflow_name, ti.workflow))
        # debug info
        for state_key, state in ti.workflow.states.items():
            log.debug("   STATE: %s %s" % (state_key, state))
            for p in state.permissions:
                log.debug("          %s" % (p,))
    
    # setup
    if ti.workflow:
        apply_customization_workflow(type_key, ti)
        register_specific_workflow_adapter(ti)
Example #4
0
def _load(workflow, name):
    """ (workflow:etree_doc, name:str) -> Workflow
    """
    transitions = []
    states = []
    domain = strip_none(workflow.get("domain")) 
    # !+domain(mr, jul-2011) drop? only used as state/transition title default
    wuids = set() # unique IDs in this XML workflow file
    auditable = as_bool(strip_none(workflow.get("auditable")) or "false")
    versionable = as_bool(strip_none(workflow.get("versionable")) or "false")
    if versionable:
        assert auditable, "Workflow [%s] is versionable but not auditable" % (
            name)
    note = strip_none(workflow.get("note"))
        
    # initial_state, must be ""
    assert workflow.get("initial_state") == "", "Workflow [%s] initial_state " \
        "attribute must be empty string, not [%s]" % (
            name, workflow.get("initial_state"))
    initial_state = None
    
    ZCML_PROCESSED = bool(name in ZCML_WORKFLOWS_PROCESSED)
    if not ZCML_PROCESSED:
        ZCML_WORKFLOWS_PROCESSED.add(name)
        ZCML_LINES.append(ZCML_INDENT)
        ZCML_LINES.append(ZCML_INDENT)
        ZCML_LINES.append("%s<!-- %s -->" % (ZCML_INDENT, name))
    
    def validate_id(id, tag):
        """Ensure that ID values are unique within the same XML doc scope.
        """
        m = 'Invalid <%s> id="%s" in workflow [%s]' % (tag, id, name)
        assert id is not None, "%s -- id may not be None" % (m)
        assert ID_RE.match(id), '%s -- only letters, numbers, "_" allowed' % (m)
        assert id not in wuids, "%s -- id not unique in workflow document" % (m)
        wuids.add(id)
    
    def get_like_state(state_id):
        if state_id is None:
            return
        for state in states:
            if state.id == state_id:
                return state
        assert False, 'Invalid value: like_state="%s"' % (state_id)
    
    def check_add_permission(permissions, like_permissions, assignment, p, r):
        for perm in [(GRANT, p, r), (DENY, p, r)]:
            assert perm not in permissions, "Workflow [%s] state [%s] " \
                "conflicting state permission: (%s, %s, %s)" % (
                    name, state_id, assignment, p, r)
            if perm in like_permissions:
                like_permissions.remove(perm)
        permissions.append((assignment, p, r))
    
    def assert_valid_attr_names(e, allowed_attr_names):
        for key in e.keys():
            assert key in allowed_attr_names, \
                "Workflow [%s]: unknown attribute %s in %s" % (
                    name, key, etree.tostring(e))
    
    # top-level child ordering
    grouping, allowed_child_ordering = 0, ("grant", "state", "transition")
    for child in workflow.iterchildren():
        if not isinstance(child.tag, basestring):
            # ignore comments
            continue
        while child.tag != allowed_child_ordering[grouping]:
            grouping += 1
            assert grouping < 3, "Workflow [%s] element <%s> %s not allowed " \
                "here -- element order must respect: %s" % (
                    name, child.tag, child.items(), allowed_child_ordering)
    
    # global grants
    for p in workflow.iterchildren("grant"):
        pid = strip_none(p.get("permission"))
        role = strip_none(p.get("role"))
        #+!assertRegisteredPermission(permission)
        ZCML_LINES.append(
            '%s<grant permission="%s" role="%s" />' % (ZCML_INDENT, pid, role))
    
    # states
    for s in workflow.iterchildren("state"):
        assert_valid_attr_names(s, STATE_ATTRS)
        # @id
        state_id = strip_none(s.get("id"))
        assert state_id, "Workflow State must define @id"
        validate_id(state_id, "state")
        # actions
        actions = []
        # version
        if strip_none(s.get("version")) is not None:
            make_version = as_bool(strip_none(s.get("version")))
            if make_version is None:
                raise ValueError("Invalid state value "
                    '[version="%s"]' % s.get("version"))
            if make_version:
                actions.append(ACTIONS_MODULE.create_version)
        
        # state-id-inferred action - if "actions" module defines an action for
        # this state (associated via a naming convention), then use it.
        # !+ tmp, until actions are user-exposed as part of <state>
        action_name = "_%s_%s" % (name, state_id)
        if hasattr(ACTIONS_MODULE, action_name):
            actions.append(getattr(ACTIONS_MODULE, action_name))
        # @like_state, permissions
        permissions = [] # [ tuple(bool:int, permission:str, role:str) ]
        # state.@like_state : to reduce repetition and enhance maintainibility
        # of workflow XML files, a state may specify a @like_state attribute to 
        # inherit all permissions defined by the specified like_state; further
        # permissions specific to this state may be added, but as these may 
        # also override inherited permissions we streamline those out so that 
        # downstream execution (a permission should be granted or denied only 
        # once per transition to a state).
        like_permissions = []
        like_state = get_like_state(strip_none(s.get("like_state")))
        if like_state:
            like_permissions.extend(like_state.permissions)
        # (within same state) a deny is *always* executed after a *grant*
        for i, assign in enumerate(["grant", "deny"]):
            for p in s.iterchildren(assign):
                permission = strip_none(p.get("permission"))
                role = strip_none(p.get("role"))
                #+!assertRegisteredPermission(permission)
                check_add_permission(permissions, like_permissions, 
                    ASSIGNMENTS[i], permission, role)
        if like_state:
            # splice any remaining like_permissions at beginning of permissions
            permissions[0:0] = like_permissions
        # notifications
        notifications = [] # [ notification.Notification ]
        for n in s.iterchildren("notification"):
            notifications.append(
                Notification(
                    strip_none(n.get("condition")), # python resolvable
                    strip_none(n.get("subject")), # template source, i18n
                    strip_none(n.get("from")), # template source
                    strip_none(n.get("to")), # template source
                    strip_none(n.get("body")), # template source, i18n
                    strip_none(n.get("note")), # documentational note
                )
            )
        # states
        states.append(
            State(state_id, Message(s.get("title", domain)), 
                strip_none(s.get("note")),
                actions, permissions, notifications,
                as_bool(strip_none(s.get("permissions_from_parent")) or "false"),
                as_bool(strip_none(s.get("obsolete")) or "false")
            )
        )
    
    
    STATE_IDS = [ s.id for s in states ]
    
    # transitions
    for t in workflow.iterchildren("transition"):
        assert_valid_attr_names(t, TRANS_ATTRS)
        for key in TRANS_ATTRS_REQUIREDS:
            if key == "source" and t.get(key) == "":
                # initial_state, an explicit empty string
                continue
            elif strip_none(t.get(key)) is None:
                raise SyntaxError('No required "%s" attribute in %s' % (
                    key, etree.tostring(t)))
        
        # 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)
        # update ZCML for dedicated permission for (XML multi-source) transition
        tid = "%s.%s" % (
            ".".join([ source or "" for source in sources ]),
            destination)
        pid = "bungeni.%s.wf.%s" % (name, tid)
        if not ZCML_PROCESSED:
            if is_zcml_permissionable(t):
                zcml_transition_permission(pid, t.get("title"), 
                    t.get("roles", "bungeni.Clerk").split())
        
        kw = {}
        # optionals -- only set on kw IFF explicitly defined
        for i in TRANS_ATTRS_OPTIONALS:
            val = t.get(i)
            if not val:
                # we let setting of defaults be handled upstream
                continue
            kw[i] = val
        
        # data up-typing
        #
        # trigger
        if "trigger" in kw:
            kw["trigger"] = trigger_value_map[kw["trigger"]]
        # permission - one-to-one per transition, may only be {pid} or None
        if is_zcml_permissionable(t):
            kw["permission"] = pid
        else:
            assert kw.get("permission") is None, "Not allowed to set a " \
                "permission on (creation) transition: %s" % (tid)
        # 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"] = as_bool(kw["require_confirmation"])
                assert kw["require_confirmation"] is not None
            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 = (Message(t.get("title"), domain), source, destination)
            transitions.append(Transition(*args, **kw))
            log.debug("[%s] adding transition [%s-%s] [%s]" % (
                name, source or "", destination, kw))
    
    return Workflow(name, states, transitions, auditable, versionable, note)
Example #5
0
def _load(workflow, name):
    """ (workflow:etree_doc, name:str) -> Workflow
    """
    # !+ @title, @description
    transitions = []
    states = []
    domain = strip_none(workflow.get("domain"))
    # !+domain(mr, jul-2011) needed?
    wuids = set()  # unique IDs in this XML workflow file
    note = strip_none(workflow.get("note"))
    allowed_tags = (strip_none(workflow.get("tags")) or "").split()

    # initial_state, in XML this must be ""
    assert workflow.get("initial_state") == "", "Workflow [%s] initial_state " \
        "attribute must be empty string, not [%s]" % (
            name, workflow.get("initial_state"))
    initial_state = None

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

    def validate_id(id, tag):
        """Ensure that ID values are unique within the same XML doc scope.
        """
        m = 'Invalid <%s> id="%s" in workflow [%s]' % (tag, id, name)
        assert id is not None, "%s -- id may not be None" % (m)
        assert ID_RE.match(
            id), '%s -- only letters, numbers, "_" allowed' % (m)
        assert id not in wuids, "%s -- id not unique in workflow document" % (
            m)
        wuids.add(id)

    def get_like_state(state_id):
        if state_id is None:
            return
        for state in states:
            if state.id == state_id:
                return state
        raise ValueError('Invalid value: like_state="%s"' % (state_id))

    def check_add_permission(permissions, like_permissions, assignment, p, r):
        for perm in [(GRANT, p, r), (DENY, p, r)]:
            assert perm not in permissions, "Workflow [%s] state [%s] " \
                "duplicated or conflicting state permission: (%s, %s, %s)" % (
                    name, state_id, assignment, p, r)
            if perm in like_permissions:
                like_permissions.remove(perm)
        permissions.append((assignment, p, r))

    def assert_valid_attr_names(elem, allowed_attr_names):
        for key in elem.keys():
            assert key in allowed_attr_names, \
                "Workflow [%s]: unknown attribute %s in %s" % (
                    name, key, etree.tostring(elem))

    # top-level child ordering
    grouping, allowed_child_ordering = 0, ("feature", "grant", "state",
                                           "transition")
    for child in workflow.iterchildren():
        if not isinstance(child.tag, basestring):
            # ignore comments
            continue
        while child.tag != allowed_child_ordering[grouping]:
            grouping += 1
            assert grouping < 4, "Workflow [%s] element <%s> %s not allowed " \
                "here -- element order must respect: %s" % (
                    name, child.tag, child.items(), allowed_child_ordering)

    # features
    features = []
    for f in workflow.iterchildren("feature"):
        assert_valid_attr_names(f, FEATURE_ATTRS)
        # @name
        feature_name = strip_none(f.get("name"))
        assert feature_name, "Workflow Feature must define @name"
        # !+ archetype/feature inter-dep; should be part of feature descriptor
        feature_enabled = as_bool(strip_none(f.get("enabled")) or "true")
        if feature_enabled and feature_name == "version":
            assert "audit" in [ fe.name for fe in features if fe.enabled ], \
                "Workflow [%s] has version but no audit feature" % (name)
        features.append(
            Feature(feature_name,
                    enabled=feature_enabled,
                    note=strip_none(f.get("note"))))

    # global grants
    _permission_role_mixes = {}
    for p in workflow.iterchildren("grant"):
        pid = strip_none(p.get("permission"))
        role = strip_none(p.get("role"))
        # for each global permission, build list of roles it is set to
        _permission_role_mixes.setdefault(pid, []).append(role)
        #+!assertRegisteredPermission(permission)
        assert pid and role, "Global grant must specify valid permission/role"
        ZCML_LINES.append('%s<grant permission="%s" role="%s" />' %
                          (ZCML_INDENT, pid, role))
    for perm, roles in _permission_role_mixes.items():
        # assert roles mix limitations for state permissions
        assert_distinct_permission_scopes(perm, roles, name, "global grants")

    # states
    for s in workflow.iterchildren("state"):
        assert_valid_attr_names(s, STATE_ATTRS)
        # @id
        state_id = strip_none(s.get("id"))
        assert state_id, "Workflow State must define @id"
        validate_id(state_id, "state")
        # actions
        actions = []
        # version (prior to any custom actions)
        if strip_none(s.get("version")) is not None:
            make_version = as_bool(strip_none(s.get("version")))
            if make_version is None:
                raise ValueError("Invalid state value "
                                 '[version="%s"]' % s.get("version"))
            if make_version:
                actions.append(ACTIONS_MODULE.create_version)
        # state-id-inferred action - if "actions" module defines an action for
        # this state (associated via a naming convention), then use it.
        # !+ tmp, until actions are user-exposed as part of <state>
        action_name = "_%s_%s" % (name, state_id)
        if hasattr(ACTIONS_MODULE, action_name):
            actions.append(getattr(ACTIONS_MODULE, action_name))
        # publish (after any custom actions)
        if strip_none(s.get("publish")) is not None:
            do_pub = as_bool(strip_none(s.get("publish")))
            if do_pub is None:
                raise ValueError("Invalid state value "
                                 '[publish="%s"]' % s.get("publish"))
            if do_pub:
                actions.append(ACTIONS_MODULE.publish_to_xml)
        # @tags
        tags = (strip_none(s.get("tags")) or "").split()
        # @like_state, permissions
        permissions = []  # [ tuple(bool:int, permission:str, role:str) ]
        # state.@like_state : to reduce repetition and enhance maintainibility
        # of workflow XML files, a state may specify a @like_state attribute to
        # inherit all permissions defined by the specified like_state; further
        # permissions specific to this state may be added, but as these may
        # also override inherited permissions we streamline those out so that
        # downstream execution (a permission should be granted or denied only
        # once per transition to a state).
        like_permissions = []
        like_state = get_like_state(strip_none(s.get("like_state")))
        if like_state:
            like_permissions.extend(like_state.permissions)
        # (within same state) a deny is *always* executed after a *grant*
        for i, assign in enumerate(["grant", "deny"]):
            for p in s.iterchildren(assign):
                permission = strip_none(p.get("permission"))
                role = strip_none(p.get("role"))
                #+!assertRegisteredPermission(permission)
                check_add_permission(permissions, like_permissions,
                                     ASSIGNMENTS[i], permission, role)
        if like_state:
            # splice any remaining like_permissions at beginning of permissions
            permissions[0:0] = like_permissions
        # notifications
        notifications = []  # [ notification.Notification ]
        for n in s.iterchildren("notification"):
            notifications.append(
                Notification(
                    strip_none(n.get("condition")),  # python resolvable
                    strip_none(n.get("subject")),  # template source, i18n
                    strip_none(n.get("from")),  # template source
                    strip_none(n.get("to")),  # template source
                    strip_none(n.get("body")),  # template source, i18n
                    strip_none(n.get("note")),  # documentational note
                ))
        # states
        states.append(
            State(
                state_id, Message(strip_none(s.get("title")), domain),
                strip_none(s.get("note")), actions, permissions, notifications,
                tags,
                as_bool(
                    strip_none(s.get("permissions_from_parent")) or "false"),
                as_bool(strip_none(s.get("obsolete")) or "false")))

    STATE_IDS = [s.id for s in states]

    # transitions
    for t in workflow.iterchildren("transition"):
        assert_valid_attr_names(t, TRANS_ATTRS)
        for key in TRANS_ATTRS_REQUIREDS:
            if key == "source" and t.get(key) == "":
                # initial_state, an explicit empty string
                continue
            elif strip_none(t.get(key)) is None:
                raise SyntaxError('No required "%s" attribute in %s' %
                                  (key, etree.tostring(t)))

        # title
        title = strip_none(t.get("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 = strip_none(t.get(i))
            if not val:
                # we let setting of defaults be handled upstream
                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 = kw.pop("roles", None)  # space separated str
        if not is_zcml_permissionable(t):
            assert not roles, "Workflow [%s] - non-permissionable transition " \
                "does not allow @roles [%s]." % (name, roles)
            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" % (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.
            #
            # Note: the "-" char is not allowed within a permission id
            # (so we use "." also here).
            #
            tid = "%s.%s" % (sources[0] or "", destination)
            pid = "bungeni.%s.wf.%s" % (name, tid)
            if not ZCML_PROCESSED:
                # use "bungeni.Clerk" as default list of roles
                roles = (roles or "bungeni.Clerk").split()
                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"] = as_bool(
                    kw["require_confirmation"])
                assert kw["require_confirmation"] is not None
            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 = (Message(title, domain), source, destination)
            transitions.append(Transition(*args, **kw))
            log.debug("[%s] adding transition [%s-%s] [%s]" %
                      (name, source or "", destination, kw))

    return Workflow(name, features, allowed_tags, states, transitions, note)
Example #6
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", ""))
        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.
            #
            # Note: the "-" char is not allowed within a permission id 
            # (so we use "." also here).
            #
            tid = "%s.%s" % (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))
    
    return Workflow(workflow_name,
        workflow_features, workflow_facets, states, transitions, global_grants,
        workflow_title, workflow_description, note)
Example #7
0
def get_loaded_workflow(workflow_name):
    """Retrieve the previously loaded workflow."""
    try:
        return Workflow.get_singleton(workflow_name)
    except KeyError:
        return None  # not a "workflowed" feature
Example #8
0
def get_loaded_workflow(workflow_name):
    """Retrieve the previously loaded workflow."""
    try:
        return Workflow.get_singleton(workflow_name)
    except KeyError:
        return None  # not a "workflowed" feature