[Python-modules-commits] [django-fsm] 01/02: Imported Upstream version 2.2.1
Michael Fladischer
fladi at moszumanska.debian.org
Wed Jun 24 07:53:38 UTC 2015
This is an automated email from the git hooks/post-receive script.
fladi pushed a commit to branch master
in repository django-fsm.
commit 5f5423d4d9b8b666bf3fb139c335ed6ad993def1
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date: Tue Apr 28 15:46:16 2015 +0200
Imported Upstream version 2.2.1
---
.gitignore | 5 +
.pylintrc | 63 +++
.travis.yml | 9 +
LICENSE | 19 +
README.md | 394 ++++++++++++++++
django_fsm/__init__.py | 522 +++++++++++++++++++++
django_fsm/management/__init__.py | 0
django_fsm/management/commands/__init__.py | 0
.../management/commands/graph_transitions.py | 117 +++++
django_fsm/models.py | 4 +
django_fsm/signals.py | 5 +
django_fsm/tests/__init__.py | 0
django_fsm/tests/test_basic_transitions.py | 141 ++++++
django_fsm/tests/test_conditions.py | 50 ++
django_fsm/tests/test_inheritance.py | 52 ++
django_fsm/tests/test_integer_field.py | 39 ++
django_fsm/tests/test_key_field.py | 149 ++++++
django_fsm/tests/test_protected_field.py | 30 ++
requirements.txt | 1 +
setup.py | 33 ++
tests/__init__.py | 0
tests/manage.py | 13 +
tests/settings.py | 15 +
tests/testapp/__init__.py | 0
tests/testapp/fixtures/initial_data.json | 27 ++
tests/testapp/models.py | 117 +++++
tests/testapp/tests/__init__.py | 0
tests/testapp/tests/test_custom_data.py | 40 ++
tests/testapp/tests/test_exception_transitions.py | 40 ++
tests/testapp/tests/test_lock_mixin.py | 84 ++++
tests/testapp/tests/test_permissions.py | 33 ++
tests/testapp/tests/test_state_transitions.py | 67 +++
tests/testapp/views.py | 1 +
tox.ini | 34 ++
34 files changed, 2104 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f94ac6f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.pyc
+dist
+django_fsm.egg-info
+reports
+.tox
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..6d0c5f9
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,63 @@
+[MASTER]
+persistent=yes
+
+[MESSAGES CONTROL]
+# C0111 = Missing docstring
+# I0011 = # Warning locally suppressed using disable-msg
+# I0012 = # Warning locally suppressed using disable-msg
+disable=I0011,I0012
+
+[REPORTS]
+output-format=parseable
+include-ids=no
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+[VARIABLES]
+init-import=no
+
+
+[SIMILARITIES]
+min-similarity-lines=4
+ignore-comments=yes
+ignore-docstrings=yes
+
+
+[MISCELLANEOUS]
+notes=FIXME,XXX,TODO
+
+
+[FORMAT]
+max-line-length=160
+max-module-lines=500
+indent-string=' '
+
+
+[DESIGN]
+max-args=5
+max-locals=15
+max-returns=6
+max-branchs=12
+max-statements=50
+max-parents=7
+max-attributes=7
+min-public-methods=0
+max-public-methods=20
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..53806c6
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+language: python
+env:
+ - $TOX_ENV=py26
+ - $TOX_ENV=py27
+ - $TOX_ENV=py33
+install:
+ - pip install tox
+script:
+ - tox -e $TOX_ENV
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..edca474
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+copyright (c) 2010 Mikhail Podgurskiy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4ac8234
--- /dev/null
+++ b/README.md
@@ -0,0 +1,394 @@
+Django friendly finite state machine support
+============================================
+[![Build Status](https://travis-ci.org/kmmbvnr/django-fsm.svg?branch=master)](https://travis-ci.org/kmmbvnr/django-fsm)
+
+django-fsm adds declarative states management for django models.
+
+Instead of adding some state field to a django model, and managing its
+values by hand, you could use FSMState field and mark model methods
+with the `transition` decorator. Your method could contain the side-effects
+of the state change.
+
+Nice introduction is available here: https://gist.github.com/Nagyman/9502133
+
+You may also take a look at django-fsm-admin project
+containing a mixin and template tags to integrate django-fsm
+state transitions into the django admin.
+
+https://github.com/gadventures/django-fsm-admin
+
+Transition logging support could be achived with help of django-fsm-log package
+
+https://github.com/gizmag/django-fsm-log
+
+FSM really helps to structure the code, especially when a new
+developer comes to the project. FSM is most effective when you use it
+for some sequential steps.
+
+If you need parallel task execution, view and background task code reuse
+over different flows - check my new project django-viewflow
+
+https://github.com/kmmbvnr/django-viewflow
+
+Installation
+------------
+```bash
+$ pip install django-fsm
+```
+Or, for the latest git version
+```bash
+$ pip install -e git://github.com/kmmbvnr/django-fsm.git#egg=django-fsm
+```
+
+The library has full Python 3 support
+
+
+Usage
+-----
+
+Add FSMState field to your model
+```python
+from django_fsm import FSMField, transition
+
+class BlogPost(models.Model):
+ state = FSMField(default='new')
+```
+
+Use the `transition` decorator to annotate model methods
+```python
+ at transition(field=state, source='new', target='published')
+def publish(self):
+ """
+ This function may contain side-effects,
+ like updating caches, notifying users, etc.
+ The return value will be discarded.
+ """
+```
+
+`source` parameter accepts a list of states, or an individual state.
+You can use `*` for source, to allow switching to `target` from any state.
+
+If calling publish() succeeds without raising an exception, the state field
+will be changed, but not written to the database.
+```python
+from django_fsm import can_proceed
+
+def publish_view(request, post_id):
+ post = get_object__or_404(BlogPost, pk=post_id)
+ if not can_proceed(post.publish):
+ raise PermissionDenied
+
+ post.publish()
+ post.save()
+ return redirect('/')
+```
+
+If some conditions are required to be met before changing the state, use the
+`conditions` argument to `transition`. `conditions` must be a list of functions
+taking one argument, the model instance. The function must return either
+`True` or `False` or a value that evaluates to `True` or `False`. If all functions
+return `True`, all conditions are considered to be met and the transition
+is allowed to happen. If one of the functions returns `False`, the transition
+will not happen. These functions should not have any side effects.
+
+You can use ordinary functions
+```python
+def can_publish(instance):
+ # No publishing after 17 hours
+ if datetime.datetime.now().hour > 17:
+ return False
+ return True
+```
+
+Or model methods
+```python
+def can_destroy(self):
+ return self.is_under_investigation()
+```
+
+Use the conditions like this:
+```python
+ at transition(field=state, source='new', target='published', conditions=[can_publish])
+ def publish(self):
+ """
+ Side effects galore
+ """
+
+ at transition(field=state, source='*', target='destroyed', conditions=[can_destroy])
+ def destroy(self):
+ """
+ Side effects galore
+ """
+```
+
+You could instantiate a field with protected=True option, that prevents direct state field modification
+```python
+class BlogPost(models.Model):
+ state = FSMField(default='new', protected=True)
+
+model = BlogPost()
+model.state = 'invalid' # Raises AttributeError
+```
+
+
+### `custom` properties
+Custom properties can be added by providing a dictionary to the `custom` keyword on the `transition` decorator.
+```python
+ at transition(field=state,
+ source='*',
+ target='onhold',
+ custom=dict(verbose='Hold for legal reasons'), transition_type='manual')
+def legal_hold(self):
+ """
+ Side effects galore
+ """
+```
+
+### `on_error` state
+
+In case of transition method would raise exception, you can provide specific target state
+
+```python
+ at transition(field=state, source='new', target='published', on_error='failed')
+def publish(self):
+ """
+ Some exceptio could happends here
+ """
+```
+
+### `state_choices`
+Instead of passing two elements list `choices` you could use three elements `state_choices`,
+the last element states for string reference to model proxy class.
+
+Base class instance would be dynamically changed to corresponding Proxy class instance, depending on the state.
+Even for queryset results, you will get Proxy class instances, even if QuerySet executed on base class.
+
+Check the [test case](https://github.com/kmmbvnr/django-fsm/blob/master/tests/testapp/tests/test_state_transitions.py)
+for example usage. Or read about [implementation internals](http://schinckel.net/2013/06/13/django-proxy-model-state-machine/)
+
+### Permissions
+It is common to have permissions attached to each model transition. `django-fsm` handles this with
+`permission` keyword on the `transition` decorator. `permission` accepts a permission string, or
+callable that expects `user` argument and returns True if user can perform the transition
+
+```python
+ at transition(field=state, source='*', target='publish',
+ permission=lambda user: not user.has_perm('myapp.can_make_mistakes'))
+def publish(self):
+ pass
+
+ at transition(field=state, source='*', target='publish',
+ permission='myapp.can_remove_post')
+def remove(self):
+ pass
+```
+
+You can check permission with `has_transition_permission` method
+
+``` python
+from django_fsm import has_transition_perm
+def publish_view(request, post_id):
+ post = get_object_or_404(BlogPost, pk=post_id)
+ if not has_transition_perm(post.publish, request.user):
+ raise PermissionDenied
+
+ post.publish()
+ post.save()
+ return redirect('/')
+```
+
+### Model methods
+
+`get_all_FIELD_transitions`
+Enumerates all declared transitions
+
+`get_available_FIELD_transitions`
+Returns all transitions data available in current state
+
+`get_available_user_FIELD_transitions`
+Enumerates all transitions data available in current state for provided user
+
+### Foreign Key constraints support
+
+If you store the states in the db table you could use FSMKeyField to
+ensure Foreign Key database integrity.
+
+In your model :
+```python
+class DbState(models.Model):
+ id = models.CharField(primary_key=True, max_length=50)
+ label = models.CharField(max_length=255)
+
+ def __unicode__(self):
+ return self.label
+
+
+class BlogPost(models.Model):
+ state = FSMKeyField(DbState, default='new')
+
+ @transition(field=state, source='new', target='published')
+ def publish(self):
+ pass
+```
+
+In your fixtures/initial_data.json :
+```json
+[
+ {
+ "pk": "new",
+ "model": "myapp.dbstate",
+ "fields": {
+ "label": "_NEW_"
+ }
+ },
+ {
+ "pk": "published",
+ "model": "myapp.dbstate",
+ "fields": {
+ "label": "_PUBLISHED_"
+ }
+ }
+]
+```
+
+Note : source and target parameters in @transition decorator use pk values of DBState model
+as names, even if field "real" name is used, without _id postfix, as field parameter.
+
+
+### Integer Field support
+
+You can also use `FSMIntegerField`. This is handy when you want to use enum style constants.
+```python
+class BlogPostStateEnum(object):
+ NEW = 10
+ PUBLISHED = 20
+ HIDDEN = 30
+
+class BlogPostWithIntegerField(models.Model):
+ state = FSMIntegerField(default=BlogPostStateEnum.NEW)
+
+ @transition(field=state, source=BlogPostStateEnum.NEW, target=BlogPostStateEnum.PUBLISHED)
+ def publish(self):
+ pass
+```
+
+### Signals
+
+`django_fsm.signals.pre_transition` and `django_fsm.signals.post_transition` are called before
+and after allowed transition. No signals on invalid transition are called.
+
+Arguments sent with these signals:
+
+**sender**
+ The model class.
+
+**instance**
+ The actual instance being procceed
+
+**name**
+ Transition name
+
+**source**
+ Source model state
+
+**target**
+ Target model state
+
+## Optimistic locking
+
+`django-fsm` provides optimistic locking mixin, to avoid concurent model state changes.
+If model state was changed in database `django_fsm.ConcurrentTransition` exception would be raised
+on model.save()
+
+```python
+from django_fsm import FMSField, ConcurrentTransitionMixin
+
+class BlogPost(ConcurrentTransitionMixin, models.Model):
+ state = FSMField(default='new')
+```
+
+For guaranteed protection against race conditions caused by concurrently executed transitions, make sure:
+* Your transitions do not have any side effects except for changes in the database,
+* You always run the save() method on the object within `django.db.transaction.atomic()` block.
+
+Following these recommendations, you can rely on ConcurrentTransitionMixin to cause a rollback of all the changes
+that have been executed in an inconsistent (out of sync) state, thus practically negating their effect.
+
+## Drawing transitions
+
+Renders a graphical overview of your models states transitions
+
+You need `pip install graphviz>=0.4` library
+
+```bash
+# Create a dot file
+$ ./manage.py graph_transitions > transitions.dot
+
+# Create a PNG image file only for specific model
+$ ./manage.py graph_transitions -o blog_transitions.png myapp.Blog
+```
+
+Changelog
+---------
+
+<img src="https://f.cloud.github.com/assets/41479/2227946/a9e77760-9ad0-11e3-804f-301d075470fe.png" alt="django-fsm" width="100px"/>
+
+### django-fsm 2.2.1 2015-04-27
+* Improved exception message for unmet transition conditions.
+* Don't send post transiton signal in case of no state chnages on exception
+* Allow empty string as correct state value
+* Imporved graphviz fsm visualisation
+* Clean django 1.8 warnings
+
+
+### django-fsm 2.2.0 2014-09-03
+* Support for [class substitution](http://schinckel.net/2013/06/13/django-proxy-model-state-machine/) to proxy classes depending on the state
+* Added ConcurrentTransitionMixin with optimistic locking support
+* Default db_index=True for FSMIntegerField removed
+* Graph transition code migrated to new graphviz library with python 3 support
+* Ability to change state on transition exception
+
+### django-fsm 2.1.0 2014-05-15
+* Support for attaching permission checks on model transitions
+
+### django-fsm 2.0.0 2014-03-15
+* Backward incompatible release
+* All public code import moved directly to django_fsm package
+* Correct support for several @transitions decorator with different source states and conditions on same method
+* save parameter from transition decorator removed
+* get_available_FIELD_transitions return Transition data object instead of tuple
+* Models got get_available_FIELD_transitions, even if field specified as string reference
+* New get_all_FIELD_transitions method contributed to class
+
+### django-fsm 1.6.0 2014-03-15
+* FSMIntegerField and FSMKeyField support
+
+### django-fsm 1.5.1 2014-01-04
+
+* Ad-hoc support for state fields from proxy and inherited models
+
+### django-fsm 1.5.0 2013-09-17
+
+* Python 3 compatibility
+
+### django-fsm 1.4.0 2011-12-21
+
+* Add graph_transition command for drawing state transition picture
+
+### django-fsm 1.3.0 2011-07-28
+
+* Add direct field modification protection
+
+### django-fsm 1.2.0 2011-03-23
+
+* Add pre_transition and post_transition signals
+
+### django-fsm 1.1.0 2011-02-22
+
+* Add support for transition conditions
+* Allow multiple FSMField in one model
+* Contribute get_available_FIELD_transitions for model class
+
+### django-fsm 1.0.0 2010-10-12
+
+* Initial public release
diff --git a/django_fsm/__init__.py b/django_fsm/__init__.py
new file mode 100644
index 0000000..373d795
--- /dev/null
+++ b/django_fsm/__init__.py
@@ -0,0 +1,522 @@
+# -*- coding: utf-8 -*-
+"""
+State tracking functionality for django models
+"""
+import inspect
+from functools import wraps
+import sys
+
+from django.db import models
+try:
+ from django.apps import apps as django_apps
+ def get_model(app_label, model_name):
+ app = django_apps.get_app_config(app_label)
+ return app.get_model(model_name)
+except ImportError:
+ from django.db.models.loading import get_model
+from django.db.models.signals import class_prepared
+from django.utils.functional import curry
+from django_fsm.signals import pre_transition, post_transition
+
+
+__all__ = ['TransitionNotAllowed', 'ConcurrentTransition',
+ 'FSMFieldMixin', 'FSMField', 'FSMIntegerField',
+ 'FSMKeyField', 'ConcurrentTransitionMixin', 'transition',
+ 'can_proceed', 'has_transition_perm']
+
+if sys.version_info[:2] == (2, 6):
+ # Backport of Python 2.7 inspect.getmembers,
+ # since Python 2.6 ships buggy implementation
+ def __getmembers(object, predicate=None):
+ """Return all members of an object as (name, value) pairs sorted by name.
+ Optionally, only return members that satisfy a given predicate."""
+ results = []
+ for key in dir(object):
+ try:
+ value = getattr(object, key)
+ except AttributeError:
+ continue
+ if not predicate or predicate(value):
+ results.append((key, value))
+ results.sort()
+ return results
+ inspect.getmembers = __getmembers
+
+# South support; see http://south.aeracode.org/docs/tutorial/part4.html#simple-inheritance
+try:
+ from south.modelsinspector import add_introspection_rules
+except ImportError:
+ pass
+else:
+ add_introspection_rules([], [r"^django_fsm\.FSMField"])
+ add_introspection_rules([], [r"^django_fsm\.FSMIntegerField"])
+ add_introspection_rules([], [r"^django_fsm\.FSMKeyField"])
+
+
+class TransitionNotAllowed(Exception):
+ """Raised when a transition is not allowed"""
+
+
+class ConcurrentTransition(Exception):
+ """
+ Raised when the transition cannot be executed because the
+ object has become stale (state has been changed since it
+ was fetched from the database).
+ """
+
+
+class Transition(object):
+ def __init__(self, method, source, target, on_error, conditions, permission, custom):
+ self.method = method
+ self.source = source
+ self.target = target
+ self.on_error = on_error
+ self.conditions = conditions
+ self.permission = permission
+ self.custom = custom
+
+ @property
+ def name(self):
+ return self.method.__name__
+
+ def has_perm(self, user):
+ if not self.permission:
+ return True
+ elif callable(self.permission) and self.permission(user):
+ return True
+ elif user.has_perm(self.permission):
+ return True
+ else:
+ return False
+
+
+def get_available_FIELD_transitions(instance, field):
+ """
+ List of transitions available in current model state
+ with all conditions met
+ """
+ curr_state = field.get_state(instance)
+ transitions = field.transitions[instance.__class__]
+
+ for name, transition in transitions.items():
+ meta = transition._django_fsm
+
+ for state in [curr_state, '*']:
+ if state in meta.transitions:
+ transition = meta.transitions[state]
+ if all(map(lambda condition: condition(instance), transition.conditions)):
+ yield transition
+
+
+def get_all_FIELD_transitions(instance, field):
+ """
+ List of all transitions available in current model state
+ """
+ return field.get_all_transitions(instance.__class__)
+
+
+def get_available_user_FIELD_transitions(instance, user, field):
+ """
+ List of transitions available in current model state
+ with all conditions met and user have rights on it
+ """
+ for transition in get_available_FIELD_transitions(instance, field):
+ if transition.has_perm(user):
+ yield transition
+
+
+class FSMMeta(object):
+ """
+ Models methods transitions meta information
+ """
+ def __init__(self, field, method):
+ self.field = field
+ self.transitions = {} # source -> Transition
+
+ def get_transition(self, source):
+ transition = self.transitions.get(source, None)
+ if transition is None:
+ transition = self.transitions.get('*', None)
+ return transition
+
+ def add_transition(self, method, source, target, on_error=None, conditions=[], permission=None, custom={}):
+ if source in self.transitions:
+ raise AssertionError('Duplicate transition for {0} state'.format(source))
+
+ self.transitions[source] = Transition(
+ method=method,
+ source=source,
+ target=target,
+ on_error=on_error,
+ conditions=conditions,
+ permission=permission,
+ custom=custom)
+
+ def has_transition(self, state):
+ """
+ Lookup if any transition exists from current model state using current method
+ """
+ return state in self.transitions or '*' in self.transitions
+
+ def conditions_met(self, instance, state):
+ """
+ Check if all conditions have been met
+ """
+ transition = self.get_transition(state)
+
+ if transition is None:
+ return False
+ elif transition.conditions is None:
+ return True
+ else:
+ return all(map(lambda condition: condition(instance), transition.conditions))
+
+ def has_transition_perm(self, instance, state, user):
+ transition = self.get_transition(state)
+
+ if not transition:
+ return False
+ else:
+ return transition.has_perm(user)
+
+ def next_state(self, current_state):
+ transition = self.get_transition(current_state)
+
+ if transition is None:
+ raise TransitionNotAllowed('No transition from {0}'.format(current_state))
+
+ return transition.target
+
+ def exception_state(self, current_state):
+ transition = self.get_transition(current_state)
+
+ if transition is None:
+ raise TransitionNotAllowed('No transition from {0}'.format(current_state))
+
+ return transition.on_error
+
+
+class FSMFieldDescriptor(object):
+ def __init__(self, field):
+ self.field = field
+
+ def __get__(self, instance, type=None):
+ if instance is None:
+ raise AttributeError('Can only be accessed via an instance.')
+ return self.field.get_state(instance)
+
+ def __set__(self, instance, value):
+ if self.field.protected and self.field.name in instance.__dict__:
+ raise AttributeError('Direct {0} modification is not allowed'.format(self.field.name))
+
+ # Update state
+ self.field.set_proxy(instance, value)
+ self.field.set_state(instance, value)
+
+
+class FSMFieldMixin(object):
+ descriptor_class = FSMFieldDescriptor
+
+ def __init__(self, *args, **kwargs):
+ self.protected = kwargs.pop('protected', False)
+ self.transitions = {} # cls -> (transitions name -> method)
+ self.state_proxy = {} # state -> ProxyClsRef
+
+ state_choices = kwargs.pop('state_choices', None)
+ choices = kwargs.get('choices', None)
+ if state_choices is not None and choices is not None:
+ raise ValueError('Use one of choices or state_choces value')
+
+ if state_choices is not None:
+ choices = []
+ for state, title, proxy_cls_ref in state_choices:
+ choices.append((state, title))
+ self.state_proxy[state] = proxy_cls_ref
+ kwargs['choices'] = choices
+
+ super(FSMFieldMixin, self).__init__(*args, **kwargs)
+
+ def deconstruct(self):
+ name, path, args, kwargs = super(FSMFieldMixin, self).deconstruct()
+ if self.protected:
+ kwargs['protected'] = self.protected
+ return name, path, args, kwargs
+
+ def get_state(self, instance):
+ return instance.__dict__[self.name]
+
+ def set_state(self, instance, state):
+ instance.__dict__[self.name] = state
+
+ def set_proxy(self, instance, state):
+ """
+ Change class
+ """
+ if state in self.state_proxy:
+ state_proxy = self.state_proxy[state]
+
+ try:
+ app_label, model_name = state_proxy.split(".")
+ except ValueError:
+ # If we can't split, assume a model in current app
+ app_label = instance._meta.app_label
+ model_name = state_proxy
+
+ model = get_model(app_label, model_name)
+ if model is None:
+ raise ValueError('No model found {0}'.format(state_proxy))
+
+ instance.__class__ = model
+
+ def change_state(self, instance, method, *args, **kwargs):
+ meta = method._django_fsm
+ method_name = method.__name__
+ current_state = self.get_state(instance)
+
+ if not meta.has_transition(current_state):
+ raise TransitionNotAllowed(
+ "Can't switch from state '{0}' using method '{1}'".format(current_state, method_name))
+ if not meta.conditions_met(instance, current_state):
+ raise TransitionNotAllowed(
+ "Transition conditions have not been met for method '{0}'".format(method_name))
+
+ next_state = meta.next_state(current_state)
+
+ signal_kwargs = {
+ 'sender': instance.__class__,
+ 'instance': instance,
+ 'name': method_name,
+ 'source': current_state,
+ 'target': next_state
+ }
+
+ pre_transition.send(**signal_kwargs)
+
+ try:
+ result = method(instance, *args, **kwargs)
+ if next_state is not None:
+ self.set_proxy(instance, next_state)
+ self.set_state(instance, next_state)
+ except Exception as exc:
+ exception_state = meta.exception_state(current_state)
+ if exception_state:
+ self.set_proxy(instance, exception_state)
+ self.set_state(instance, exception_state)
+ signal_kwargs['target'] = exception_state
+ signal_kwargs['exception'] = exc
+ post_transition.send(**signal_kwargs)
+ raise
+ else:
+ post_transition.send(**signal_kwargs)
+
+ return result
+
+ def get_all_transitions(self, instance_cls):
+ """
+ Returns [(source, target, name, method)] for all field transitions
+ """
+ transitions = self.transitions[instance_cls]
+
+ for name, transition in transitions.items():
+ meta = transition._django_fsm
+
+ for transition in meta.transitions.values():
+ yield transition
+
+ def contribute_to_class(self, cls, name, virtual_only=False):
+ self.base_cls = cls
+
+ super(FSMFieldMixin, self).contribute_to_class(cls, name, virtual_only=virtual_only)
+ setattr(cls, self.name, self.descriptor_class(self))
+ setattr(cls, 'get_all_{0}_transitions'.format(self.name),
+ curry(get_all_FIELD_transitions, field=self))
+ setattr(cls, 'get_available_{0}_transitions'.format(self.name),
+ curry(get_available_FIELD_transitions, field=self))
+ setattr(cls, 'get_available_user_{0}_transitions'.format(self.name),
+ curry(get_available_user_FIELD_transitions, field=self))
+
+ class_prepared.connect(self._collect_transitions)
+
+ def _collect_transitions(self, *args, **kwargs):
+ sender = kwargs['sender']
+
+ if not issubclass(sender, self.base_cls):
+ return
+
+ def is_field_transition_method(attr):
+ return (inspect.ismethod(attr) or inspect.isfunction(attr)) \
+ and hasattr(attr, '_django_fsm') \
+ and attr._django_fsm.field in [self, self.name]
+
+ sender_transitions = {}
+ transitions = inspect.getmembers(sender, predicate=is_field_transition_method)
+ for method_name, method in transitions:
+ method._django_fsm.field = self
+ sender_transitions[method_name] = method
+
+ self.transitions[sender] = sender_transitions
+
+
+class FSMField(FSMFieldMixin, models.CharField):
+ """
+ State Machine support for Django model as CharField
+ """
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('max_length', 50)
+ super(FSMField, self).__init__(*args, **kwargs)
+
+
+class FSMIntegerField(FSMFieldMixin, models.IntegerField):
+ """
+ Same as FSMField, but stores the state value in an IntegerField.
+ """
+ pass
+
+
+class FSMKeyField(FSMFieldMixin, models.ForeignKey):
+ """
+ State Machine support for Django model
+ """
+ def get_state(self, instance):
+ return instance.__dict__[self.attname]
+
+ def set_state(self, instance, state):
+ instance.__dict__[self.attname] = self.to_python(state)
+
+
+class ConcurrentTransitionMixin(object):
+ """
+ Protects a Model from undesirable effects caused by concurrently executed transitions,
+ e.g. running the same transition multiple times at the same time, or running different
+ transitions with the same SOURCE state at the same time.
+
+ This behavior is achieved using an idea based on optimistic locking. No additional
+ version field is required though; only the state field(s) is/are used for the tracking.
+ This scheme is not that strict as true *optimistic locking* mechanism, it is however
+ more lightweight - leveraging the specifics of FSM models.
+
+ Instance of a model based on this Mixin will be prevented from saving into DB if any
+ of its state fields (instances of FSMFieldMixin) has been changed since the object
+ was fetched from the database. *ConcurrentTransition* exception will be raised in such
+ cases.
+
+ For guaranteed protection against such race conditions, make sure:
+ * Your transitions do not have any side effects except for changes in the database,
+ * You always run the save() method on the object within django.db.transaction.atomic()
+ block.
+
+ Following these recommendations, you can rely on ConcurrentTransitionMixin to cause
+ a rollback of all the changes that have been executed in an inconsistent (out of sync)
+ state, thus practically negating their effect.
+ """
+ def __init__(self, *args, **kwargs):
+ super(ConcurrentTransitionMixin, self).__init__(*args, **kwargs)
+ self._update_initial_state()
+
+ @property
+ def state_fields(self):
+ return filter(
+ lambda field: isinstance(field, FSMFieldMixin),
+ self._meta.fields
+ )
+
+ def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):
+ # _do_update is called once for each model class in the inheritance hierarchy.
+ # We can only filter the base_qs on state fields (can be more than one!) present in this particular model.
+
+ # Select state fields to filter on
+ filter_on = filter(lambda field: field.model == base_qs.model, self.state_fields)
+
+ # state filter will be used to narrow down the standard filter checking only PK
+ state_filter = dict((field.attname, self.__initial_states[field.attname]) for field in filter_on)
+
... 1333 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-fsm.git
More information about the Python-modules-commits
mailing list