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 базе. Нам лишь стоит воспользоваться набором функций.

Смотрим нашу модель:

# -*- coding:utf-8 -*-
from django.db import models
from django.utils.translation import ugettext_lazy as _
from treebeard.ns_tree import NS_Node #@UnresolvedImport

GENRE_DIR_STYLE = (
(0, _('Музыка')),
(1, _('Жанр')),
(2, _('Направление')),
(3, _('Стиль')),
)

class GenreDirStyle(NS_Node):
name = models.CharField(max_length=1000, verbose_name=_(u'Title'))
name_ru = models.CharField(max_length=1000, verbose_name=_(u'Название'), blank=True, null=True)
type = models.IntegerField(choices=GENRE_DIR_STYLE)
description = models.CharField(max_length=10000, verbose_name=_(u'Описание'), blank=True)

class Meta:
ordering = ["name"]
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 внимательней и переписываем примерно так:

>>> get = lambda node_id: Category.objects.get(pk=node_id)
>>> root = Category.add_root(name='Computer Hardware')
>>> node = get(root.id).add_child(name='Memory')
>>> get(node.id).add_sibling(name='Hard Drives')

>>> get(node.id).add_sibling(name='SSD')

>>> get(node.id).add_child(name='Desktop Memory')

>>> get(node.id).add_child(name='Laptop Memory')

>>> get(node.id).add_child(name='Server Memory')


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


# -*- coding: utf-8 -*-
from annoying.decorators import render_to#@UnresolvedImport
from django.shortcuts import get_object_or_404

from models import GenreDirStyle

@render_to('genres/genre_tree.html')
def genre_tree(request):

pop_genre = GenreDirStyle.objects.get(name="Популярная музыка", type=0)
classic_genre = GenreDirStyle.objects.get(name="Классическая музыка", type=0)

pop_tree = GenreDirStyle.get_tree(pop_genre)
classic_tree = GenreDirStyle.get_tree(classic_genre)

return {
'pop_tree': pop_tree,
'classic_tree': classic_tree
}

@render_to('genres/genre_genre.html')
def genre_genre(request, genre_id):

genre = get_object_or_404(GenreDirStyle, id = int(genre_id))

return {
'genre': genre
}

На данном этапе django-toolbar показывал замечательные 5 запросов за 13 мс. А вот общая генерация страницы занимала 6573.00 ms. Это очень долго, хотя при выключенном debug режиме ощутимо быстрее. Все упирается в рендеринг. Проэтому применим кеш в темплейте (на шесть часов, например):

{% load cache %}
{% cache 21600 pop_tree_chache %}
{% for node in pop_tree %}
{% include "genres/genre_node.html" %}
{% endfor %}
{% endcache %}

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

CACHE_BACKEND = 'locmem:///'

Темплейты интуитивно понятны, покажу лишь темплейт жанра, включаемый в цикл вывода дерева.

{% load dj_tags %}


(Ужасно, blogger все сломал, смотрите здесь)
Обратите внимание на фильтр multiply и substract. Это не стандартные фильтры django, а написаные в нашем приложении dj_tags.

# -*- coding: utf-8 -*-
from django import template
register=template.Library()

@register.filter(name='multiply')
def multiply(value, arg):
return int(value) * int(arg)

@register.filter(name='subtract')
def subtract(value, arg):
return int(value) - int(arg)

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

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




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