Пишем следующее приложение. Я решил, что жанры, направления и стили; инструменты; композиции; исполнители будут у нас отдельными приложениями, потому что планирую, что они обрастут серьезной функциональностью.
Начнем с жанров, направлений и стилей. Создадим приложение 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')
- <category: category:="" hard="" drives="">
- >>> get(node.id).add_sibling(name='SSD')
- <category: category:="" ssd="">
- >>> get(node.id).add_child(name='Desktop Memory')
- <category: category:="" desktop="" memory="">
- >>> get(node.id).add_child(name='Laptop Memory')
- <category: category:="" laptop="" memory="">
- >>> get(node.id).add_child(name='Server Memory')
- <category: category:="" server="" memory="">
- </category:></category:></category:></category:></category:>
Не забудем обновить 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 %}
- <div style="padding-left:{{ node.get_depth|multiply:20|subtract:20 }}px;">
- <h{{ node.type|add:"1"="" }}="">
- <a href="{% url genre_genre node.pk %}">{% if node.get_depth > 1 %}{{ node.get_type_display }} {% endif %}{{ node.name }}</a>
- </h{{>
- </div>
(Ужасно, 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)!