"""Sheets to assign workflows to resources and change states."""
from colander import deferred
from colander import OneOf
from colander import drop
from colander import All
from deform.widget import SelectWidget
from deform.widget import Widget
from pyramid.testing import DummyRequest
from zope.deprecation import deprecated
from zope.interface import implementer
from adhocracy_core.interfaces import ISheet
from adhocracy_core.schema import MappingSchema
from adhocracy_core.schema import DateTime
from adhocracy_core.schema import Text
from adhocracy_core.schema import SingleLine
from adhocracy_core.schema import SequenceSchema
from adhocracy_core.schema import SchemaNode
from adhocracy_core.schema import create_deferred_permission_validator
from adhocracy_core.sheets import add_sheet_to_registry
from adhocracy_core.sheets import sheet_meta
from adhocracy_core.sheets import AnnotationRessourceSheet
from adhocracy_core.utils import get_iresource
from adhocracy_core.interfaces import IResourceSheet
from adhocracy_core.interfaces import IWorkflow
[docs]class ISample(ISheet):
"""Sheet with the sample workflow."""
deprecated('ISample', 'Backward compatible code, dont use')
@deferred
def deferred_state_validator(node, kw: dict) -> OneOf:
"""Validate workflow state."""
context = kw['context']
workflow = kw['workflow']
request = kw['request']
creating = kw['creating']
if workflow is None:
allowed = []
elif creating:
allowed = [workflow._initial_state]
else:
nexts = workflow.get_next_states(context, request)
current = workflow.state_of(context)
allowed = [current] + nexts
return OneOf(allowed)
@deferred
def deferred_state_default(node, kw: dict) -> str:
"""Return initial workflow state."""
workflow = kw['workflow']
initial_state = workflow and workflow._initial_state or ''
return initial_state
@deferred
def deferred_state_widget(node, kw: dict) -> Widget:
"""Workflow state widget."""
validator = deferred_state_validator(node, kw)
states = [(x, x) for x in validator.choices]
return SelectWidget(values=states)
[docs]class StateName(SingleLine):
"""Workflow state name.
Possible values are set by the given `workflow` binding.
"""
@deferred
[docs] def validator(self, kw: dict):
"""Validator."""
workflow = kw['workflow']
if workflow is None:
states = []
else:
states = workflow._states.keys()
return OneOf(states)
@deferred
[docs]class StateData(MappingSchema):
"""Resource specific data for a workflow state."""
missing = drop
@deferred
[docs] def default(self, kw):
return {}
name = StateName()
description = Text(missing='',
default='')
start_date = DateTime(missing=None,
default=None)
[docs]class StateDataList(SequenceSchema):
"""List of StateData."""
data = StateData()
[docs]def deferred_workflow_validator(node: SchemaNode, kw: dict) -> callable:
"""Deferred workflow name validator."""
context = kw['context']
registry = kw['registry']
creating = kw['creating']
if creating:
meta = creating
else:
iresource = get_iresource(context)
meta = registry.content.resources_meta[iresource]
if meta.default_workflow in meta.alternative_workflows:
workflows = ('',) + meta.alternative_workflows
else:
workflows = ('', meta.default_workflow) + meta.alternative_workflows
return OneOf(workflows)
[docs]class Workflow(SingleLine):
"""SchemaNode for workflow types.
Default value requires the schema binding `workflow`.
"""
@deferred
[docs] def default(node: MappingSchema, kw: dict) -> IWorkflow:
workflow = kw['workflow']
return workflow and workflow.type or ''
@deferred
[docs] def validator(node: MappingSchema, kw: dict):
return All(deferred_workflow_validator(node, kw),
create_deferred_permission_validator('edit_workflow')(node,
kw),
)
@deferred
[docs]class WorkflowAssignmentSchema(MappingSchema):
"""Workflow assignment sheet data structure."""
workflow = Workflow(missing=drop)
"""Workflow assigned to the sheet context resource.
Available workflows are defined in :mod:`adhocracy_core.workflows`.
"""
workflow_state = SingleLine(missing=drop,
default=deferred_state_default,
validator=deferred_state_validator,
widget=deferred_state_widget,
)
"""Workflow state of the sheet context resource.
Setting this executes a transition to the new state value.
"""
state_data = StateDataList(missing=drop)
"""Optional List of :class:`StateData`.
example:
{'name': 'state1', 'description': 'text', 'start_date': <DateTime>}
"""
[docs]class IWorkflowAssignment(ISheet):
"""Market interface for the workflow assignment sheet."""
@implementer(IResourceSheet)
[docs]class WorkflowAssignmentSheet(AnnotationRessourceSheet):
"""Sheet class for workflow assignment sheets.
It allows to view and modify the workflow state of `context`.
If the you set a new workflow state a transition to this state is executed.
The workflow of `context` is only found, if the :term:`resource_type`
metadata of `context` has a valid 'workflow` entry.
"""
def _get_basic_bindings(self) -> dict:
bindings = super()._get_basic_bindings()
workflow = self._get_workflow()
bindings['workflow'] = workflow
return bindings
def _get_workflow(self) -> IWorkflow:
if self.creating:
name = self.creating.default_workflow
else:
name = self._get_data_appstruct().get('workflow')
workflow = self.registry.content.workflows.get(name, None)
return workflow
def _store_data(self, appstruct: dict, initialize_workflow=True):
if 'workflow' in appstruct and 'workflow_state' in appstruct:
del appstruct['workflow_state'] # we cannot change both
if 'workflow' in appstruct and initialize_workflow:
self._initialize(appstruct['workflow'])
elif 'workflow_state' in appstruct:
self._do_transition_to(appstruct['workflow_state'])
del appstruct['workflow_state']
super()._store_data(appstruct)
def _initialize(self, name: str):
if name != '': # pragma: no branch
workflow = self.registry.content.workflows[name]
workflow.initialize(self.context, request=self.request)
def _get_data_appstruct(self) -> dict:
"""Get data appstruct."""
appstruct = super()._get_data_appstruct()
if 'workflow' in appstruct:
name = appstruct['workflow']
if name: # pragma: no branch
workflow = self.registry.content.workflows[name]
appstruct['workflow_state'] = workflow.state_of(self.context)
return appstruct
def _do_transition_to(self, name: str):
"""Do transition to state `name`, don`t check user permissions."""
request = self.request or DummyRequest() # ease testing
workflow = self._get_workflow()
workflow.transition_to_state(self.context, request, to_state=name)
workflow_meta = sheet_meta._replace(
isheet=IWorkflowAssignment,
schema_class=WorkflowAssignmentSchema,
sheet_class=WorkflowAssignmentSheet,
permission_edit='do_transition'
)
[docs]def includeme(config):
"""Add sheet."""
add_sheet_to_registry(workflow_meta, config.registry)