Skip to content

Commit

Permalink
[#105] Introduce context variable which contains information about th…
Browse files Browse the repository at this point in the history
…e hooking as well and document the dynamic hooking [WIP]
  • Loading branch information
javrasya committed Nov 17, 2019
1 parent af99b85 commit 5543e3c
Show file tree
Hide file tree
Showing 16 changed files with 324 additions and 90 deletions.
44 changes: 44 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,50 @@ Usage
Whenever a model object is saved, it's state field will be initialized with the
state is given at step-4 above by ``django-river``.

Hooking Up With The Events
--------------------------

`django-river` provides you to have your custom code run on certain events. And since version v2.1.0 this has also been supported for on the fly changes. You can
create your functions and also the hooks to a certain events by just creating few database items. Let's see what event types that can be hooked a function to;

* An approval is approved
* A transition goes through
* The workflow is complete

For all these event types, you can create a hooking with a given function which is created separately and preliminary than the hookings for all the workflow objects you have
or you will possible have, or for a specific workflow object. You can also hook up before or after the events happen.

Create Function
^^^^^^^^^^^^^^^

This will be the description of your functions. So you define them once and you can use them with multiple hooking up. Just go to `/admin/river/function/` admin page
and create your functions there. `django-river` function admin support python code highlights.

.. code:: python
INSTALLED_APPS=[
...
codemirror2
river
...
]
Here is an example function;

.. code:: python
from datetime import datetime
def handle(context):
print(datetime.now())
print(context)
`django-river` will pass a `context` down to your function in respect to in order for you to know why the function is triggered. And the `context` will look different for
different type of events





Contribute
----------

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ factory-boy==2.11.1
mock==2.0.0
pyhamcrest==1.9.0
django-cte==1.1.4
django-codemirror2==0.2
6 changes: 4 additions & 2 deletions river/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django.contrib import admin

from river.admin.transitionapprovalmeta import TransitionApprovalMetaAdmin
from river.admin.workflow import WorkflowAdmin
from river.admin.function_admin import *
from river.admin.transitionapprovalmeta import *
from river.admin.workflow import *
from river.admin.hook_admins import *
from river.models import State

admin.site.register(State)
25 changes: 25 additions & 0 deletions river/admin/function_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from codemirror2.widgets import CodeMirrorEditor
from django import forms
from django.contrib import admin

from river.models import Function


class FunctionForm(forms.ModelForm):
body = forms.CharField(widget=CodeMirrorEditor(options={'mode': 'python'}))

class Meta:
model = Function
fields = ('name', 'body',)


class FunctionAdmin(admin.ModelAdmin):
form = FunctionForm
list_display = ('name', 'function_version', 'date_created', 'date_updated')
readonly_fields = ('version', 'date_created', 'date_updated')

def function_version(self, obj):
return "v%s" % obj.version


admin.site.register(Function, FunctionAdmin)
20 changes: 20 additions & 0 deletions river/admin/hook_admins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.contrib import admin

from river.models import OnApprovedHook, OnTransitHook, OnCompleteHook


class OnApprovedHookAdmin(admin.ModelAdmin):
list_display = ('workflow', 'callback_function', 'transition_approval_meta')


class OnTransitHookAdmin(admin.ModelAdmin):
list_display = ('workflow', 'callback_function', 'source_state', 'destination_state')


class OnCompleteHookAdmin(admin.ModelAdmin):
list_display = ('workflow', 'callback_function')


admin.site.register(OnApprovedHook, OnApprovedHookAdmin)
admin.site.register(OnTransitHook, OnTransitHookAdmin)
admin.site.register(OnCompleteHook, OnCompleteHookAdmin)
70 changes: 68 additions & 2 deletions river/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError

__author__ = 'ahmetdal'

LOGGER = logging.getLogger(__name__)


Expand All @@ -24,9 +22,77 @@ def ready(self):
except (OperationalError, ProgrammingError):
pass

for model_class in self._get_all_workflow_classes():
self._register_hook_inlines(model_class)

LOGGER.debug('RiverApp is loaded.')

@classmethod
def _get_all_workflow_fields(cls):
from river.core.workflowregistry import workflow_registry
return reduce(operator.concat, map(list, workflow_registry.workflows.values()), [])

@classmethod
def _get_all_workflow_classes(cls):
from river.core.workflowregistry import workflow_registry
return list(workflow_registry.class_index.values())

@classmethod
def _get_workflow_class_fields(cls, model):
from river.core.workflowregistry import workflow_registry
return workflow_registry.workflows[id(model)]

def _register_hook_inlines(self, model):
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline

from river.models import OnApprovedHook, OnTransitHook, OnCompleteHook
_get_workflow_class_fields = self._get_workflow_class_fields

class BaseHookInline(GenericTabularInline):
fields = ("callback_function", "hook_type")

class OnApprovedHookInline(BaseHookInline):
model = OnApprovedHook

def __init__(self, *args, **kwargs):
super(OnApprovedHookInline, self).__init__(*args, **kwargs)
self.fields += ("transition_approval_meta",)

class OnTransitHookInline(BaseHookInline):
model = OnTransitHook

def __init__(self, *args, **kwargs):
super(OnTransitHookInline, self).__init__(*args, **kwargs)
self.fields += ("source_state", "destination_state",)

class OnCompleteHookInline(BaseHookInline):
model = OnCompleteHook

class DefaultWorkflowModelAdmin(admin.ModelAdmin):
inlines = [
OnApprovedHookInline,
OnTransitHookInline,
OnCompleteHookInline
]

def __init__(self, *args, **kwargs):
super(DefaultWorkflowModelAdmin, self).__init__(*args, **kwargs)
self.readonly_fields += tuple(_get_workflow_class_fields(model))

registered_admin = admin.site._registry.get(model, None)
if registered_admin:
if OnApprovedHookInline not in registered_admin.inlines:
registered_admin.inlines.append(OnApprovedHookInline)
registered_admin.inlines.append(OnTransitHookInline)
registered_admin.inlines.append(OnCompleteHookInline)
registered_admin.readonly_fields = list(set(list(registered_admin.readonly_fields) + list(_get_workflow_class_fields(model))))
admin.site._registry[model] = registered_admin
else:
admin.site.register(model, DefaultWorkflowModelAdmin)



def handle(context):
print(datetime.now())
print(context)
28 changes: 28 additions & 0 deletions river/migrations/0004_auto_20191016_1731.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 2.1.4 on 2019-10-16 22:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('river', '0003_auto_20191015_1628'),
]

