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)
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)
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)
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)
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)
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)
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