В качестве хобби, я решил открыть для себя мир веб-разработки, попутно освещая свои успехи, а может и неудачи на пути к просветлению…

В настоящее время я публично описываю процесс создания веб-сайта с нуля на Django и Bootstrap, где задача разработать такие приложения как: аккаунты, вопросы, статьи и книги.

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

Создание веб-сайта с нуля на Django и Bootstrap. Книги. Список книг и их содержимое. Представления

Сергей Серов

Представления

В директории приложения открываем views.py и наполняем следующим:

from django.shortcuts import render as _render, get_object_or_404, redirect
from django.db.models import F
from django.conf import settings

from core.utils import pagination

from .models import Book, Chapter


def render(request, template_name, context=None, *args, **kwargs):
    context = context or {}
    context['BOOKS_SUBTITLE'] = settings.BOOKS_SUBTITLE
    return _render(request, template_name, context, *args, **kwargs)


def index_view(request):
    books = Book.public.all()
    return render(request, 'books/index.html', {
        'books': books,
    })


def book_view(request, slug, pk):
    # получаем все объекты для предварительного просмотра администратором
    queryset = Book.objects if request.user.is_superuser else Book.public
    book = get_object_or_404(queryset, pk=pk)
    if book.slug != slug:
        # делаем постоянный редирект, слаг был изменен
        return redirect(book, permanent=True)
    return render(request, 'books/book.html', {
        'book': book,
        'chapters': book.chapters(),
    })


def chapter_view(request, book_slug, book_pk, slug, pk):
    # получаем все объекты для предварительного просмотра администратором
    queryset = Chapter.objects if request.user.is_superuser else Chapter.public
    chapter = get_object_or_404(queryset.select_related(), pk=pk)
    if chapter.slug != slug or chapter.book.slug != book_slug or chapter.book.pk != int(book_pk):
        # делаем постоянный редирект, слаг был изменен или указан несуществующий pk книги
        return redirect(chapter, permanent=True)
    # добавляем еще один просмотр главе
    Chapter.public.filter(pk=pk).update(n_views=F('n_views') + 1)
    return render(request, 'books/chapter.html', {
        'chapter': chapter,
        'page': pagination(request, chapter.pages(), 1),
    })

Для начала мы модифицируем стандартную функцию вывода render, добавляя в нее контекст: подзаголовок приложения.

К слову, в приложении articles мы добавляем аналогичную функцию, но соответствующей настройкой приложения: ARTICLES_SUBTITLE.

Далее в index_view выводим список доступных на данный момент книг.

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

В начале мы определяем статус пользователя, если это администратор, мы сможем просмотреть даже неопубликованную на данный момент книгу.

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

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

Представление chapter_view очень похоже на предыдущее, но здесь стоит выделить несколько моментов.

В запросе мы используем select_related поскольку в шаблоне нам необходимо вывести название книги.

Далее проверяем не только слаг главы, но и книги и ее идентификатор, для актуальности ссылки.

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

Затем нам необходимо обновить счетчик просмотров у опубликованной главы.

За один запрос получаем содержимое количества просмотров и тут же увеличиваем его на единицу.

Функцию pagination мы описывали в приложении вопросов — это просто удобная обертка над родным средством пагинации.

Здесь мы получаем всего лишь один объект на страницу, но у нас появляются инструменты для работы с предыдущей и следующей страницами.

Более подробно мы все это рассмотрим в шаблонах.

Ленты обновлений книг и глав

В каталоге приложения создаем файл feeds.py:

from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed
from django.urls import reverse_lazy
from django.conf import settings

from .models import Book, Chapter


class BooksFeed(Feed):
    feed_type = Atom1Feed
    title = settings.SITE_TITLE
    link = reverse_lazy('books:index')
    subtitle = settings.BOOKS_SUBTITLE
    author_name = settings.SITE_AUTHOR

    def items(self):
        return Book.public.all()

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        return item.summary


class ChaptersBookFeed(Feed):
    feed_type = Atom1Feed
    subtitle = settings.BOOKS_SUBTITLE
    author_name = settings.SITE_AUTHOR

    def get_object(self, request, *args, **kwargs):
        return Book.public.get(pk=kwargs['pk'])

    def title(self, obj):
        return '{} | {}'.format(obj.title, settings.SITE_TITLE)

    def link(self, obj):
        return obj.get_absolute_url()

    def items(self, obj):
        return Chapter.public.filter(book_id=obj.pk).select_related()

    def item_title(self, item):
        return item.title

    def item_description(self, item):
        return item.summary

По просьбе заказчика убраны ограничения на вывод объектов и используются стандартные сортировки моделей, в том числе для вывода глав по порядку.

Карта книг и глав

В директории приложения создаем файл sitemaps.py:

from django.contrib.sitemaps import GenericSitemap

from .models import Book, Chapter

books_sitemaps = {
    'books': GenericSitemap({
        'queryset': Book.public,
        'date_field': 'updated',
    }, priority=1.0, changefreq=None),
    'books_chapters': GenericSitemap({
        'queryset': Chapter.public.select_related(),
        'date_field': 'updated',
    }, priority=0.9, changefreq=None),
}

Приложение Книги теперь является основным, приоритет выставлен 1.0 для книг и 0.9 для глав соответственно.

В приложении Статьи приоритет объектов изменяем на 0.8.

Маршруты

В каталоге приложения создаем файл urls.py:

from django.conf.urls import url

from . import views
from . import feeds

app_name = 'books'
urlpatterns = [
    url(r'^$', views.index_view, name='index'),
    url(r'^feed/$', feeds.BooksFeed(), name='feed'),
    url(r'^feed/(?P<pk>\d+)/$', feeds.ChaptersBookFeed(), name='book_feed'),
    url(r'^(?P<slug>[\w-]+)-(?P<pk>\d+)/$', views.book_view, name='book'),
    url(r'^(?P<book_slug>[\w-]+)-(?P<book_pk>\d+)/(?P<slug>[\w-]+)-(?P<pk>\d+)/$', views.chapter_view, name='chapter'),
]

В директории проекта переходим в каталог zero, затем открываем urls.py и приводим к следующему виду:

from django.conf.urls import url, include
from django.contrib import admin
from django.views.generic.base import RedirectView
from django.contrib.sitemaps.views import sitemap
from django.conf.urls.static import static
from django.conf import settings

from questions.sitemaps import questions_sitemaps
from articles.sitemaps import articles_sitemaps
from books.sitemaps import books_sitemaps

sitemaps = {}
sitemaps.update(questions_sitemaps)
sitemaps.update(articles_sitemaps)
sitemaps.update(books_sitemaps)

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^ckeditor/', include('ckeditor_uploader.urls')),
    url(r'^accounts/', include('accounts.urls')),
    url(r'^questions/', include('questions.urls')),
    url(r'^articles/', include('articles.urls')),
    url(r'^books/', include('books.urls')),
    url(r'^$', RedirectView.as_view(pattern_name='books:index'), name='index'),
    url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='sitemap'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


# Django Debug Toolbar

if settings.DEBUG:
    import debug_toolbar
    urlpatterns += [
        url(r'^__debug__/', include(debug_toolbar.urls)),
    ]

Сначала мы подключили маршруты приложения Книги и установили на него перенаправление с главной страницы сайта.

Затем добавили в карту сайта список книг и глав для поисковых систем.