跳转到主要内容

Django友好的有限状态机支持。

项目描述

Django友好有限状态机支持

CI tests codecov Documentation MIT License

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('/')

如果更改状态之前需要满足某些条件,请使用transitionconditions参数。conditions必须是一个函数列表,这些函数接受一个参数,即模型实例。函数必须返回TrueFalse或一个评估为TrueFalse的值。如果所有函数都返回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 接受一个权限字符串或可调用对象,该对象期望 instanceuser 参数,如果用户可以执行转换则返回 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_transitiondjango_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 包实现转换日志支持

https://github.com/gizmag/django-fsm-log

项目详情


下载文件

下载适合您平台的应用程序。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源分布

django_fsm_2-4.0.0.tar.gz (20.1 kB 查看哈希值)

上传时间

构建分布

django_fsm_2-4.0.0-py3-none-any.whl (21.8 kB 查看哈希值)

上传时间 Python 3

支持者

AWSAWS云计算和安全赞助商DatadogDatadog监控FastlyFastlyCDNGoogleGoogle下载分析MicrosoftMicrosoftPSF赞助商PingdomPingdom监控SentrySentry错误日志StatusPageStatusPage状态页面