6.14.2010

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

Выдалось время и появилось настроение. Чего-то нового сваял :)
  1. Первой целью стояло добавление тагов. Очень полезная вещь для организации однотипных данных. Посмотрел что предлагает интернет на этот вопрос. Есть проект django-tagging, типа все готовое прямо из коробки под джангу. Не понравился по нескольким причинам - я не понял как он работает (какие-то мега-архи приемы используются для управления тагами, типа прямых запросов к бд), куча не нужных мне наворотов, находится он бете и имеет баг лист, да и народ от него не в восторге. Вообще вопрос построения тагов теоретически выглядит довольно простым - так что было решено отказаться и самому написать простенькое решение, для целей самообучения имхо лучше. Когда понадобится облако тагов - я подумаю в сторону перехода на сей стандартный плагин. Мое требование такое - чтобы было одно поле на форме, в котором записаны все таги итема через пробел, при сохранении это поле split-ится по пробелам, и к итему добавляются/убираются теги. Как узналось в процессе - это de.li.cious style таги.
  2. Отправной точкой является модель тага
    class Tag(models.Model):
        name = models.CharField('keyword, bookmark or term', max_length=100)
    
    и отношение many-to-many к модели purchase
    class Purchase(models.Model):
        ...
        tags = models.ManyToManyField('Tag')
    
  3. Обновляем схему базы. У нас появляется новая таблица Тагов, и таблица отношения таг-покупка (по foreign-ключам из тагов и покупок). Нужно заметить что таблица PurchaseItem не меняется, значит все правильно.
  4. Таким образом получилась следующая штука, которую лучше всего опробовать на shell-е:
    from homebudget.purchases.models import Purchase, Tag
    t = Tag(name='beer')
    t.save()
    p = Purchase.objects.filter(name=u'туборг')[0]
    p.tags.add(t)
    p.save()
    [i.name for i in p.tags.all()]
    [i.name for i in t.purchases_set.all()]
    
    Нужно заметить, что пока объект Purchase не материализован (не сохранен) в базе, у него нет поля tags. И наоборот, пока Tag не создан в базе у него нет поля purchases_set.
  5. Если сейчас оставить все как есть и посмотреть что находится на вьюхах добавления и редактирования, то может стать плохо. Так как я использовал ModelForm для генерации html-формы для Purchase - для всех полей подставилась стандартная реализация. И для поля ManyToManyField она тоже есть, и выглядит прям отвратительно (тупой селект). Минусы - совершенно не подходит под мое представление, и добавить новый тег - нужна отдельная форма. Так что нах стандарты!
  6. В PurchaseItemForm добавляем новое собственное поле, и удаляем из представление оригинальное поле tag модели PurchaseItem
    from django.forms import CharField
    class PurchaseItemForm(ModelForm):
        ...
        tags_inner = CharField()
        ...
        class Meta:
            model = Purchase
            exclude = ('tags',)
    
  7. Правильное поле есть, но оно никак не обрабатывается. Сначала мы его заполним данными. Как мне кажется - лучше всего это сделать через override функции __init__ формы. Сначала вызываем родной __init__ (в котором как мы сделали - уже нет оригинального поля tags), потом заполняем собственное поле tags_inner тагами, разделенными пробелами. Проверка на null-ы нужна как раз для создания и редактирования (пока объект Purchase не создан, у него нет поля tags).
    class PurchaseItemForm(ModelForm):
        ...
        def __init__(self, *args, **kwargs):
            super(PurchaseItemForm, self).__init__(*args, **kwargs)
            if kwargs.has_key('instance'):
                inst = kwargs['instance']
                if inst is not None and inst.pk is not None:
                    self.fields["tags_inner"].initial = " ".join([i.name for i in inst.tags.all()])
    
  8. Теперь сохранение. Я выбрал решение через override функции save (которая вызывается при сохранении данных формы из вьюхи). Сначала - обязательно вызываем базовый метод (чтобы объект Purchase наверняка сохранился в базе), потом - вытаскиваем из cleaned_data значения поля tags_inner. Далее понятные операции - split по пробелам, получаем все таги буквенно, потом создаем новые, удаляем несуществующие, и сохраняем финально.
    def save(self, *args, **kwargs):
            inst = super(PurchaseItemForm, self).save(*args, **kwargs)
            tags = self.cleaned_data["tags_inner"].strip().split(' ')
            for tag in tags:
                if len(Tag.objects.filter(name=tag))==0:
                    new_tag = Tag(name=tag)
                    new_tag.save()
                if len(inst.tags.filter(name=tag))==0:
                    inst.tags.add(Tag.objects.get(name=tag))
            for tag in inst.tags.all():
                if tag.name not in tags:
                    inst.tags.remove(tag)
            inst.save()
    
  9. Ну вроде бы с реализацией все - можно пробовать. Какие минусы сейчас есть: отсутствует валидация, отсутствует зачистка тагов к которым не привязан не один итем, поле tags теперь обязательное для заполнения, решение деревянное и не гибкое. Запишу в todo :) А пока нужно наполнить тагам базу.
  10. После того как база заполнена можно сделать вьюху для просмотра статистики по тагам. Во views.py добавляем purchases_by_tag
    def purchases_by_tag(request, tag_name):
        try:
            tag = Tag.objects.get(name=tag_name)
        except:
            raise Http404
        total = Decimal('0.0')
        for item in tag.purchase_set.all():
            total += item.price_total
        return render_to_response('purchases/tag_view.html', {
            'total': total,
            'tag': tag,
        })
    
    Добавляем новый темплейт tag_view.html
    <p>Tag: {{tag.name}}</p>
    <ul>
        {% for item in tag.purchase_set.all %}
            <li>
                {{item.name}}
                <b>{{item.price_total}}</b>
                {{item.purchase_date}}
                <a href='/purchases/edit/{{item.pk}}/'>Edit</a>
            </li>
        {% endfor %}
    </ul>
    <p>Count: {{tag.purchase_set.count}}</p>
    <p>Total: {{total}}</p>
    
    Добавляем новое правило урлов в urls.py:
    urlpatterns = patterns(
        ...
        (r'^purchases/tags/(?P\w+)/$', 'purchases_by_tag'),
    )
    
    Теперь по ссылке http://127.0.0.1:8000/purchases/tags/beer/ можно узнать когда, и на какую в сумму мы выпили пива :)
  11. Можно навести красивости, типа в daily view добавить в каждую строчку таги связанные с покупкой, типа
    Tags:
       {% for tag in p.tags.all %}
       <span>{{tag.name}}</span>
       {% endfor %}
    
  12. Для реализации тагов мне в один момент понадобился дебаг, использовал для этого модуль logging, очень удобный надо сказать, вот так его заставить работать (output идет в консоль локального сервера, что архи удобно):
    import logging
    logging.basicConfig(
        level = logging.DEBUG,
        format = '%(asctime)s %(levelname)s %(message)s',
    )
    
    а вот так легко использовать:
    logging.debug(p.tags)
    
  13. Еще по мелочам, из области рефакторинга, наткнулся на мега функцию locals - она возвращает все локальные переменные из области видимости, в виде словаря 'имя переменной'-'значение'. Это позволило значительно сократить код на render_to_response, до:
    return render_to_response('purchases/month_view.html', {
        'view_date': view_date,
        'dict': day_price,
        'total': month_total,
    })
    
    после:
    return render_to_response('purchases/month_view.html', locals())
    
    И это в каждой вьюхе, очень удобно. Конечно же пришлось поправить все темплейты и вьюхи на предмет именования переменных, но так все таки намного лучше.
  14. На последок добавил Master Page - по крайней мере так оно обзывается в ASP.NET. Нужно это для двух вещей - чтобы все страницы имели общую часть - в мастере, и чтобы меньше писать кода. После коротких поисков нашел что это в джанге реализуется через наследование темплейтов читать тут. В общем по простому: добавил master.html
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
        <link rel="stylesheet" href="content/css/main.css" />
        <title>Homebudget site: {% block title %} manage yourself {% endblock %}</title>
    {% block head %}{% endblock %}
    </head>
    
    <body>
    
    <div id="content">
    {% block content %}
    {% endblock %}
    </div>
    
    </body>
    </html>
    
    Теперь он будет у меня основным шаблоном. Любой другой шаблон может его расширять и дополнять. Все элементы block - можно использовать в дочернем шаблоне. Пока я полностью не разобрался, но то что есть сейчас меня устраивает. Вот например новый шаблон tag_view.html
    {% extends "purchases/master.html" %}
    
    {% block title %} view purchases by tag {% endblock%}
    
    {% block content %}
    <p>Tag: {{tag.name}}</p>
    <ul>
        {% for item in tag.purchase_set.all %}
            <li>
                {{item.name}}
                <b>{{item.price_total}}</b>
                {{item.purchase_date}}
                <a href='/purchases/edit/{{item.pk}}/'>Edit</a>
            </li>
        {% endfor %}
    </ul>
    <p>Count: {{tag.purchase_set.count}}</p>
    <p>Total: {{total}}</p>
    {% endblock %}
    
  15. Кстати говоря, в мастере используется ссылка на css-документ, он должен лежать в папке обозначенной в конфигурации как MEDIA_ROOT, и для этого пути должно быть правило в урл-реврайтере... Для чего оно нужно? чтобы в можно было вынести весь статический контент в отдельный сайт, необремененный обработчиками джанги и тп. В settings.py:
    MEDIA_ROOT = '/home/username/projects/homebudget/content/'
    ...
    MEDIA_URL = '/content/'
    
    И в urls.py
    from django.conf import settings
    if settings.LOCAL_DEVELOPMENT:
        urlpatterns += patterns("django.views",
            url(r"%s(?P.*)/$" % settings.MEDIA_URL[1:], "static.serve", { "document_root": settings.MEDIA_ROOT, })
        )
    
    Переменная LOCAL_DEVELOPMENT заведена на будующее.

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

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