- Первой целью стояло добавление тагов. Очень полезная вещь для организации однотипных данных. Посмотрел что предлагает интернет на этот вопрос. Есть проект django-tagging, типа все готовое прямо из коробки под джангу. Не понравился по нескольким причинам - я не понял как он работает (какие-то мега-архи приемы используются для управления тагами, типа прямых запросов к бд), куча не нужных мне наворотов, находится он бете и имеет баг лист, да и народ от него не в восторге. Вообще вопрос построения тагов теоретически выглядит довольно простым - так что было решено отказаться и самому написать простенькое решение, для целей самообучения имхо лучше. Когда понадобится облако тагов - я подумаю в сторону перехода на сей стандартный плагин. Мое требование такое - чтобы было одно поле на форме, в котором записаны все таги итема через пробел, при сохранении это поле split-ится по пробелам, и к итему добавляются/убираются теги. Как узналось в процессе - это de.li.cious style таги.
- Отправной точкой является модель тага
class Tag(models.Model): name = models.CharField('keyword, bookmark or term', max_length=100)и отношение many-to-many к модели purchaseclass Purchase(models.Model): ... tags = models.ManyToManyField('Tag') - Обновляем схему базы. У нас появляется новая таблица Тагов, и таблица отношения таг-покупка (по foreign-ключам из тагов и покупок). Нужно заметить что таблица PurchaseItem не меняется, значит все правильно.
- Таким образом получилась следующая штука, которую лучше всего опробовать на 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. - Если сейчас оставить все как есть и посмотреть что находится на вьюхах добавления и редактирования, то может стать плохо. Так как я использовал ModelForm для генерации html-формы для Purchase - для всех полей подставилась стандартная реализация. И для поля ManyToManyField она тоже есть, и выглядит прям отвратительно (тупой селект). Минусы - совершенно не подходит под мое представление, и добавить новый тег - нужна отдельная форма. Так что нах стандарты!
- В PurchaseItemForm добавляем новое собственное поле, и удаляем из представление оригинальное поле tag модели PurchaseItem
from django.forms import CharField class PurchaseItemForm(ModelForm): ... tags_inner = CharField() ... class Meta: model = Purchase exclude = ('tags',) - Правильное поле есть, но оно никак не обрабатывается. Сначала мы его заполним данными. Как мне кажется - лучше всего это сделать через 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()]) - Теперь сохранение. Я выбрал решение через 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() - Ну вроде бы с реализацией все - можно пробовать. Какие минусы сейчас есть: отсутствует валидация, отсутствует зачистка тагов к которым не привязан не один итем, поле tags теперь обязательное для заполнения, решение деревянное и не гибкое. Запишу в todo :) А пока нужно наполнить тагам базу.
- После того как база заполнена можно сделать вьюху для просмотра статистики по тагам. Во 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Теперь по ссылке http://127.0.0.1:8000/purchases/tags/beer/ можно узнать когда, и на какую в сумму мы выпили пива :)\w+)/$', 'purchases_by_tag'), ) - Можно навести красивости, типа в daily view добавить в каждую строчку таги связанные с покупкой, типа
Tags: {% for tag in p.tags.all %} <span>{{tag.name}}</span> {% endfor %} - Для реализации тагов мне в один момент понадобился дебаг, использовал для этого модуль logging, очень удобный надо сказать, вот так его заставить работать (output идет в консоль локального сервера, что архи удобно):
import logging logging.basicConfig( level = logging.DEBUG, format = '%(asctime)s %(levelname)s %(message)s', )а вот так легко использовать:logging.debug(p.tags)
- Еще по мелочам, из области рефакторинга, наткнулся на мега функцию 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())И это в каждой вьюхе, очень удобно. Конечно же пришлось поправить все темплейты и вьюхи на предмет именования переменных, но так все таки намного лучше. - На последок добавил 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 %} - Кстати говоря, в мастере используется ссылка на css-документ, он должен лежать в папке обозначенной в конфигурации как MEDIA_ROOT, и для этого пути должно быть правило в урл-реврайтере... Для чего оно нужно? чтобы в можно было вынести весь статический контент в отдельный сайт, необремененный обработчиками джанги и тп. В settings.py:
MEDIA_ROOT = '/home/username/projects/homebudget/content/' ... MEDIA_URL = '/content/'
И в urls.pyfrom django.conf import settings if settings.LOCAL_DEVELOPMENT: urlpatterns += patterns("django.views", url(r"%s(?PПеременная LOCAL_DEVELOPMENT заведена на будующее..*)/$" % settings.MEDIA_URL[1:], "static.serve", { "document_root": settings.MEDIA_ROOT, }) )
Посты по теме:

Комментариев нет:
Отправить комментарий