Django-Юг с 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 (и части "Пользовательские поля / Самоанализ"), но не на 100% ясно, что мне нужно сделать, чтобы заставить поля работать.

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

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

в мой models.py, который позволил./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

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

кроме того, промежуточное программное обеспечение aud_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 ответа

Решение

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

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

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

При миграции на юг не только хранятся имена полей в ваших моделях, но также тип и аргументы, которые ему передаются. В результате Юг должен понять, какие параметры заданы полем, а какие следует сохранить.

Поэтому, когда вы создаете правило, подобное этому:

add_introspection_rules([], ["^audit_log\.models\.fields\.LastUserField"])

Чем Юг создаст таблицу с таким столбцом:

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

Как мы это исправим?

Что ж, мы можем сказать Сауту, что мы передаем эти аргументы как значения по умолчанию, чтобы он мог спокойно их игнорировать.

Итак, мы создаем набор правил, таких как это:

rules = [(                                          
    (fields.LastUserField,),                        
    [],                                             
    {                                               
        'to': ['rel.to', {'default': User}],        
        'null': ['null', {'default': True}],        
    },                                              
)]   
add_introspection_rules(                           
    rules,                                         
    ['^audit_log\.models\.fields\.LastUserField'], 
)       

Из-за этого Юг теперь понимает, как хранить параметры и какие параметры необходимо игнорировать. Таким образом, новое определение поля будет таким:

(
  'action_user',
  self.gf('audit_log.models.fields.LastUserField')(
    related_name='_somemodel_audit_log_entry'
  )
),

Как мы видим, related_name все еще здесь, но to а также null параметры исчезли. Так что теперь мы можем смело применять миграцию без конфликтов.

Другие вопросы по тегам