[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