operations = [
migrations.AlterField(
model_name='onapprovedhook',
name='hook_type',
field=models.CharField(choices=[('BEFORE', 'Before'), ('AFTER', 'After')], max_length=50, verbose_name='When?'),
),
migrations.AlterField(
model_name='oncompletehook',
name='hook_type',
field=models.CharField(choices=[('BEFORE', 'Before'), ('AFTER', 'After')], max_length=50, verbose_name='When?'),
),
migrations.AlterField(
model_name='ontransithook',
name='hook_type',
field=models.CharField(choices=[('BEFORE', 'Before'), ('AFTER', 'After')], max_length=50, verbose_name='When?'),
),
]
7 changes: 5 additions & 2 deletions river/models/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class Function(BaseModel):
body = models.TextField(verbose_name=_("Function Body"), max_length=100000, null=False, blank=False)
version = models.IntegerField(verbose_name=_("Function Version"), default=0)

def __str__(self):
return "%s - %s" % (self.name, "v%s" % self.version)

def get(self):
func = loaded_functions.get(self.name, None)
if not func or func["version"] != self.version:
Expand All @@ -23,10 +26,10 @@ def get(self):
return func["function"]

def _load(self):
func_body = "def _wrapper(*args, **kwargs):\n"
func_body = "def _wrapper(context):\n"
for line in self.body.split("\n"):
func_body += "\t" + line + "\n"
func_body += "\thandle(*args,**kwargs)\n"
func_body += "\thandle(context)\n"
exec(func_body)
return eval("_wrapper")

Expand Down
4 changes: 2 additions & 2 deletions river/models/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ class Meta:
object_id = models.PositiveIntegerField(blank=True, null=True)
workflow_object = GenericForeignKey('content_type', 'object_id')

