Django友好的有限状态机支持。
项目描述
Django友好有限状态机支持
django-fsm为django模型添加了简单的声明式状态管理。
[!IMPORTANT] Django FSM-2最初是Django FSM的分支 Django FSM.
非常感谢Mikhail Podgurskiy启动了这个了不起的项目,并持续维护了这么多年。
不幸的是,该项目已经停滞了近两年,官方宣布将没有新的发布。虽然Viewflow被提出作为一个替代方案,但过渡并不容易。
如果您只需要一个简单的Django状态机,Django FSM-2是Django FSM的继承者,包括依赖关系更新(计划中)
简介
FSM(状态机)真正有助于结构化代码,并集中管理模型的整个生命周期。
而不是在Django模型中添加一个CharField字段,并在每个地方手动管理其值,FSMFields
允许您通过装饰器一次性声明您的transitions
。这些方法可以包含副作用、权限或逻辑,以便更容易地管理生命周期。
更多介绍请见这里:https://gist.github.com/Nagyman/9502133
安装
$ pip install django-fsm-2
或者,对于最新的git版本
$ pip install -e git://github.com/django-commons/django-fsm-2.git#egg=django-fsm
用法
将FSMState字段添加到您的模型中
from django_fsm import FSMField, transition
class BlogPost(models.Model):
state = FSMField(default='new')
使用transition
装饰器来注释模型方法
@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.
"""
field
参数接受一个字符串属性名或实际的字段实例。
如果调用publish()成功而没有抛出异常,状态字段将被更改,但不会写入数据库。
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('/')
如果更改状态之前需要满足某些条件,请使用transition
的conditions
参数。conditions
必须是一个函数列表,这些函数接受一个参数,即模型实例。函数必须返回True
或False
或一个评估为True
或False
的值。如果所有函数都返回True
,则认为所有条件都已满足,并且允许发生转换。如果一个函数返回False
,则不会发生转换。这些函数不应有任何副作用。
您可以使用普通函数
def can_publish(instance):
# No publishing after 17 hours
if datetime.datetime.now().hour > 17:
return False
return True
或模型方法
def can_destroy(self):
return self.is_under_investigation()
使用条件如下
@transition(field=state, source='new', target='published', conditions=[can_publish])
def publish(self):
"""
Side effects galore
"""
@transition(field=state, source='*', target='destroyed', conditions=[can_destroy])
def destroy(self):
"""
Side effects galore
"""
您可以使用protected=True
选项创建字段实例以防止直接修改状态字段。
class BlogPost(models.Model):
state = FSMField(default='new', protected=True)
model = BlogPost()
model.state = 'invalid' # Raises AttributeError
请注意,对具有受保护FSMField的模型实例调用refresh_from_db将导致异常。
source
状态
source
参数可以接受一个状态列表、一个单独的状态或django_fsm.State
实现。
您可以使用*
作为source
以允许从任何状态切换到target
。
您可以使用+
作为source
以允许从任何状态切换到target
,但不包括target
状态。
target
状态
target
状态参数可以指向一个特定的状态或django_fsm.State
实现。
from django_fsm import FSMField, transition, RETURN_VALUE, GET_STATE
@transition(field=state,
source='*',
target=RETURN_VALUE('for_moderators', 'published'))
def publish(self, is_public=False):
return 'for_moderators' if is_public else 'published'
@transition(
field=state,
source='for_moderators',
target=GET_STATE(
lambda self, allowed: 'published' if allowed else 'rejected',
states=['published', 'rejected']))
def moderate(self, allowed):
pass
@transition(
field=state,
source='for_moderators',
target=GET_STATE(
lambda self, **kwargs: 'published' if kwargs.get("allowed", True) else 'rejected',
states=['published', 'rejected']))
def moderate(self, allowed=True):
pass
custom
属性
可以通过在transition
装饰器的custom
关键字上提供一个字典来添加自定义属性。
@transition(field=state,
source='*',
target='onhold',
custom=dict(verbose='Hold for legal reasons'))
def legal_hold(self):
"""
Side effects galore
"""
on_error
状态
如果转换方法抛出异常,您可以提供一个特定的目标状态
@transition(field=state, source='new', target='published', on_error='failed')
def publish(self):
"""
Some exception could happen here
"""
state_choices
您可以使用三个元素的state_choices
而不是传递一个包含两个元素的迭代器choices
,最后一个元素是一个模型代理类的字符串引用。
基类实例将根据状态动态更改为相应的代理类实例。即使是查询集的结果,也会得到代理类实例,即使查询集是在基类上执行的。
查看测试用例以获取示例用法。或者阅读有关实现内部机制的说明。
权限
通常,每个模型状态转换都会附加权限。使用 django-fsm
中的 transition
装饰器上的 permission
关键字来处理这种情况。permission
接受一个权限字符串或可调用对象,该对象期望 instance
和 user
参数,如果用户可以执行转换则返回 True。
@transition(field=state, source='*', target='published',
permission=lambda instance, user: not user.has_perm('myapp.can_make_mistakes'))
def publish(self):
pass
@transition(field=state, source='*', target='removed',
permission='myapp.can_remove_post')
def remove(self):
pass
您可以使用 has_transition_permission
方法来检查权限
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('/')
模型方法
get_all_FIELD_transitions
列出所有声明的转换
get_available_FIELD_transitions
返回当前状态中可用的所有转换数据
get_available_user_FIELD_transitions
列出为提供的用户提供当前状态中可用的所有转换数据
外键约束支持
如果您在数据库表中存储状态,则可以使用 FSMKeyField 来确保外键数据库完整性。
在您的模型中
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
在您的 fixtures/initial_data.json 中
[
{
"pk": "new",
"model": "myapp.dbstate",
"fields": {
"label": "_NEW_"
}
},
{
"pk": "published",
"model": "myapp.dbstate",
"fields": {
"label": "_PUBLISHED_"
}
}
]
注意:@transition 装饰器中的 source 和 target 参数使用 DBState 模型的 pk 值作为名称,即使使用字段“真实”名称,也不加 _id 后缀,作为字段参数。
整数字段支持
您还可以使用 FSMIntegerField
。当您想使用枚举样式的常量时,这很有用。
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
信号
django_fsm.signals.pre_transition
和 django_fsm.signals.post_transition
分别在允许的转换前后被调用。在无效转换上不会调用任何信号。
这些信号发送的参数
sender 模型类。
instance 正在处理的实际实例。
name 转换名称。
source 源模型状态。
target 目标模型状态。
乐观锁
django-fsm
提供了乐观锁混合,以避免并发模型状态更改。如果模型状态在数据库中被更改,则在模型.save() 上会引发 django_fsm.ConcurrentTransition
异常。
from django_fsm import FSMField, ConcurrentTransitionMixin
class BlogPost(ConcurrentTransitionMixin, models.Model):
state = FSMField(default='new')
为了确保由并发执行的转换引起的竞态条件得到保护,请确保
- 您的转换除了数据库中的更改外没有其他副作用,
- 您始终在
django.db.transaction.atomic()
块中运行对象的 save() 方法。
遵循这些建议,您就可以依靠 ConcurrentTransitionMixin 来导致所有在不一致(不同步)状态下执行的改变回滚,从而实际上抵消了它们的影响。
绘制转换
呈现您模型状态转换的图形概览。
您需要安装 pip install "graphviz>=0.4"
库并将 django_fsm
添加到您的 INSTALLED_APPS
。
INSTALLED_APPS = (
...
'django_fsm',
...
)
# 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
扩展
您还可以查看包含混合和模板标记以将 django-fsm 状态转换集成到 django admin 的 django-fsm-admin 项目。
https://github.com/gadventures/django-fsm-admin
可以通过 django-fsm-log 包实现转换日志支持
项目详情
下载文件
下载适合您平台的应用程序。如果您不确定选择哪个,请了解有关 安装包 的更多信息。