如何用Python/Django实现撤销功能

16 投票
3 回答
5698 浏览
提问于 2025-04-16 08:43

我有一个Django应用,允许用户导入一个包含联系信息的CSV文件(比如会员编号、名字、姓氏等等)。

当用户导入文件时,应用会检查数据库中是否有匹配的记录,如果没有,就会插入一条新记录;如果有匹配的,就会用新数据更新已有的数据。

我想问的是:用Django或纯Python,怎么实现一个撤销功能,让用户可以撤销导入操作,并把多个记录恢复到原来的状态?

我最初的想法是创建一个像这样的表(伪代码):

Table HISTORY
   unique_id
   record_affected_id
   old_value
   new_value

然后如果用户点击“撤销”,我可以查找与他们的操作相关的唯一ID,并把所有受影响的记录恢复到旧值。

我在想是否有更简单的方法,或者有没有人有类似的经验可以分享。

3 个回答

1

你的历史记录表看起来没问题,不过你其实不需要 new_value 这个字段来实现撤销功能。没错,这就是“撤销”功能常见的实现方式(另一种方法是像 Lennart 提到的那样,在所有记录中加入版本号)。使用单独的日志表的好处是,你在平常查询的时候就不用去处理版本号了。

3

你需要有版本控制,问题不在于Python或Django,而是如何设计数据库来实现这一点。一种常见的方法是用唯一的ID来存储文档,并跟踪哪个是“当前”的版本。要撤销操作,只需将“当前”指针指回到旧的版本就可以了。就我所见,这正是你正在做的事情。

虽然这是常见的做法,但我不确定这是否是最好的方法。我从未见过其他方式,这可能意味着这是最好的,或者最好的方法并不明显。:-)

在Django中以通用的方式实现这一点可能是个难题,但如果你让你的Django应用以自定义的方式支持它,那就会简单一些。

接下来你会遇到一些有趣(其实不太有趣)的问题,比如如何在“未来”的版本中编辑内容,然后一次性发布一整套文档,像是内容的预发布一样。但希望你不需要这样做。:-)

12

看看这个django-reversion。它为Django模型提供了版本控制,可以很容易地添加到现有项目中。

它不是用“当前”指针的方法。相反,它每次保存对象时都会将其序列化,并将其存储在一个单独的Version模型中,这个模型通过通用外键指向这个对象。(关系字段默认以主键的形式序列化。)此外,它还允许以灵活的方式将Version分组到Revision中。

所以你可以这样做:

  • 当用户上传CSV文件时,像往常一样保存更改,但在执行导入的函数上添加@revision.create_on_success装饰器——这样该函数对记录所做的任何更改都会存储在一个单独的修订版下。
  • 当用户点击“撤销”时,你只需恢复最新的修订版。

这可以这样实现:

@revision.create_on_success
def import_csv(request, csv):
    # Old versions of all objects save()d here will
    # belong to single revision.

def undo_last_csv_import(request):
    # First, get latest revision saved by this user.
    # (Assuming you create revisions only when user imports a CSV
    # and do not version control other data.)
    revision = Revision.objects.filter(user=request.user)\
        .order_by('-date_created')[0]
    # And revert it, delete=True means we want to delete
    # any newly added records as well
    revision.revert(delete=True)

它依赖于你只在用户导入CSV时创建修订版。这意味着,如果你还打算对其他数据进行版本控制,那么你需要实现某种标志,以便获取最新导入所影响的记录。然后你可以通过这个标志获取记录,获取它最新保存的版本,并恢复该版本所属的整个修订版。像这样:

def undo_last_csv_import(request):
    some_record = Record.objects.by_user(request.user).from_the_last_import()[0]
    latest_saved_version_of_some_record = Version.objects.get_for_date(
        some_record,
        datetime.now(), # The latest saved Version at the moment.
        )
    # Revert all versions that belong to the same revision
    # as the version we got above.
    latest_saved_version_of_some_record.revision.revert()

这不是一个完美的解决方案,肯定还有更好的方法来使用这个应用。我建议你看看代码,以更好地理解django-reversion是如何工作的——文档写得很好,我找不到没有注释的函数。^_^d

(文档也不错,但对我来说有点误导,也就是说,他们写Version.objects.get_for_date(your_model, date),其中your_model实际上是一个模型实例。)

更新:django-reversion正在积极维护,所以不要过于依赖上面的代码,最好查看他们的wiki,了解如何在Django的管理界面之外管理版本和修订版。例如,修订评论已经得到支持,这可能会简化一些事情。

撰写回答