django-south с django-audit-log

Я пытаюсь выполнить миграцию django-south в существующее приложение, чтобы добавить django-audit-log к нему (для отслеживания инициированных пользователем изменений модуля), но я сталкиваюсь с существенные ошибки. В частности, с полем action_user_id, которое является LastUserField (в котором хранится пользователь, указавший отслеживаемое изменение).

Если бы я начинал с пустой модели, я мог бы просто добавить audit_log через:

from audit_log.models.managers import AuditLog
...
class SomeModel(models.Model)
    ...
    audit_log = AuditLog()

Применяя это простое изменение и выполняя миграцию схемы в django-south, я получаю ошибку:

 ! 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 (и части Custom Fields/Introspection), но не на 100% ясно, что мне нужно сделать, чтобы заставить поля работать.

Я пытаюсь добавить:

from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^audit_log\.models\.fields\.LastUserField"])

к моим моделям.py, который позволил схеме миграции ./manage.py создать сценарий миграции с предыдущей ошибкой, исчезает. Однако, когда я пытаюсь выполнить миграцию (применить миграцию), я получаю следующие ошибки:

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'

EDIT (12/20, полдень): я могу применить миграцию схемы, если добавлю строки в models.py

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, если только база данных не требует усечения длинного имени). Я не уверен, что имена индексов когда-либо имеют значение; но хотел быть в безопасности.


person dr jimbob    schedule 17.12.2010    source источник
comment
Проблема только в том, что юг не знает, как переносить ваши поля. Для настраиваемых полей вам нужно добавить свои собственные правила самоанализа, где вы указываете югу, какие параметры важны, а какие можно игнорировать. Если у меня будет время сегодня вечером, я напишу пример того, как заставить самоанализ работать должным образом.   -  person Wolph    schedule 20.12.2010
comment
@WoLpH: Да, проблема в том, чтобы заставить самоанализ работать, но, к счастью, была документация для самоанализа. Мне, новичку в schema-migrations/django-south (как и в django-audit-log), это было непонятно. Первые несколько вещей, которые я пробовал, не работали с LastUserField. Я был бы признателен, если бы вы могли заставить работать самоанализ (так что мне не нужно вручную вводить SQL), но, честно говоря, я перестал пытаться сам и переключился на другие проблемы.   -  person dr jimbob    schedule 23.12.2010
comment
Я был очень занят, поэтому у меня еще не было времени, чтобы дать вам правильный ответ. Пожалуйста, потерпите меня еще немного :) (или кто-нибудь другой может это объяснить). Короче говоря, это не работает, потому что вы не сказали югу о параметрах, поэтому он игнорирует их, что приводит к тому, что они передаются дважды.   -  person Wolph    schedule 23.12.2010


Ответы (2)


Несмотря на то, что я использовал шаги в ответе @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'])

Любые предложения о том, что я мог бы сделать, чтобы избежать этого хакерства?

person takinbo    schedule 12.01.2011
comment
Я автор django-audit-log. Страница проекта на GitHub находится здесь: github.com/Atomidata/django-audit-log . Для пакета было бы полезно, если бы люди сообщали о проблемах и исправлениях, подобных этому. Я не использовал приложение вместе с югом, но я посмотрю на проблему. Я создал и создал проблему по этому поводу и рассмотрю лучший способ исправить это. - person Vasil; 04.04.2011

Вот, наконец, ответ (и объяснение).

При миграции 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 — это пользователь (первый аргумент — это атрибут 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 исчезли. Итак, теперь мы можем безопасно применить миграцию, не получая конфликтов.

person Wolph    schedule 25.12.2010