15 сентября 2010 г.

musicmans.ru | Как сделать сайт на Django | Жанры, направления, стили

Прошу прощения за долгое отсутствие.
Пишем следующее приложение. Я решил, что жанры, направления и стили; инструменты; композиции; исполнители будут у нас отдельными приложениями, потому что планирую, что они обрастут серьезной функциональностью.

Начнем с жанров, направлений и стилей. Создадим приложение genre, сразу создаем модель жанра.
Чтобы создать модель, нам надо определиться, что такое жанр собственно? К сожалению, в русском интернете, большая путница и мешанина из жанров, стилей и направлений. Нет ни исследований, ни достаточно устоявшихся критериев. Прочитав эту заметку начал задумываться структуре систематизации жанров и стилей. И главное, неплохо было бы найти уже существующую отлаженную структуру (Amazon). После долгих поисков (amg, amazon, mp3.com, discogs) остановился на варианте от amg.

Итак, напишем модель жанра, направления и стиля и заполним их структурой.

Так как я один, и у меня нет редакторов, то пришлось воспользоваться результатом чужих трудов и спарсить структуру жанров, направлений и стилей с amg. Надеюсь они на меня за это не в обиде.

Код приводить не буду, расскажу что использовал lxml, а также окружение проекта для записи жанров/стилей.

После того как данные в базе, сделаем initial data для приложения:

>python manage.py dumpdata genre > apps\genre\fixtures\initial_data.json

Теперь попробуем вывести дерево жанров/стилей.

На данном этапе понимаем, что вывод "детей" стилей сулит нам лавинообразные запросы к базе данных (select_related нам тоже не поможет, он не срабатывает для моделей с полями отношения ForeignKey с null=True ( father = models.ForeignKey('self', verbose_name=_(u'Родитель'), related_name='child_dirs_styles', null=True) ) ), поэтому воспользуемся приложением для хранения деревьев в базе данных django-treebeard (документация).

C:\>c:\Python26\Scripts\pip.exe install git+git://github.com/tabo/django-treebeard@8eb52a4f4274615e86a7572a8bab39b79d718b88

Добавляем 'treebeard' в INSTALLED_APPS. Если вы используете админку, то настройка немного посложней.

Воспользуемся моделью хранения деревьев Nested Sets. Не стоит пугаться последней ссылки, тем то и хорошо приложение treebeard, что за нас уже решен вопрос хранения деревьев в SQL базе. Нам лишь стоит воспользоваться набором функций.

Смотрим нашу модель:
  1. # -*- coding:utf-8 -*-  
  2. from django.db import models  
  3. from django.utils.translation import ugettext_lazy as _  
  4. from treebeard.ns_tree import NS_Node #@UnresolvedImport   
  5.   
  6. GENRE_DIR_STYLE = (  
  7.     (0, _('Музыка')),  
  8.     (1, _('Жанр')),  
  9.     (2, _('Направление')),  
  10.     (3, _('Стиль')),  
  11. )  
  12.   
  13. class GenreDirStyle(NS_Node):  
  14.     name = models.CharField(max_length=1000, verbose_name=_(u'Title'))  
  15.     name_ru = models.CharField(max_length=1000, verbose_name=_(u'Название'), blank=True, null=True)  
  16.     type = models.IntegerField(choices=GENRE_DIR_STYLE)  
  17.     description = models.CharField(max_length=10000, verbose_name=_(u'Описание'), blank=True)  
  18.   
  19.     class Meta:  
  20.         ordering = ["name"]  
  21.         verbose_name = _(u'Жанр, направление, стиль')  


Пробуем, работает ли миграции South с treebeard:

./manage.py schemamigration genre --auto

получаем сообщение следующего вида

? The field 'GenreDirStyle.lft' does not have a default specified, yet is NOT NULL.
? Since you are adding or removing this field, you MUST specify a default
? value to use for existing rows. Would you like to:
? 1. Quit now, and add a default to the field in models.py
? 2. Specify a one-off value to use for existing columns now
? Please select a choice:

South нас просит указать обязательное значение по умолчанию. Нажимаем 2, и значение 0.

Как создать дерево? Просто:
1. Создаем корень дерева.
pop_music = GenreDirStyle.add_root(name = "Популярная музыка", type = 0)#оно сразу сохраняется save()
2. Создаем жанр:
pop_music.add_child(name = "Rock", type = 1)

