Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add support for drawing transitions

  • Loading branch information...
commit 67591a6ae2b6859e086bfcb89723853bc44d637f 1 parent 6dbee4a
@kmmbvnr authored
View
13 README.md
@@ -152,8 +152,21 @@ Arguments sent with these signals:
Target model state
+### Drawing transitions
+
+ Renders a graphical overview of your models states transitions
+
+ # Create a dot file
+ $ ./manage.py graph_transitions -a > transitions.dot
+
+ # Create a PNG image file only for specific model
+ $ ./manage.py graph_transitions -o blog_transitions.png myapp.Blog
+
+
Changelog
---------
+django 1.4.0 2011-12-21
+ * Add graph_transition command fro drawing state transition picture
django-fsm 1.3.0 2011-07-28
* Add direct field modification protection
View
4 django_fsm/db/fields/fsmfield.py
@@ -3,6 +3,7 @@
"""
State tracking functionality for django models
"""
+import warnings
from collections import defaultdict
from functools import wraps
from django.db import models
@@ -108,6 +109,9 @@ def transition(field=None, source='*', target=None, save=False, conditions=[]):
Set target to None if current state need to be validated and not
changed after function call
"""
+ if field is None:
+ warnings.warn("Non explicid field transition support going to be removed", DeprecationWarning, stacklevel=2)
+
# pylint: disable=C0111
def inner_transition(func):
if not hasattr(func, '_django_fsm'):
View
0  django_fsm/management/__init__.py
No changes.
View
0  django_fsm/management/commands/__init__.py
No changes.
View
102 django_fsm/management/commands/graph_transitions.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8; mode: django -*-
+import pygraphviz
+from optparse import make_option
+from django.core.management.base import BaseCommand
+from django.db.models import get_apps, get_app, get_models, get_model
+from django_fsm.db.fields import FSMField
+
+def all_fsm_fields(model):
+ return [field for field in model._meta.fields \
+ if isinstance(field, FSMField)]
+
+
+def node_name(field, state):
+ opts = field.model._meta
+ return "%s.%s.%s.%s" % (opts.app_label,opts.verbose_name, field.name, state)
+
+
+def generate_dot(fields):
+ result = pygraphviz.AGraph(directed=True)
+ model_graphs = {}
+
+ for field in fields:
+ sources, any_targets = [], []
+
+ for transition in field.transitions:
+ for source, target in transition._django_fsm.transitions.iteritems():
+ opts = field.model._meta
+ if field.model in model_graphs:
+ model_graph = model_graphs[field.model]
+ else:
+ model_graph = result.subgraph(name="cluster_%s_%s" % (opts.app_label, opts.object_name),
+ label="%s.%s" % (opts.app_label, opts.object_name))
+
+ if source == '*':
+ any_targets.append(target)
+ else:
+ if target != None:
+ source_node = node_name(field, source)
+ target_node = node_name(field, target)
+ if source_node not in model_graph:
+ model_graph.add_node(source_node, label=source)
+ if target_node not in model_graph:
+ model_graph.add_node(target_node, label=target)
+ model_graph.add_edge(source_node, target_node)
+ sources.append(source)
+ for target in any_targets:
+ target_node = node_name(field, target)
+ model_graph.add_node(target_node, label=target)
+ for source in sources:
+ model_graph.add_edge(node_name(field, source), target_node)
+
+ return result
+
+
+class Command(BaseCommand):
+ requires_model_validation = True
+
+ option_list = BaseCommand.option_list + (
+ make_option('--output', '-o', action='store', dest='outputfile',
+ help='Render output file. Type of output dependent on file extensions. Use png or jpg to render graph to image.'),
+ make_option('--layout', '-l', action='store', dest='layout', default='dot',
+ help='Layout to be used by GraphViz for visualization. Layouts: circo dot fdp neato nop nop1 nop2 twopi'),
+ )
+
+
+ help = ("Creates a GraphViz dot file with transitions for selected fields")
+ args = "[appname[.model[.field]]]"
+
+ def render_output(self, graph, **options):
+ graph.layout(prog=options['layout'])
+ graph.draw(options['outputfile'])
+
+ def handle(self, *args, **options):
+ fields = []
+ if len(args) != 0:
+ for arg in args:
+ field_spec = arg.split('.')
+
+ if len(field_spec) == 1:
+ app = get_app(field_spec[0])
+ models = get_models(app)
+ for model in models:
+ fields += all_fsm_fields(model)
+ elif len(field_spec) == 2:
+ model = get_model(field_spec[0], field_spec[1])
+ fields += all_fsm_fields(model)
+ elif len(field_spec) == 3:
+ model = get_model(field_spec[0], field_spec[1])
+ fields += [model._meta.get_field_by_name(field_spec[2])[0]]
+ else:
+ for app in get_apps():
+ for model in get_models(app):
+ fields += all_fsm_fields(model)
+
+ dotdata = generate_dot(fields)
+
+ if options['outputfile']:
+ self.render_output(dotdata, **options)
+ else:
+ print dotdata
+
+
View
2  setup.py
@@ -7,7 +7,7 @@
setup(
name='django-fsm',
- version='1.3.0',
+ version='1.4.0',
description='Django friendly finite state machine support.',
author='Mikhail Podgurskiy',
author_email='kmmbvnr@gmail.com',
View
3  tests/requirements.pip
@@ -1,4 +1,5 @@
-django==1.2.1
+django==1.3.1
+pygraphviz
# tests only
ipython
View
2  tests/settings.py
@@ -1,4 +1,4 @@
-PROJECT_APPS = ('django_fsm',)
+PROJECT_APPS = ('django_fsm', 'testapp',)
INSTALLED_APPS = ('django_jenkins',) + PROJECT_APPS
DATABASE_ENGINE = 'sqlite3'
View
0  tests/testapp/__init__.py
No changes.
View
65 tests/testapp/models.py
@@ -0,0 +1,65 @@
+from django.db import models
+from django_fsm.db.fields import FSMField, transition
+
+
+class Application(models.Model):
+ """
+ Student application need to be approved by dept chair and dean.
+ Test workflow
+ """
+ state = FSMField(default='new')
+
+ @transition(field=state, source='new', target='draft')
+ def draft(self):
+ pass
+
+ @transition(field=state, source=['new', 'draft'], target='dept')
+ def to_approvement(self):
+ pass
+
+ @transition(field=state, source='dept', target='dean')
+ def dept_approved(self):
+ pass
+
+ @transition(field=state, source='dept', target='new')
+ def dept_rejected(self):
+ pass
+
+ @transition(field=state, source='dean', target='done')
+ def dean_approved(self):
+ pass
+
+ @transition(field=state, source='dean', target='dept')
+ def dean_rejected(self):
+ pass
+
+
+class BlogPost(models.Model):
+ """
+ Test workflow
+ """
+ state = FSMField(default='new')
+
+ @transition(field=state, source='new', target='published')
+ def publish(self):
+ pass
+
+ @transition(field=state, source='published')
+ def notify_all(self):
+ pass
+
+ @transition(field=state, source='published', target='hidden')
+ def hide(self):
+ pass
+
+ @transition(field=state, source='new', target='removed')
+ def remove(self):
+ raise Exception('No rights to delete %s' % self)
+
+ @transition(field=state, source=['published','hidden'], target='stolen')
+ def steal(self):
+ pass
+
+ @transition(field=state, source='*', target='moderated')
+ def moderate(self):
+ pass
View
16 tests/testapp/tests.py
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.assertEqual(1 + 1, 2)
View
1  tests/testapp/views.py
@@ -0,0 +1 @@
+# Create your views here.
Please sign in to comment.
Something went wrong with that request. Please try again.