6.27.2010

хроники django или homebudget part 5

За посленее время много чего натворил в проекте, попытаюсь все изложить.

  • довел до ума систему тегов
  • добавил South к проекту для удобной работы
  • переделал логику добавления/редактирования покупок
  • сделал красивое отображение месяца

Так судя по всему, первые два пункта получились достаточно обширными, так что сегодня только про них. HTML и javascript останутся на завтра.

South - удобный процесс миграции БД

  1. для начала я обновил себе django до последней дев-сборки (в репозиториях убунты все-то лежит 1.1).
    sudo apt-get remove python-django
    cd ~/devtools
    svn co http://code.djangoproject.com/svn/django/trunk/ django
    cd django
    python setup.py install
  2. стоит проверить что django проекты не потеряли работоспособность (:
  3. Устанавливаем south (для этого нужен mercurial)
    cd ~/devtools
    hg clone http://bitbucket.org/andrewgodwin/south/
    cd south
    sudo python setup.py install
  4. Добавляем south в installed_app (в settings.py)
    INSTALLED_APPS = (
         ...
         'south',
    )
  5. для параноиков - backup/restore существующей бд.
    • Backup:
      sudo -u databaseuser pg_dump homebudget > dump_db
    • Restore:
      sudo -u postres psql homebudget2 < dump_db
  6. создаем таблицы для south в проекте (это последний раз когда мы сделали syncdb(: )
    ./manage.py syncdb
  7. теперь если бд пустая (т.е. таблицы для проектов не созданы), добавляем проекты в installed_apps и выполняем
    ./manage.py schemamigration purchases --initial
    ./manage.py migrate purchases
    
  8. если данные в ней уже были, то: создаем начальную точку миграции
    ./manage.py schemamigration purchases --initial
    
    исправляем скрипт создания (комментим все строки с созданием таблиц)
    purchases/migration/0001_initial.py
    
    накатываем "псевдо" изменения
    ./manage.py migrate purchases
    
    теперь закоменченные строки можна убрать - для истории
  9. основной сценарий работы c South
    • делаем изменения в модели
    • создаем автоматическую точку миграции
      ./manage.py schemamigration purchases --auto
      
    • проверяем сгенеренный код в purchases/migration/XXXX_randomname.py
    • если что-то не так - правим код в методах backward и forward
    • в объекте orm находится замечательная django orm :)
      allpurchases = orm.Purchase.objects.all()
      
    • накатывание изменений на БД
      ./manage.py migrate purchases
    • посмотреть все миграции (звездочками помечены те которые успешно прошли)
      ./manage.py migrate purchases --list
    • если хочется смигрировать только данные
      ./manage.py datamigration purchases my_super_migration_of_data
    • или можно даже вот так
      ./manage.py schemamigration purchases --empty my_super_migration_of_data

Таги

  1. как уже упоминалось ранее таги были добавлены самопальные. Работают примерно так - в модели Purchase есть поле ManyToMany к модели Tag. Дефолтная реализация ManyToMany откидывается. В форму насильно добавляется новое CharField, переопределяются методы загрузки и сохранения формы, в которых как раз идут операции с полем m2m и CharField. Все делается в ручную. Это не никак не нравилось, так как чтобы переиспользовать эти таги пришлось бы прибегать к copy-paste, что не есть хорошо. В общем решил продолжить поиски компонент:
    • django-tagging - жутко наворочанный пакет, вроде бы есть все что нужно, все очень кастомно. но того как оно сделано я вряд ли скоро пойму. много-много черной магии для выполнения простых операций. много, очень много sql-я (без orm-а). Сразу же был откинут.
    • django-taggit проект поменьше чем предыдущий. но черная магия опять таки присутствует. в некоторых моментах я тупо не разобрался.
    • tagsfield от Ивана Сагалаева. Вот оно то что нужно. Идея проста - наследуемся от ManyToManyField и заменяем дефолтный widget на собственный. Кода ну очень мало, все предельно понятно. Выбрал я его. Конечно же понадобились изменения.
  2. первым делом копируем к себе tagsfield, добавляем его в installed_apps
  3. изначально проект работает так: в html записывается список всех тагов и список всех тагов объекта, через javascript все это управляется - юзер может добавлять и убирать теги обекта, может добавлять новые. Когда форма сабмитится список активных тагов сохраняется в поле m2m.
  4. для моего подхода (de.li.cious style таги) было сделано следующее
  5. поле MultipleHiddenInput было заменено на TextInput. Поясняю - при сабмите приходил список значений hidden-input-ов, а мне нужно только одно text-input поле.
    class TagsFormField(forms.Field):
        text_widget = forms.TextInput
  6. собственно загрузка списка тагов (метод render):
    'value_tags': " ".join(value_tags)
  7. из модели убрал поле created и переименовал модель в BaseTag (зачем - ниже)
    class BaseTag(models.Model):
        value = models.CharField(max_length=50)
        norm_value = models.CharField(max_length=50, editable=False)
  8. сохранение:
    def save_form_data(self, instance, data):
        splitted = data.strip().split(' ')
        tags = [self._get_tag(value) for value in splitted]
        setattr(instance, self.attname, tags)
  9. шаблон виджета:
    <div id="{{ id }}" class="tagsfield">
    <input type="text" name="{{ name }}" value="{{ value_tags }}" />
    </div>
  10. удалил больше не нужные файлы и зачистил js и css файлы. Еще были изменения, но все незначительные, так что лучше их смотреть в репозитории.
  11. теперь об одном ограничении оригинального кода - все таги хранятся в одной таблице. То есть если навешать таги на два класса - таги у них будут общие. Плохо. Для решения использовал еще один уровень абстракции, в приложении Purchases создал новый класс PurchaseTag который является наследником BaseTag. Теперь в бд будет две таблицы - одна для BaseTag из приложения TagField и одна для PurchaseTag из приложения Purchase. Таги для них не пересекаются :) Ну и собственно TagField одинаково хорошо работает для PurchaseTag класса. Тем более в последствии можно кастомизировать PurchaseTag и добавить к нему дополнительные поля. В файле purchases/models.py (старый класс Tag уже затерт)
    from homebudget.tagsfield import fields
    from homebudget.tagsfield.models import BaseTag
    class Purchase(models.Model):
        ... 
        tags = fields.TagsField('PurchaseTag')
    
    class PurchaseTag(BaseTag):
        pass
    
    class PurchaseItemForm(ModelForm):
        class Meta:
            model = Purchase
            exclude = ('purchase_date',)
    
  12. Ну и в коде везде где обращались к Tag теперь обращаемся к PurchaseTag, ну и из мелочи - поле name изменилось на value.
  13. Как проходила миграция данных (замена старых таго на новые). У меня база уже забита. Собственно для этого и прикручивался South (см. выше)
  14. Сначала была таблица для приложения TagField (чтобы не было никаких проблем)
  15. Потом было добавлена модель PurchaseTag в проект Purchases, в модель Purchase было добавлено поле ntags = fields.TagsField('PurchaseTag'), старое поле tags = models.ManyToManyField('Tag') не удаляя.
  16. Первая попытка миграции. Не работает (!!) и South отправляет на страницы http://south.aeracode.org/wiki/MyFieldsDontWork http://south.aeracode.org/docs/customfields.html#extending-introspection. Типа дописывайте сами себе правила на собственные поля. Ну ведь использовано поле отнаследованное от m2m, значит и вести оно должно себя также.. Значит спиздим определение ManyToManyField из кода south-а
    rules = [
    (
        (TagsField,),
        [],
        {
            "to": ["rel.to", {}],
            "symmetrical": ["rel.symmetrical", {"default": True}],
            "related_name": ["rel.related_name", {"default": None}],
            "db_table": ["db_table", {"default": None}],
            "through": ["rel.through", {"ignore_if_auto_through": True}],
        },
    ),
    ]
    from south.modelsinspector import add_introspection_rules
    add_introspection_rules(rules, ["^homebudget\.tagsfield\."])
    Поставим это прямо возле определения TagsField-а.
  17. Теперь создание точки миграции должно пройти успешно. Теперь у нас есть два поля тагов в модели. Делаем миграцию данных. Для каждого старого тега добавляем новый, и повторяем все соотношения. В общем выглядит это так:
    class Migration(DataMigration):
        def forwards(self, orm):
            plist = orm.Purchase.objects.all()
            for p in plist:
                old_tags = p.tags.all()
                for i in old_tags:
                    norm_value = i.name.lower()
                    tag, created = orm.PurchaseTag.objects.get_or_create(
                        norm_value = norm_value,
                        defaults = {'value': i.name}
                    )
                    p.ntags.add(tag)
                    p.tags.remove(i)
                p.save()
            old_tags = orm.Tag.objects.all()
            for i in old_tags:
                i.delete()
        def backwards(self, orm):
            "Write your backwards methods here."
            plist = orm.Purchase.objects.all()
            for p in plist:
                old_tags = p.ntags.all()
                for i in old_tags:
                    tag, created = orm.Tag.objects.get_or_create(
                        name = i.value,
                    )
                    p.tags.add(tag)
                    p.ntags.remove(i)
                p.save()
            old_tags = orm.PurchaseTag.objects.all()
            for i in old_tags:
                i.delete()
  18. Теперь удаляем модель Tag (старую) и поле tags. Апдейтим базу через South.
  19. Все хорошо, только поле называется ntags... Переименовываем поле. Смотрим в скрипт сгенеренный South-ом - не порядок, он пытается удалить нашу таблицу и создать новую. Исправляем его ошибку и накатываем изменения
    class Migration(SchemaMigration):
        def forwards(self, orm):
            db.rename_table('purchases_purchase_ntags', 'purchases_purchase_tags')
    
        def backwards(self, orm):
            db.rename_table('purchases_purchase_tags', 'purchases_purchase_ntags')
  20. Теперь все окей :)

Посты по теме:

Комментариев нет: