Skip to content
Snippets Groups Projects
Commit 1855b1d3 authored by Todd Dembrey's avatar Todd Dembrey
Browse files

Refactor the workflow to be more explicit and less magical

parent c05772db
No related branches found
No related tags found
No related merge requests found
...@@ -43,6 +43,11 @@ class PhaseFactory(factory.Factory): ...@@ -43,6 +43,11 @@ class PhaseFactory(factory.Factory):
name = factory.Faker('word') name = factory.Faker('word')
actions = ListSubFactory(ActionFactory, count=factory.SelfAttribute('num_actions')) actions = ListSubFactory(ActionFactory, count=factory.SelfAttribute('num_actions'))
@classmethod
def _create(cls, model_class, *args, **kwargs):
actions = kwargs.pop('actions')
new_class = type(model_class.__name__, (model_class,), {'actions': actions})
return new_class(*args, **kwargs)
class StageFactory(factory.Factory): class StageFactory(factory.Factory):
class Meta: class Meta:
......
...@@ -23,6 +23,10 @@ class TestWorkflowCreation(SimpleTestCase): ...@@ -23,6 +23,10 @@ class TestWorkflowCreation(SimpleTestCase):
phase = workflow.stages[0].phases[0] phase = workflow.stages[0].phases[0]
self.assertEqual(workflow.current(str(phase)), phase) self.assertEqual(workflow.current(str(phase)), phase)
def test_returns_next_stage(self):
workflow = WorkflowFactory(num_stages=2, stages__num_phases=1)
self.assertEqual(workflow.next_stage(workflow.stages[0]), workflow.stages[1])
def test_returns_none_if_no_next(self): def test_returns_none_if_no_next(self):
workflow = WorkflowFactory(num_stages=1, stages__num_phases=1) workflow = WorkflowFactory(num_stages=1, stages__num_phases=1)
self.assertEqual(workflow.next(workflow.stages[0].phases[0]), None) self.assertEqual(workflow.next(workflow.stages[0].phases[0]), None)
...@@ -46,19 +50,20 @@ class TestStageCreation(SimpleTestCase): ...@@ -46,19 +50,20 @@ class TestStageCreation(SimpleTestCase):
self.assertEqual(stage.form, form) self.assertEqual(stage.form, form)
self.assertEqual(stage.phases, phases) self.assertEqual(stage.phases, phases)
def test_can_iterate_through_phases(self): def test_can_get_next_phase(self):
stage = StageFactory() stage = StageFactory(num_phases=2)
for phase, check in zip(stage, stage.phases): # type: ignore # spurious error self.assertEqual(stage.next(stage.phases[0]), stage.phases[1])
self.assertEqual(phase, check)
def test_get_none_if_no_next_phase(self):
stage = StageFactory(num_phases=1)
self.assertEqual(stage.next(stage.phases[0]), None)
class TestPhaseCreation(SimpleTestCase): class TestPhaseCreation(SimpleTestCase):
def test_can_create_phase(self): def test_can_create_phase(self):
actions = ActionFactory.create_batch(2)
name = 'the_phase' name = 'the_phase'
phase = Phase(name, actions) phase = Phase(name)
self.assertEqual(phase.name, name) self.assertEqual(phase.name, name)
self.assertEqual(phase.actions, [action.name for action in actions])
def test_can_get_action_from_phase(self): def test_can_get_action_from_phase(self):
actions = ActionFactory.create_batch(3) actions = ActionFactory.create_batch(3)
......
...@@ -7,43 +7,65 @@ class Workflow: ...@@ -7,43 +7,65 @@ class Workflow:
def __init__(self, name: str, stages: Sequence['Stage']) -> None: def __init__(self, name: str, stages: Sequence['Stage']) -> None:
self.name = name self.name = name
self.stages = stages self.stages = stages
self.mapping = self.build_mapping(stages)
def build_mapping(self, stages: Sequence['Stage']) -> Dict[str, Tuple[int, int]]:
mapping: Dict[str, Tuple[int, int]] = {}
for i, stage in enumerate(stages):
for j, phase in enumerate(stage):
while str(phase) in mapping:
phase.occurance += 1
mapping[str(phase)] = (i, j)
return mapping
def current_index(self, phase: Union['Phase', str, None]):
if isinstance(phase, Phase):
phase = str(phase)
try:
return self.mapping[phase]
except KeyError:
return 0, -1
def __getitem__(self, value): def current(self, current_phase: Union[str, 'Phase']) -> Union['Phase', None]:
return self.stages[value[0]].phases[value[1]] if isinstance(current_phase, Phase):
return current_phase
if not current_phase:
return self.first()
stage_name, phase_name, _ = current_phase.split('__')
for stage in self.stages:
if stage == stage_name:
return stage.current(phase_name)
return None
def first(self):
return self.stages[0].next()
def process(self, current_phase: str, action: str) -> Union['Phase', None]:
phase = self.current(current_phase)
new_phase = phase.process(action)
if not new_phase:
new_stage = self.next_stage(phase.stage)
return new_stage.first()
return new_phase
def next_stage(self, current_stage: 'Stage') -> 'Stage':
for i, stage in enumerate(self.stages):
if stage == current_stage:
try:
return self.stages[i+1]
except IndexError:
pass
return None
def next(self, current_phase: Union[str, 'Phase']=None) -> Union['Phase', None]:
if not current_phase:
return self.first()
def current(self, current_phase: str): phase = self.current(current_phase)
return self[self.current_index(current_phase)]
def next(self, current_phase: Union['Phase', str]=None) -> Union['Phase', None]: for stage in self.stages:
stage_idx, phase_idx = self.current_index(current_phase) if stage == phase.stage:
try: next_phase = stage.next(phase)
return self[stage_idx, phase_idx + 1] if not next_phase:
except IndexError: continue
try: return next_phase
return self[stage_idx + 1, 0]
except IndexError:
return None
next_stage = self.next_stage(phase.stage)
if next_stage:
return stage.next()
return None
class Stage(Iterable['Phase']):
def __str__(self):
return self.name
class Stage:
def __init__(self, name: str, form: Form, phases: Sequence['Phase'], def __init__(self, name: str, form: Form, phases: Sequence['Phase'],
current_phase: Union['Phase', None]=None) -> None: current_phase: Union['Phase', None]=None) -> None:
self.name = name self.name = name
...@@ -52,22 +74,51 @@ class Stage(Iterable['Phase']): ...@@ -52,22 +74,51 @@ class Stage(Iterable['Phase']):
phase.stage = self phase.stage = self
self.phases = phases self.phases = phases
def __iter__(self) -> Iterator['Phase']: def __eq__(self, other):
yield from self.phases if isinstance(other, str):
return self.name == other
return super().__eq__(other)
def __str__(self): def __str__(self):
return self.name return self.name
def current(self, phase_name: str) -> 'Phase':
for phase in self.phases:
if phase == phase_name:
return phase
return None
def first(self) -> 'Phase':
return self.phases[0]
def next(self, current_phase: 'Phase'=None) -> 'Phase':
if not current_phase:
return self.first()
for i, phase in enumerate(self.phases):
if phase == current_phase:
try:
return self.phases[i+1]
except IndexError:
pass
return None
class Phase: class Phase:
def __init__(self, name: str, actions: Sequence['Action']) -> None: actions: Sequence['Action'] = list()
def __init__(self, name: str) -> None:
self.name = name self.name = name
self.stage: Union['Stage', None] = None self.stage: Union['Stage', None] = None
self._actions = {action.name: action for action in actions} self._actions = {action.name: action for action in self.actions}
self.occurance: int = 0 self.occurance: int = 0
def __eq__(self, other):
if isinstance(other, str):
return self.name == other
return super().__eq__(other)
@property @property
def actions(self): def action_names(self):
return list(self._actions.keys()) return list(self._actions.keys())
def __str__(self): def __str__(self):
...@@ -76,6 +127,9 @@ class Phase: ...@@ -76,6 +127,9 @@ class Phase:
def __getitem__(self, value): def __getitem__(self, value):
return self._actions[value] return self._actions[value]
def process(self, action):
return self[action]()
class Action: class Action:
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
...@@ -84,35 +138,61 @@ class Action: ...@@ -84,35 +138,61 @@ class Action:
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.process(*args, **kwargs) return self.process(*args, **kwargs)
def process(self, *args, **kwargs): def process(self, *args, **kwargs) -> 'Phase':
# Use this to define the behaviour of the action # Use this to define the behaviour of the action
raise NotImplementedError raise NotImplementedError
# --- OTF Workflow --- # --- OTF Workflow ---
class ChangePhaseAction(Action):
def __init__(self, phase, *args, **kwargs):
self.target_phase = phase
super().__init__(*args, **kwargs)
def process(self):
if isinstance(self.target_phase, str):
phase = globals()[self.target_phase]
else:
phase = self.target_phase
return phase
reject_action = ChangePhaseAction('rejected', 'Reject')
accept_action = ChangePhaseAction('accepted', 'Accept')
progress_external = ChangePhaseAction('external_review', 'Progress')
progress_stage = ChangePhaseAction(None, 'Progress Stage')
class ReviewPhase(Phase): class ReviewPhase(Phase):
pass actions = [progress_stage, reject_action]
class ProposalReviewPhase(Phase):
actions = [progress_external, reject_action]
next_phase = Action('next') class FinalReviewPhase(Phase):
actions = [accept_action, reject_action]
internal_review = ReviewPhase('Under Review', [next_phase])
ac_review = ReviewPhase('Under Review', [next_phase]) concept_review = ReviewPhase('Internal Review')
response = Phase('Ready to Respond', [next_phase]) proposal_review = ProposalReviewPhase('Internal Review')
rejected = Phase('Rejected', []) external_review = FinalReviewPhase('AC Review')
accepted = Phase('Accepted', [next_phase]) rejected = Phase('Rejected')
progress = Phase('Progress', [next_phase]) accepted = Phase('Accepted')
standard_stage = Stage('Standard', Form(), [internal_review, response, ac_review, response, accepted, rejected]) concept_note = Stage('Concept', Form(), [concept_review, accepted, rejected])
first_stage = Stage('Standard', Form(), [internal_review, response, progress, rejected]) proposal = Stage('Proposal', Form(), [proposal_review, external_review, accepted, rejected])
single_stage = Workflow('Single Stage', [standard_stage]) single_stage = Workflow('Single Stage', [proposal])
two_stage = Workflow('Two Stage', [first_stage, standard_stage]) two_stage = Workflow('Two Stage', [concept_note, proposal])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment