Django: пишем свое расширение приложения

Одна из сильных сторон Django, это наличие большого количества готовых приложений (applications) для него. С другой стороны, часто функциональность этих приложений или недостаточна, или немного не такая, как нам бы хотелось. Возникает необходимость расширить приложение, добавив свою функциональность. Давайте попробуем это сделать.

Описывать буду кратко, без всех подробностей, так, чтобы было ясно общее направление, но не более.

Постановка задачи

Давайте, для примера, возьмем блог, построенный на основе Diario. Что мы хотим? Мы хотим, для каждой статьи в блоге добавить свои заголовки "author", "keywords" "description" так, чтобы можно бы было для каждой статьи иметь свое содержание этих заголовков.

Давайте составим план, что для этого нужно будет сделать. Итак, нам нужно:

  • Добавить свою модель, которая будет содержать нужные нам поля.
  • Заменить в сайте администратора ("админке") имеющийся интерфейс добавления/редактирования постов своим, включающим добавленные нами поля.
  • Добавить вывод наших полей в контекст соответствующих темплейтов.
  • Изменить темплейты для вывода нужных заголовков.

Создание нового приложения

Для порядка, новую функциональность удобно добавлять в виде приложения Django. Так оно будет в отдельном директории и вообще, в духе Django.

Создаем приложение:

python manage.py startapp myblog_seo

Добавляем наше приложение в файле settings.py , в список установленных приложений нашего проекта:

INSTALLED_APPS = (
...
'blog.myblog_seo',
)

Как вы заметили, наш проект называется незатейливо: blog

Добавление модели

Давайте теперь создадим модель. Вот исходный код нашей модели:

# -*- coding: utf-8 -*-
from django.contrib.sites.models import Site
from django.db.models import permalink
from django.utils.translation import ugettext_lazy as _

from django.db import models
from diario.models import Entry
fdrom diario.managers import PublishedManager, CurrentSitePublishedManager

class MyBlogSeo(Entry):
    author = models.CharField(max_length=100,blank=True,null=True)
    keywords = models.CharField(max_length=1024,blank=True,null=True)
    description = models.CharField(max_length=1024,blank=True,null=True)

    # managers
    objects   = models.Manager()
    published = PublishedManager()
    on_site   = CurrentSiteManager('publish_on')
    published_on_site = CurrentSitePublishedManager('publish_on')

    class Meta:
        verbose_name = "Пост"
        verbose_name_plural = "Посты"

from django.db.models import signals
from diario.signals import convert_body_to_html, update_draft_date
signals.pre_save.connect(convert_body_to_html, sender=MyBlogSeo)
signals.pre_save.connect(update_draft_date, sender=MyBlogSeo)
  

Что здесь интересного?

  • Наш класс MyBlogSeo наследник класса Entry Diario.
  • Внутри нашего класса добавлен класс Meta для того, чтобы в админке у наших записей были такие заголовки, какие мы хотим.
  • Обратите внимание на то, что в конце файла models необходимо добавить подключение сигналов, указав в качестве сендера нашу модель, вместо модели Entry Diario. Дело в том, что в Diario пост попадает сначала в поле body_source, а затем, при сохранении, копируется тело поста из поля body_source в поле body таблицы базы данных. Без этих строчек, введенная информация не попадет по назначению.

Проверим нашу модель и создадим таблицы в базе данных:

 python manage.py sqlall myblog_seo
 python manage.py syncdb
  

Подключим нашу модель к "админке"

Для этого в директории нашего приложения создадим файл admin.py, следующего содержания:

# -*- coding: utf-8 -*-

from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from blog.myblog_seo.models import MyBlogSeo # Добавили
from diario.admin import EntryAdmin
from diario.settings import HAS_TAG_SUPPORT

if HAS_TAG_SUPPORT:
    TAG_FIELD = ['tags']
else:
    TAG_FIELD = []

class MyBlogSeoAdmin(admin.ModelAdmin): # Изменили название класса на свое
    date_hierarchy = 'pub_date'
    fieldsets = (
        # (None, {
        (_('SEO'), {
            'fields': ['title', 'slug', 'pub_date', 'body_source'] + TAG_FIELD
        }),
        (_('Status'), {
            'fields': ('is_draft', 'publish_on')
        }),
        (_('Discussion'), {
            'fields': ('enable_comments',)
        }),
        (_('SEO'), {'fields': ('author','keywords','description',)}), # Добавили
        (_('Advanced options'), {
            'classes': ('collapse',),
            'fields': ('markup',),
        })
    )
    list_display   = (['title','author',
                       'pub_date', 'is_draft',
                       'enable_comments',])
    list_filter   = ('is_draft', 'publish_on', 'enable_comments', 'markup')
    prepopulated_fields = {'slug': ('title',)}
    radio_fields = {'markup': admin.VERTICAL}
    search_fields = ['title', 'slug', 'body']

admin.site.register(MyBlogSeo,MyBlogSeoAdmin)  # Изменили классы на свои
  

Содержимое этого файла, это скопированное содержимое файла lib/python2.6/site-packages/diario/admin.py, с небольшими изменениями. Изменения:

  • Добавили импорт нашей модели.
  • В fieldsets добавили перечень полей нашей модели.
  • В строке регистрации изменили названия файлов на свои.

Отлично, теперь у нас в админке появился раздел для добавления/редактирования постов с полями из нашей модели.

В этой точке нужно проделать парочку дополнительных телодвижений, решить некоторые проблемы.

Первая проблема - два раздела в админке

Теперь у нас в админке два раздела для редактирования одного и того же, старый раздел, для редактирования постов блога и новый, для для редактирования постов блога с добавленными нами полями. Что не есть красиво. Решаем ее так, в файле urls.py нашего проекта добавляем строку:

admin.autodiscover()         # После этой строки добавлять
admin.site.unregister(Entry) # Добавлено
  

Добавленная строка разрегистрирует класс Entry и в админке останется только наш раздел, с добавочными полями.

Вторая проблема - название раздела в админке

Админка генерит названия разделов из названий приложений. Не всегда это есть хорошо, так как то, что удобно для названия приложения, бывает удобным в качестве названия раздела админки. Нужно как-то поменять название нашего раздела. Для этого переопределим темплейт админки.

Копируем файл темплейта админки в директорию, где лежат наши темплейты.

Из: lib/python2.6/site-packages/django/contrib/admin/templates/admin/index.html

В: blog/templates/admin/index.html

Подредактируем темплейт, и получим то, что хотим в названии:
    	{% ifnotequal app.name "Myblog_Seo" %}
            <caption><a href="{{ app.app_url }}" class="section"$gt;
		{% blocktrans with app.name as name %}
		    {{ name }}
		{% endblocktrans %}</a></caption>
	{% endifnotequal %}

	{% ifequal app.name "Myblog_Seo" %}
            <caption><a href="{{ app.app_url }}" class="section">Блог</a></caption>
	{% endifequal %}
  

Третья проблема, не видны старые посты

Новые посты добавляется нормально, старые - не видны. Берем любой клиент используемой базы данных, подключаемся и в таблице, соответствующей нашей модели, в поле entry_ptr_id добавляем значения, которые берем из таблицы diario_entry, поле id. Все, старые записи появились.

Изменения во View

Тперь, нам нужно как-то передать информацию из базы, ну или в терминах Django, модели, в темплейт, чтобы там вставить ее в желаемое место. Для этого придется переопределить вью. План действий такой:

  • Копируем соотвествующий вью из diario в свой вью.
  • Корректируем его, так как нужно.
  • В файле urls.py вставляем с нужным регулярным выражением вызов нашего вью так, чтобы он вызывался раньше, чем штатный вью.

Наш вью:

# -*- coding: utf-8 -*-

from django.views.generic import date_based, list_detail
#from diario.models import Entry
from blog.myblog_seo.models import MyBlogSeo

def entry_detail(request, *args, **kwargs):
    kwargs['date_field'] = 'pub_date'
    kwargs['slug_field'] = 'slug'
    if request.user.has_perm('diario.change_entry'):
        kwargs['allow_future'] = True
        #kwargs['queryset'] = Entry.on_site.all()    # Было
        kwargs['queryset'] = MyBlogSeo.on_site.all() # Стало
    else:
        kwargs['allow_future'] = False
        # kwargs['queryset'] = Entry.published_on_site.all()    # Было
        kwargs['queryset'] = MyBlogSeo.published_on_site.all() # Стало
        
    return date_based.object_detail(request, *args, **kwargs)
  

Сам этот вью делает следующее, он получает на вход параметры, с которыми вызывается, добавляет в эти параметры свои значения, и в конце вызывает встроенный в Django стандартный вью со параметрами, в которые включены наши добавки.

Мы заменяем вызов менеджера модели Entry на вызов менеджеров нашей модели MyBlogSeo. Поэтому вызов менеджера возвратит queryset, нашей модели, вместо модели Diario. Этот Queryset будет кроме полей оригинальной модели, будет включать добавленные в нашей модели поля. Из темплейта доступ к нашим полям будет такой же, как и к полям Diario:

    {{ entry.<имя поля> }}
  

Добавленяи в файл urls.py:

urlpatterns = patterns('',
         ...
                       url(
        regex  = '^(?P\d{4})/(?P[0-9]{2})/(?P\d{2})/(?P[-\w]+)/$',
        view   = 'blog.myblog_seo.views.entry_detail',
        kwargs = dict(info_dict, month_format='%m'),
        name   = 'diario-entry'
        ),
                       # Diario
                       (r'^blog/', include('diario.urls.entries')),
                       (r'^', include('diario.urls.entries')),
       ...
                       )

  

Добавляем показ заголовков в темплейт

Остался последний удар. Прописать показ того, что мы хотим в темплейты. Это у же не просто, а очень просто. В темплейте для просмотре темы переопределяем блок, ответственный за показ заголовков отдаваемой страницы и вставляем в этот блок показ наших заголовков:

    <meta name="author" content="{{ entry.author }}" />
    <meta name="keywords" content="{{ entry.keywords }}" />
    <meta name="description" content="{{ entry.description }}" />
  

Заключение

Конечно, описанный мною путь, он не единственно возможный, но мне он кажется наиболее естественным и вытекающим из идеологии Python и Django.

Опубликовано: September 19, 2010

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


Имя: Gordano

Возможно прозвучит банально, однако всё равно большое человеческое спасибо. Статья написана в духе замечательных книг серии "Head First", т.е. читая статью приходится спотыкаться о разные нюансы вместе с автором и опять же вместе с ним искать решения. Django заметно усилил свои позиции у нас в стране, во многом благодаря таким авторам.

Пользуясь случаем передаю Хабра людям привет )



Имя: sergej f.

Spasibo!



Имя: Олег

Отличная статья, большое спасибо



Имя: Oleg

Статья на первый взляд интересная, но для начинающих можно было бы исходники приложить.



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

Имя:

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