django-south与django-audit-log的结合
我正在尝试对一个已有的应用进行 django-south迁移,目的是添加 django-audit-log,这样可以追踪用户对某个模块所做的更改。但是我遇到了一些比较严重的错误,特别是关于一个叫 action_user_id 的字段,它是一个 LastUserField(这个字段用来存储进行更改的用户)。
如果我从一个空的模型开始,我可以通过以下方式直接添加审计日志:
from audit_log.models.managers import AuditLog
...
class SomeModel(models.Model)
...
audit_log = AuditLog()
但是,简单地应用这个更改并在 django-south 中进行 schemamigration 时,我遇到了错误:
! Cannot freeze field 'myapp.mymodelauditlogentry.action_user'
! (this field has class audit_log.models.fields.LastUserField)
! South cannot introspect some fields; this is probably because they are custom
! fields. If they worked in 0.6 or below, this is because we have removed the
! models parser (it often broke things).
! To fix this, read http://south.aeracode.org/wiki/MyFieldsDontWork
我看过 MyFieldsDontWork 的维基页面(还有自定义字段/自省的部分),但不太清楚我需要做什么才能让这些字段正常工作。
我尝试在我的 models.py 中添加:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^audit_log\.models\.fields\.LastUserField"])
这样 ./manage.py schemamigration 就能生成一个迁移脚本,之前的错误也消失了。然而,当我尝试迁移(也就是应用这个迁移)时,又出现了以下错误:
Running migrations for myapp:
- Migrating forwards to 0004_auto__add_mymodelauditlogentry.
> my_app:0004_auto__add_mymodelauditlogentry
Traceback (most recent call last):
File "./manage.py", line 11, in <module>
execute_manager(settings)
File "/usr/local/lib/python2.6/dist-packages/Django-1.2.3-py2.6.egg/django/core/management/__init__.py", line 438, in execute_manager
utility.execute()
File "/usr/local/lib/python2.6/dist-packages/Django-1.2.3-py2.6.egg/django/core/management/__init__.py", line 379, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python2.6/dist-packages/Django-1.2.3-py2.6.egg/django/core/management/base.py", line 191, in run_from_argv
self.execute(*args, **options.__dict__)
File "/usr/local/lib/python2.6/dist-packages/Django-1.2.3-py2.6.egg/django/core/management/base.py", line 220, in execute
output = self.handle(*args, **options)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/management/commands/migrate.py", line 105, in handle
ignore_ghosts = ignore_ghosts,
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/migration/__init__.py", line 191, in migrate_app
success = migrator.migrate_many(target, workplan, database)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/migration/migrators.py", line 221, in migrate_many
result = migrator.__class__.migrate_many(migrator, target, migrations, database)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/migration/migrators.py", line 292, in migrate_many
result = self.migrate(migration, database)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/migration/migrators.py", line 125, in migrate
result = self.run(migration)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/migration/migrators.py", line 93, in run
south.db.db.current_orm = self.orm(migration)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/migration/migrators.py", line 246, in orm
return migration.orm()
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/utils.py", line 62, in method
value = function(self)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/migration/base.py", line 422, in orm
return FakeORM(self.migration_class(), self.app_label())
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/orm.py", line 46, in FakeORM
_orm_cache[args] = _FakeORM(*args)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/orm.py", line 125, in __init__
self.models[name] = self.make_model(app_label, model_name, data)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/orm.py", line 318, in make_model
field = self.eval_in_context(code, app, extra_imports)
File "/usr/local/lib/python2.6/dist-packages/South-0.7.3-py2.6.egg/south/orm.py", line 236, in eval_in_context
return eval(code, globals(), fake_locals)
File "<string>", line 1, in <module>
File "/usr/local/lib/python2.6/dist-packages/django_audit_log-0.2.1-py2.6.egg/audit_log/models/fields.py", line 12, in __init__
super(LastUserField, self).__init__(User, null = True, **kwargs)
TypeError: __init__() got multiple values for keyword argument 'null'
编辑(12/20中午): 如果我在 models.py 中添加了这些行,我可以应用 schemamigration。
from south.modelsinspector import add_introspection_rules, add_ignored_fields
add_ignored_fields(["^audit_log\.models\.fields\.LastUserField"])
但这样一来,audit_log 中间件就无法工作,因为在 myapp_mymodelauditlogentry 中没有 action_user_id 整数字段来引用 "auth_user" 的 "id"。于是我手动应用了 SQL(使用 sqlite 语法;通过 sqliteman 在新创建的数据库上获得的)。
ALTER TABLE "myapp_mymodelauditlogentry" ADD "action_user_id" integer REFERENCES "auth_user" ("id");
这样就可以了。如果有人能解释我该如何在 django-south 的迁移/自省上下文中做到这一点,而不需要依赖原始的数据库 SQL,我会很感激,并愿意给予奖励。
另外,我为 action_user_id 创建了一个索引。我注意到正常创建模型时,会生成一个名为
CREATE INDEX "myapp_mymodelauditlogentry_26679921" ON "myapp_mymodelauditlogentry" ("action_user_id")
的索引。我查到哈希值 26679921 是基于字段名称生成的,使用的代码是 '%x' % (abs(hash(('action_user_id',))) % 4294967296L,)
,并不是基于其他任何东西(所以应该总是 _26679921,除非数据库需要将长名称截断)。我不确定索引的名称是否真的重要,但我想还是要小心点。
2 个回答
这里终于有了答案和解释。
在使用South进行迁移时,它不仅会存储你模型中字段的名称,还会记录字段的类型和传递给它的参数。这样一来,South就需要理解哪些参数是字段提供的,哪些是需要存储的。
所以当你创建一个这样的规则时:
add_introspection_rules([], ["^audit_log\.models\.fields\.LastUserField"])
那么South会创建一个包含这样的列的表:
(
'action_user',
self.gf('audit_log.models.fields.LastUserField')(
related_name='_somemodel_audit_log_entry',
null=True,
to=orm['auth.User'],
)
),
如你所见,这个表里有一个related_name
参数,一个null
参数和一个to
参数。现在我们来看看字段的定义:
class LastUserField(models.ForeignKey):
"""
A field that keeps the last user that saved an instance
of a model. None will be the value for AnonymousUser.
"""
def __init__(self, **kwargs):
models.ForeignKey.__init__(self, User, null=True, **kwargs)
#print kwargs
#super(LastUserField, self).__init__(User, null = True, **kwargs)
def contribute_to_class(self, cls, name):
super(LastUserField, self).contribute_to_class(cls, name)
registry = registration.FieldRegistry(self.__class__)
registry.add_field(cls, self)
我们在这里看到了什么?ForeignKey
的第一个参数是user(第一个参数就是to
属性)。第二个参数(也是硬编码的)是null
参数。当你应用迁移时,South
和你的字段都会试图设置这些参数。
然后你就会遇到这个错误:
TypeError: __init__() got multiple values for keyword argument 'null'
我们该如何解决这个问题呢?
其实,我们可以告诉South,我们传递这些参数是作为默认值的,这样它就可以安全地忽略它们。
所以我们创建一组这样的规则:
rules = [(
(fields.LastUserField,),
[],
{
'to': ['rel.to', {'default': User}],
'null': ['null', {'default': True}],
},
)]
add_introspection_rules(
rules,
['^audit_log\.models\.fields\.LastUserField'],
)
因此,South现在明白了如何存储这些参数,以及哪些参数需要被忽略。所以新的字段定义将是这样的:
(
'action_user',
self.gf('audit_log.models.fields.LastUserField')(
related_name='_somemodel_audit_log_entry'
)
),
正如我们所看到的,related_name
仍然在这里,但to
和null
参数已经消失了。现在我们可以安全地应用迁移,而不会出现冲突。
尽管我按照@WoLpH的回答中的步骤操作,但我还是没法创建迁移文件。我不得不修改了audit_log/models/fields.py这个文件。下面是我修改后的LastUserField字段的样子:
class LastUserField(models.ForeignKey):
"""
A field that keeps the last user that saved an instance
of a model. None will be the value for AnonymousUser.
"""
def __init__(self, **kwargs):
kwargs.pop('null', None)
kwargs.pop('to', None)
super(LastUserField, self).__init__(User, null = True, **kwargs)
def contribute_to_class(self, cls, name):
super(LastUserField, self).contribute_to_class(cls, name)
registry = registration.FieldRegistry(self.__class__)
registry.add_field(cls, self)
在我不得不采取这种方法之前,我在models.py文件中添加了以下内容(但没有效果):
rules = [((fields.LastUserField,),
[],
{
'to': ['rel.to', {'default': User}],
'null': ['null', {'default': True}],
},)]
# Add the rules for the `LastUserField`
add_introspection_rules(rules, ['^audit_log\.models\.fields\.LastUserField'])
有没有什么建议可以让我避免这种“黑客”式的解决办法呢?