И здесь сталкиваемся с проблемой, в случае парсинга (см. выше) html и создания базы "на лету". Поэтому читаем basic-usage внимательней и переписываем примерно так:
  1. >>> get = lambda node_id: Category.objects.get(pk=node_id)  
  2. >>> root = Category.add_root(name='Computer Hardware')  
  3. >>> node = get(root.id).add_child(name='Memory')  
  4. >>> get(node.id).add_sibling(name='Hard Drives')  
  5. <category: category:="" hard="" drives="">  
  6. >>> get(node.id).add_sibling(name='SSD')  
  7. <category: category:="" ssd="">  
  8. >>> get(node.id).add_child(name='Desktop Memory')  
  9. <category: category:="" desktop="" memory="">  
  10. >>> get(node.id).add_child(name='Laptop Memory')  
  11. <category: category:="" laptop="" memory="">  
  12. >>> get(node.id).add_child(name='Server Memory')  
  13. <category: category:="" server="" memory="">  
  14. </category:></category:></category:></category:></category:>  

Не забудем обновить json данные, после изменения и миграций моделей.
Для вывода дерева жанров используем функцию get_tree.

  1. # -*- coding: utf-8 -*-  
  2. from annoying.decorators import render_to#@UnresolvedImport  
  3. from django.shortcuts import get_object_or_404  
  4.   
  5. from models import GenreDirStyle  
  6.  
  7. @render_to('genres/genre_tree.html')  
  8. def genre_tree(request):  
  9.   
  10.     pop_genre = GenreDirStyle.objects.get(name="Популярная музыка", type=0)  
  11.     classic_genre = GenreDirStyle.objects.get(name="Классическая музыка", type=0)  
  12.   
  13.     pop_tree = GenreDirStyle.get_tree(pop_genre)  
  14.     classic_tree = GenreDirStyle.get_tree(classic_genre)  
  15.      
  16.     return {  
  17.           'pop_tree': pop_tree,  
  18.           'classic_tree': classic_tree  
  19.           }   
  20.  
  21. @render_to('genres/genre_genre.html')  
  22. def genre_genre(request, genre_id):  
  23.       
  24.     genre = get_object_or_404(GenreDirStyle, id = int(genre_id))  
  25.   
  26.     return {  
  27.             'genre': genre  
  28.           }   

На данном этапе django-toolbar показывал замечательные 5 запросов за 13 мс. А вот общая генерация страницы занимала 6573.00 ms. Это очень долго, хотя при выключенном debug режиме ощутимо быстрее. Все упирается в рендеринг. Проэтому применим кеш в темплейте (на шесть часов, например):
  1. {% load cache %}  
  2. {% cache 21600 pop_tree_chache %}  
  3. {% for node in pop_tree %}  
  4.   {% include "genres/genre_node.html" %}  
  5. {% endfor %}  
  6. {% endcache %}  

А также включим на время (позже настроим memcached на сервере) кеширование в память, в settings/common.py:

CACHE_BACKEND = 'locmem:///'

Темплейты интуитивно понятны, покажу лишь темплейт жанра, включаемый в цикл вывода дерева.
  1. {% load dj_tags %}  
  2. <div style="padding-left:{{ node.get_depth|multiply:20|subtract:20 }}px;">  
  3. <h{{ node.type|add:"1"="" }}="">  
  4. <a href="{% url genre_genre node.pk %}">{% if node.get_depth > 1 %}{{ node.get_type_display }} {% endif %}{{ node.name }}</a>  
  5. </h{{>  
  6. </div>  

(Ужасно, blogger все сломал, смотрите здесь)
Обратите внимание на фильтр multiply и substract. Это не стандартные фильтры django, а написаные в нашем приложении dj_tags.
  1. # -*- coding: utf-8 -*-  
  2. from django import template  
  3. register=template.Library()  
  4.  
  5. @register.filter(name='multiply')  
  6. def multiply(value, arg):  
  7.     return int(value) * int(arg)  
  8.  
  9. @register.filter(name='subtract')  
  10. def subtract(value, arg):  
  11.     return int(value) - int(arg)  

Итак, мы познакомились с хранением деревьев в базе данных с django, их выводом, затронули кеширование, написали пару темплейт тегов.

Ну, окончание, как обычно, тесты, мерж, развертывание.




ps. Не забудем обновить django (>c:\Python26\Scripts\pip.exe install --upgrade Django и прописать в requirements.txt)!