Skip to content

Commit

Permalink
Merge d472da2 into ceb3d20
Browse files Browse the repository at this point in the history
  • Loading branch information
emil-balashov committed Jan 18, 2020
2 parents ceb3d20 + d472da2 commit cc093ad
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 308 deletions.
62 changes: 0 additions & 62 deletions demo/process.ipynb

This file was deleted.

51 changes: 34 additions & 17 deletions django_logic/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import logging

from django_logic.state import State


class BaseCommand(object):
"""
Command
Implements pattern Command
"""
def __init__(self, commands=None, transition=None):
self._commands = commands or []
Expand All @@ -15,43 +20,55 @@ def execute(self, *args, **kwargs):


class Conditions(BaseCommand):
def execute(self, instance: any, **kwargs):
def execute(self, state: State, **kwargs):
"""
It checks every condition for the provided instance by executing every command
:param instance: any
:param state: State object
:return: True or False
"""
return all(command(instance, **kwargs) for command in self._commands)
return all(command(state.instance, **kwargs) for command in self._commands)


class Permissions(BaseCommand):
def execute(self, instance: any, user: any, **kwargs):
def execute(self, state: State, user: any, **kwargs):
"""
It checks the permissions for the provided user and instance by executing evey command
If user is None then permissions passed
:param instance: any
:param state: State object
:param user: any or None
:return: True or False
"""
return user is None or all(command(instance, user, **kwargs) for command in self._commands)
return user is None or all(command(state.instance, user, **kwargs) for command in self._commands)


class SideEffects(BaseCommand):
def execute(self, instance: any, field_name: str, **kwargs):
def execute(self, state: State, **kwargs):
"""Side-effects execution"""
logging.info(f'{state.instance_key} side-effects of {self._transition.action_name} started')
try:
for command in self._commands:
command(instance, **kwargs)
except Exception:
self._transition.fail_transition(instance, field_name, **kwargs)
command(state.instance, **kwargs)
except Exception as error:
logging.error(f"{state.instance_key} side-effects of "
f"'{self._transition.action_name}' action failed with {error}")
self._transition.fail_transition(state, **kwargs)
else:
self._transition.complete_transition(instance, field_name, **kwargs)
logging.info(f"{state.instance_key} side-effects of "
f"'{self._transition.action_name}' action succeed")
self._transition.complete_transition(state, **kwargs)


class Callbacks(BaseCommand):
def execute(self, instance: any, field_name: str, **kwargs):
def execute(self, state: State, **kwargs):
"""
Callback execution method.
It runs commands one by one, if any of them raises an exception
it will stop execution and send a message to logger.
Please note, it doesn't run failure callbacks in case of exception.
"""
try:
for command in self.commands:
command(instance, **kwargs)
except Exception:
# TODO: handle exception
pass
command(state.instance, **kwargs)
except Exception as error:
logging.error(f"{state.instance_key} callbacks of "
f"'{self._transition.action_name}` action failed with {error}")
2 changes: 2 additions & 0 deletions django_logic/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def get_target_states(process) -> set:
states = set()
for transition in process.transitions:
states.add(transition.target)
if transition.failed_state:
states.add(transition.failed_state)
if transition.in_progress_state:
states.add(transition.in_progress_state)
return states
Expand Down
4 changes: 0 additions & 4 deletions django_logic/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
class ManyTransitions(Exception):
pass


class TransitionNotAllowed(Exception):
pass
65 changes: 41 additions & 24 deletions django_logic/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from functools import partial

from django_logic.commands import Conditions, Permissions
from django_logic.exceptions import ManyTransitions, TransitionNotAllowed
from django_logic.exceptions import TransitionNotAllowed
from django_logic.state import State

logger = logging.getLogger(__name__)
Expand All @@ -25,27 +25,49 @@ class Process(object):
permissions = []
conditions_class = Conditions
permissions_class = Permissions
state_class = State
process_name = 'process'
queryset = None # It should be set up once for the main process

def __init__(self, field_name: str, instance=None):
def __init__(self, field_name='', instance=None, state=None):
"""
:param field_name:
:param field_name: state or status field name
:param instance: Model instance
"""
self.field_name = field_name
self.instance = instance
if field_name is '' or instance is None:
assert state is not None
self.state = state
elif state is None:
assert field_name and instance is not None
self.state = self.state_class(queryset=self.queryset, instance=instance, field_name=field_name)
else:
raise AttributeError('Process class requires either state field name and instance or state object')

def __getattr__(self, item):
transitions = list(self.get_available_transitions(action_name=item))
return partial(self._get_transition_method, item)

if len(transitions) == 1:
return partial(transitions[0].change_state,
instance=self.instance,
field_name=self.field_name)
def _get_transition_method(self, action_name: str, **kwargs):
"""
It returns a callable transition method by provided action name.
"""
user = kwargs.pop('user') if 'user' in kwargs else None
transitions = list(self.get_available_transitions(action_name=action_name, user=user))

# This exceptions should be handled otherwise it will be very annoying
elif transitions:
raise ManyTransitions("There are several transitions available")
raise AttributeError(f"Process class {self.__class__} has no transition with action name {item}")
if len(transitions) == 1:
transition = transitions[0]
logger.info(f"{self.state.instance_key}, process {self.process_name} "
f"executes '{action_name}' transition from {self.state.cached_state} "
f"to {transition.target}")
return transition.change_state(self.state, **kwargs)

elif len(transitions) > 1:
logger.error(f"Runtime error: {self.state.instance_key} has several "
f"transitions with action name '{action_name}'. "
f"Make sure to specify conditions and permissions accordingly to fix such case")
raise TransitionNotAllowed("There are several transitions available")
raise TransitionNotAllowed(f"Process class {self.__class__} has no transition with action name {action_name}")

def is_valid(self, user=None) -> bool:
"""
Expand All @@ -55,35 +77,30 @@ def is_valid(self, user=None) -> bool:
"""
permissions = self.permissions_class(commands=self.permissions)
conditions = self.conditions_class(commands=self.conditions)
return (permissions.execute(self.instance, user) and
conditions.execute(self.instance))
return (permissions.execute(self.state, user) and
conditions.execute(self.state))

def get_available_transitions(self, user=None, action_name=None):
"""
It returns all available transition which meet conditions and pass permissions.
Including nested processes.
:param action_name:
:param user: any object which used to validate permissions
:param action_name: str
:return: yield `django_logic.Transition`
"""
if not self.is_valid(user):
return

state = State().get_db_state(self.instance, self.field_name)
for transition in self.transitions:
if action_name is not None and transition.action_name != action_name:
continue

if state in transition.sources and transition.is_valid(self.instance,
self.field_name,
user):
if self.state.cached_state in transition.sources and transition.is_valid(self.state, user):
yield transition

for sub_process_class in self.nested_processes:
sub_process = sub_process_class(instance=self.instance, field_name=self.field_name)
for transition in sub_process.get_available_transitions(user=user,
action_name=action_name):
yield transition
sub_process = sub_process_class(state=self.state)
yield from sub_process.get_available_transitions(user=user, action_name=action_name)


class ProcessManager:
Expand All @@ -107,7 +124,7 @@ def non_state_fields(self):
"""
field_names = set()
for field in self._meta.fields:
if not field.primary_key and not field.name in self.state_fields:
if not field.primary_key and field.name not in self.state_fields:
field_names.add(field.name)

if field.name != field.attname:
Expand Down

0 comments on commit cc093ad

Please sign in to comment.