hook_type = models.CharField(_('Status'), choices=HOOK_TYPES, max_length=50)
hook_type = models.CharField(_('When?'), choices=HOOK_TYPES, max_length=50)

def execute(self, context):
try:
self.callback_function.get()(**context)
self.callback_function.get()(context)
except Exception as e:
LOGGER.exception(e)
52 changes: 35 additions & 17 deletions river/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __enter__(self):
hook_type=BEFORE
)
):
hook.execute(self._get_context())
hook.execute(self._get_context(BEFORE))

LOGGER.debug("The signal that is fired right before the transition ( %s -> %s ) happened for %s" % (
self.transition_approval.source_state.label, self.transition_approval.destination_state.label, self.workflow_object))
Expand All @@ -60,15 +60,21 @@ def __exit__(self, type, value, traceback):
hook_type=AFTER
)
):
hook.execute(self._get_context())
hook.execute(self._get_context(AFTER))
LOGGER.debug("The signal that is fired right after the transition ( %s -> %s ) happened for %s" % (
self.transition_approval.source_state.label, self.transition_approval.destination_state.label, self.workflow_object))

def _get_context(self):
def _get_context(self, when):
return {
"workflow": self.workflow,
"workflow_object": self.workflow_object,
"transition_approval": self.transition_approval
"hook": {
"type": "on-transit",
"when": when,
"payload": {
"workflow": self.workflow,
"workflow_object": self.workflow_object,
"transition_approval": self.transition_approval
}
},
}


Expand All @@ -89,7 +95,7 @@ def __enter__(self):
hook_type=BEFORE
)
):
hook.execute(self._get_context())
hook.execute(self._get_context(BEFORE))

LOGGER.debug("The signal that is fired right before a transition approval is approved for %s due to transition %s -> %s" % (
self.workflow_object, self.transition_approval.source_state.label, self.transition_approval.destination_state.label))
Expand All @@ -103,15 +109,21 @@ def __exit__(self, type, value, traceback):
hook_type=AFTER
)
):
hook.execute(self._get_context())
hook.execute(self._get_context(AFTER))
LOGGER.debug("The signal that is fired right after a transition approval is approved for %s due to transition %s -> %s" % (
self.workflow_object, self.transition_approval.source_state.label, self.transition_approval.destination_state.label))

def _get_context(self):
def _get_context(self, when):
return {
"workflow": self.workflow,
"workflow_object": self.workflow_object,
"transition_approval": self.transition_approval
"hook": {
"type": "on-approved",
"when": when,
"payload": {
"workflow": self.workflow,
"workflow_object": self.workflow_object,
"transition_approval": self.transition_approval
}
},
}


Expand All @@ -133,7 +145,7 @@ def __enter__(self):
hook_type=BEFORE
)
):
hook.execute(self._get_context())
hook.execute(self._get_context(BEFORE))
LOGGER.debug("The signal that is fired right before the workflow of %s is complete" % self.workflow_object)

def __exit__(self, type, value, traceback):
Expand All @@ -145,11 +157,17 @@ def __exit__(self, type, value, traceback):
hook_type=AFTER
)
):
hook.execute(self._get_context())
hook.execute(self._get_context(AFTER))
LOGGER.debug("The signal that is fired right after the workflow of %s is complete" % self.workflow_object)

def _get_context(self):
def _get_context(self, when):
return {
"workflow": self.workflow,
"workflow_object": self.workflow_object,
"hook": {
"type": "on-complete",
"when": when,
"payload": {
"workflow": self.workflow,
"workflow_object": self.workflow_object,
}
},
}
9 changes: 3 additions & 6 deletions river/tests/hooking/base_hooking_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@

callback_method = """
from river.tests.hooking.base_hooking_test import callback_output
def handle(*args, **kwargs):
print(kwargs)
callback_output['%s'] = {
"args": args,
"kwargs": kwargs
}
def handle(context):
print(context)
callback_output['%s'] = context
"""


Expand Down
Loading

0 comments on commit 5543e3c

Please sign in to comment.