简体   繁体   English

如何使用Python / Django实现“撤消”功能

[英]How to implement an “undo” feature using Python/Django

I have a Django application where I allow a user to import a CSV file with contact data (membership #, first name, last name, etc). 我有一个Django应用程序,我允许用户导入带有联系人数据的CSV文件(成员资格#,名字,姓氏等)。

When they import the file, the application checks the database for a matching record and either: 1) inserts a new record if no match exists, or 2) updates the existing data with the new data. 当他们导入文件时,应用程序会检查数据库是否有匹配的记录,并且:1)如果不存在匹配则插入新记录,或者2)使用新数据更新现有数据。

My question is: what is the best way to implement an undo feature, using Django or straight Python, so that a user can undo the import operation and revert multiple records back to their original state? 我的问题是:使用Django或直接Python实现撤销功能的最佳方法是什么,以便用户可以撤消导入操作并将多个记录恢复到原始状态?

My initial thoughts are to create a table like this (pseudo code): 我最初的想法是创建一个这样的表(伪代码):

Table HISTORY
   unique_id
   record_affected_id
   old_value
   new_value

Then if the user clicks "Undo" I can look up the unique_id that's associated with their transaction and set every record affected by that transaction to the old_value. 然后,如果用户单击“撤消”,我可以查找与其事务关联的unique_id,并将受该事务影响的每条记录设置为old_value。

I'm wondering if there's a simpler way to do this that I'm missing, or if anyone has experience with something like this. 我想知道是否有一种更简单的方法可以做到这一点,我错过了,或者如果有人有这样的经历。

Take a look at django-reversion . 看看django-reversion It provides version control for Django models. 它为Django模型提供版本控制。 Can be easily added to existing project. 可以轻松添加到现有项目中。

It doesn't employ "current" pointer approach. 它不采用“当前”指针方法。 Instead, it serializes object each time it's being saved and stores it in a separate Version model with generic foreign key pointing to this object. 相反,它在每次保存对象时将对象序列化,并将其存储在单独的Version模型中,并使用指向此对象的通用外键。 (Relationship fields are serialized as primary keys by default.) Also, it allows to group Version s into Revision s in a flexible way. (关系字段被序列化为默认主键。)另外,它允许组Version s转换Revision S IN的灵活方式。

So you can do something like that: 所以你可以这样做:

  • When user uploads CSV, just save changes as usual, but add @revision.create_on_success decorator to the function which does the import—so that any changes to records made by that function will be stored under a single revision. 当用户上传CSV时,只需像往常一样保存更改,但将@revision.create_on_success装饰器添加到执行导入的功能中,以便该功能对记录所做的任何更改都将存储在单个修订版本中。
  • When user hits "Undo", you just revert the latest revision. 当用户点击“撤消”时,您只需还原最新版本。

Here's how it could be done:: 这是怎么做的::

@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)

It relies on the fact that you create revisions only when user imports CSVs. 它依赖于仅在用户导入CSV时才创建修订的事实。 That means, if you plan to also version control other data, then you'll need to implement some kind of a flag by which you can get records affected by the latest import. 这意味着,如果您还打算对其他数据进行版本控制,那么您需要实现某种标志,通过该标志可以获取受最新导入影响的记录。 Then you can get a record by this flag, get it latest saved version, and revert the whole revision that version belongs to. 然后,您可以通过此标志获取记录,获取最新保存的版本,并还原该版本所属的整个修订版。 Like this:: 像这样::

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

It's not a beautiful solution, there most certainly are ways to do it better with this app. 这不是一个漂亮的解决方案,大多数肯定是用这个应用程序做得更好的方法。 I recommend to take a look at the code to understand better how does django-reversion work—very well documented, couldn't find a function without a docstring. 我建议看一下代码,以便更好地理解django-reversion如何工作 - 记录得很好,找不到没有docstring的函数。 ^_^d ^ _ ^ d

(Documentation is also good, but turned out to be a bit misleading for me, ie they write Version.objects.get_for_date(your_model, date) , where your_model is actually a model instance.) (文档也很好,但结果对我来说有点误导,即他们写了Version.objects.get_for_date(your_model, date) ,其中your_model实际上是一个模型实例。)

Update: django-reversion is actively maintained, so don't rely on the code above much, and better check their wiki on how to manage versions & revisions outside django's admin. 更新: django-reversion是主动维护的,所以不要依赖上面的代码,并且更好地检查他们的wiki如何管理django管理员之外的版本和修订版。 For instance, revision comments are already supported, that may simplify things a bit. 例如,已经支持修订注释,这可能会简化一些事情。

You need to have version control, and the issue there is not Python or Django, but rather how to design the database to do this. 你需要有版本控制,问题不是Python或Django,而是如何设计数据库来做到这一点。 One common way is to store documents with unique IDs and keep track of which is the "current". 一种常见的方法是存储具有唯一ID的文档并跟踪哪个是“当前”。 Undo is then just matter of putting the "current" pointer back to an older revision. 然后撤销只是将“当前”指针放回旧版本。 This is, as far as I can see, what you are doing. 据我所知,这是你在做什么。

Although this is the common way of doing it, I don't know if it's the best. 虽然这是常见的做法,但我不知道它是否是最好的。 I've never seen any other way, which might mean it's the best, or that the best way is unobvious. 我从未见过任何其他方式,这可能意味着它是最好的,或者最好的方式是不明显的。 :-) :-)

Doing this in a generic way in Django is probably A Hard Problem, but will be easier if you make your Django app support it in a custom way. 在Django中以通用方式执行此操作可能是一个难题,但如果您使Django应用程序以自定义方式支持它会更容易。

Then you get into Fun (not) Issues, like how to edit things in a "future" revision and then publish a whole set of documents at once, in a staging kind of way of content. 然后你会进入Fun(not)问题,比如如何编辑“未来”修订版中的内容,然后一次性地发布一整套文档。 But hopefully you don't need that. 但希望你不需要那个。 :-) :-)

Your history table looks fine, except that you don't need the new_value field to perform the undo. 您的历史记录表看起来很好,除了您不需要new_value字段来执行撤消。 And yes, that' how "undo" is often implemented (the other alternative being Lennart's approach of putting a version number into all records). 是的,经常实施“如何”撤销“(另一种选择是Lennart将版本号放入所有记录的方法)。 The advantage of a separate journal table is that you don't need to deal with the version number in regular queries. 单独的日志表的优点是您不需要在常规查询中处理版本号。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM